Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/types/src/home.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { TLogoProps } from "./common";
import { TIssuePriorities } from "./issues";

export type TRecentActivityFilterKeys = "all item" | "issue" | "page" | "project";
export type THomeWidgetKeys = "quick_links" | "recent_activity" | "stickies";
export type THomeWidgetKeys = "quick_links" | "recents" | "my_stickies" | "quick_tutorial" | "new_at_plane";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure consistent naming for the recent activity widget.

Here, you define "recents" as part of THomeWidgetKeys. However, in loader.tsx, the enum uses "recent_activity". Please unify the naming (e.g., use all "recents" or all "recent_activity") to avoid potential type mismatches.


export type THomeWidgetProps = {
workspaceSlug: string;
Expand Down Expand Up @@ -69,7 +69,7 @@ export type TLinkIdMap = {
};

export type TWidgetEntityData = {
key: string;
key: THomeWidgetKeys;
name: string;
is_enabled: boolean;
sort_order: number;
Expand Down
18 changes: 11 additions & 7 deletions web/core/components/home/home-dashboard-widgets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,20 @@ import { DashboardQuickLinks } from "./widgets/links";
import { ManageWidgetsModal } from "./widgets/manage";

const WIDGETS_LIST: {
[key in THomeWidgetKeys]: { component: React.FC<THomeWidgetProps>; fullWidth: boolean };
[key in THomeWidgetKeys]: { component: React.FC<THomeWidgetProps> | null; fullWidth: boolean };
} = {
quick_links: { component: DashboardQuickLinks, fullWidth: false },
recent_activity: { component: RecentActivityWidget, fullWidth: false },
stickies: { component: StickiesWidget, fullWidth: false },
recents: { component: RecentActivityWidget, fullWidth: false },
my_stickies: { component: StickiesWidget, fullWidth: false },
new_at_plane: { component: null, fullWidth: false },
quick_tutorial: { component: null, fullWidth: false },
Comment on lines +15 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Confirm unified widget keys usage.

Defining "recents", "my_stickies", etc., is consistent with the updated THomeWidgetKeys. Nonetheless, be sure to align any usage of "recent_activity" in the codebase (e.g., EWidgetKeys.RECENT_ACTIVITY) with "recents" to prevent runtime errors.

};

export const DashboardWidgets = observer(() => {
// router
const { workspaceSlug } = useParams();
// store hooks
const { toggleWidgetSettings, showWidgetSettings } = useHome();
const { toggleWidgetSettings, widgetsMap, showWidgetSettings, orderedWidgets } = useHome();

if (!workspaceSlug) return null;

Expand All @@ -36,9 +38,11 @@ export const DashboardWidgets = observer(() => {
handleOnClose={() => toggleWidgetSettings(false)}
/>

{Object.entries(WIDGETS_LIST).map(([key, widget]) => {
const WidgetComponent = widget.component;
if (widget.fullWidth)
{orderedWidgets.map((key) => {
const WidgetComponent = WIDGETS_LIST[key]?.component;
const isEnabled = widgetsMap[key]?.is_enabled;
if (!WidgetComponent || !isEnabled) return null;
if (WIDGETS_LIST[key]?.fullWidth)
return (
<div key={key} className="lg:col-span-2">
<WidgetComponent workspaceSlug={workspaceSlug.toString()} />
Expand Down
27 changes: 14 additions & 13 deletions web/core/components/home/root.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// components
import useSWR from "swr";
import { ContentWrapper } from "@plane/ui";
import { EmptyState } from "@/components/empty-state";
import { TourRoot } from "@/components/onboarding";
Expand All @@ -11,7 +11,7 @@ import { PRODUCT_TOUR_COMPLETED } from "@/constants/event-tracker";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useCommandPalette, useUserProfile, useEventTracker, useDashboard, useProject, useUser } from "@/hooks/store";
import { useCommandPalette, useUserProfile, useEventTracker, useProject, useUser } from "@/hooks/store";
import { useHome } from "@/hooks/store/use-home";
import useSize from "@/hooks/use-window-size";
import { IssuePeekOverview } from "../issues";
Expand All @@ -29,12 +29,20 @@ export const WorkspaceHomeView = observer(() => {
const { data: currentUser } = useUser();
const { data: currentUserProfile, updateTourCompleted } = useUserProfile();
const { captureEvent } = useEventTracker();
const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard();
const { toggleWidgetSettings } = useHome();
const { toggleWidgetSettings, fetchWidgets } = useHome();
const { joinedProjectIds, loader } = useProject();

const [windowWidth] = useSize();

useSWR(
workspaceSlug ? `HOME_DASHBOARD_WIDGETS_${workspaceSlug}` : null,
workspaceSlug ? () => fetchWidgets(workspaceSlug?.toString()) : null,
{
revalidateIfStale: true,
revalidateOnFocus: false,
revalidateOnReconnect: true,
}
);

const handleTourCompleted = () => {
updateTourCompleted()
.then(() => {
Expand All @@ -48,13 +56,6 @@ export const WorkspaceHomeView = observer(() => {
});
};

// fetch home dashboard widgets on workspace change
useEffect(() => {
if (!workspaceSlug) return;

fetchHomeDashboardWidgets(workspaceSlug?.toString());
}, [fetchHomeDashboardWidgets, workspaceSlug]);

// TODO: refactor loader implementation
return (
<>
Expand All @@ -63,7 +64,7 @@ export const WorkspaceHomeView = observer(() => {
<TourRoot onComplete={handleTourCompleted} />
</div>
)}
{homeDashboardId && joinedProjectIds && (
{joinedProjectIds && (
<>
{joinedProjectIds.length > 0 || loader ? (
<>
Expand Down
4 changes: 2 additions & 2 deletions web/core/components/home/user-greetings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ export const UserGreetingsView: FC<IUserGreetingsView> = (props) => {
</div>
</h6>
</div>
{/* <button
<button
onClick={handleWidgetModal}
className="flex items-center gap-2 font-medium text-custom-text-300 justify-center border border-custom-border-200 rounded p-2 my-auto mb-0"
>
<Shapes size={16} />
<div className="text-xs font-medium">Manage widgets</div>
</button> */}
</button>
</div>
);
};
2 changes: 1 addition & 1 deletion web/core/components/home/widgets/links/links.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { FC, useEffect, useState } from "react";
import { observer } from "mobx-react";
// computed
import { useHome } from "@/hooks/store/use-home";
import { EWidgetKeys, WidgetLoader } from "../loaders";
import { AddLink } from "./action";
import { ProjectLinkDetail } from "./link-detail";
import { TLinkOperations } from "./use-links";
import { EWidgetKeys, WidgetLoader } from "../loaders";

export type TLinkOperationsModal = Exclude<TLinkOperations, "create">;

Expand Down
2 changes: 1 addition & 1 deletion web/core/components/home/widgets/links/root.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { observer } from "mobx-react";
import useSWR from "swr";
import { THomeWidgetProps } from "@plane/types";
import { useHome } from "@/hooks/store/use-home";
import { LinkCreateUpdateModal } from "./create-update-link-modal";
import { ProjectLinkList } from "./links";
import { useLinks } from "./use-links";
import { THomeWidgetProps } from "@plane/types";

export const DashboardQuickLinks = observer((props: THomeWidgetProps) => {
const { workspaceSlug } = props;
Expand Down
2 changes: 1 addition & 1 deletion web/core/components/home/widgets/loaders/loader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// components
import { RecentActivityWidgetLoader } from "./recent-activity";
import { QuickLinksWidgetLoader } from "./quick-links";
import { RecentActivityWidgetLoader } from "./recent-activity";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Potential mismatch between widget keys.

You are importing RecentActivityWidgetLoader and mapping it to EWidgetKeys.RECENT_ACTIVITY = "recent_activity", but in other files, the key "recents" is used. This mismatch may cause rendering or typing issues. Please ensure the widget key is consistently named across all files.


// types

Expand Down
28 changes: 19 additions & 9 deletions web/core/components/home/widgets/manage/widget-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,38 +14,45 @@ import { attachInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree

import { observer } from "mobx-react";
// plane helpers
import { useParams } from "next/navigation";
import { createRoot } from "react-dom/client";
// ui
import { InstructionType } from "@plane/types";
import { InstructionType, TWidgetEntityData } from "@plane/types";
// components
import { DropIndicator, ToggleSwitch } from "@plane/ui";
// helpers
import { cn } from "@plane/utils";
import { useHome } from "@/hooks/store/use-home";
import { WidgetItemDragHandle } from "./widget-item-drag-handle";
import { getCanDrop, getInstructionFromPayload } from "./widget.helpers";

type Props = {
widgetId: string;
isLastChild: boolean;
widget: any;
handleDrop: (self: DropTargetRecord, source: ElementDragPayload, location: DragLocationHistory) => void;
handleToggle: (workspaceSlug: string, widgetKey: string, is_enabled: boolean) => void;
};

export const WidgetItem: FC<Props> = observer((props) => {
// props
const { isLastChild, widget, handleDrop } = props;
const { widgetId, isLastChild, handleDrop, handleToggle } = props;
const { workspaceSlug } = useParams();
//state
const [isDragging, setIsDragging] = useState(false);
const [instruction, setInstruction] = useState<InstructionType | undefined>(undefined);

//ref
const elementRef = useRef<HTMLDivElement>(null);
// hooks
const { widgetsMap } = useHome();
// derived values
const widget = widgetsMap[widgetId] as TWidgetEntityData;

// drag and drop
useEffect(() => {
const element = elementRef.current;

if (!element) return;
const initialData = { id: widget.id, isGroup: false };
const initialData = { id: widget.key, isGroup: false };
return combine(
draggable({
element,
Expand All @@ -62,7 +69,7 @@ export const WidgetItem: FC<Props> = observer((props) => {
getOffset: pointerOutsideOfPreview({ x: "0px", y: "0px" }),
render: ({ container }) => {
const root = createRoot(container);
root.render(<div className="rounded bg-custom-background-100 text-sm p-1 pr-2">{widget.title}</div>);
root.render(<div className="rounded bg-custom-background-100 text-sm p-1 pr-2">{widget.key}</div>);
return () => root.unmount();
},
nativeSetDragImage,
Expand Down Expand Up @@ -104,7 +111,7 @@ export const WidgetItem: FC<Props> = observer((props) => {
})
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [elementRef?.current, isDragging, isLastChild, widget.id]);
}, [elementRef?.current, isDragging, isLastChild, widget.key]);

return (
<div className="">
Expand All @@ -120,9 +127,12 @@ export const WidgetItem: FC<Props> = observer((props) => {
>
<div className="flex items-center">
<WidgetItemDragHandle sort_order={widget.sort_order} isDragging={isDragging} />
<div>{widget.title}</div>
<div>{widget.key.replaceAll("_", " ")}</div>
</div>
{/* <ToggleSwitch /> */}
<ToggleSwitch
value={widget.is_enabled}
onChange={() => handleToggle(workspaceSlug.toString(), widget.key, !widget.is_enabled)}
/>
</div>
{isLastChild && <DropIndicator isVisible={instruction === "reorder-below"} />}
</div>
Expand Down
37 changes: 24 additions & 13 deletions web/core/components/home/widgets/manage/widget-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,14 @@ import {
DropTargetRecord,
ElementDragPayload,
} from "@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types";
import { observer } from "mobx-react";
import { setToast, TOAST_TYPE } from "@plane/ui";
import { useHome } from "@/hooks/store/use-home";
import { WidgetItem } from "./widget-item";
import { getInstructionFromPayload, TargetData } from "./widget.helpers";

const WIDGETS_LIST = [
{ id: 1, title: "quick links" },
{ id: 2, title: "recents" },
{ id: 3, title: "stickies" },
];
export const WidgetList = ({ workspaceSlug }: { workspaceSlug: string }) => {
const { reorderWidget } = useHome();
export const WidgetList = observer(({ workspaceSlug }: { workspaceSlug: string }) => {
const { orderedWidgets, reorderWidget, toggleWidget } = useHome();

const handleDrop = (self: DropTargetRecord, source: ElementDragPayload, location: DragLocationHistory) => {
const dropTargets = location?.current?.dropTargets ?? [];
Expand All @@ -30,20 +27,34 @@ export const WidgetList = ({ workspaceSlug }: { workspaceSlug: string }) => {

if (!sourceData.id) return;
if (droppedId) {
reorderWidget(workspaceSlug, sourceData.id, droppedId, instruction); /** sequence */
try {
reorderWidget(workspaceSlug, sourceData.id, droppedId, instruction); /** sequence */
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Widget reordered successfully.",
});
} catch {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Error occurred while reordering widget.",
});
}
}
};

return (
<div className="my-4">
{WIDGETS_LIST.map((widget, index) => (
{orderedWidgets.map((widget, index) => (
<WidgetItem
key={widget.id}
widget={widget}
isLastChild={index === WIDGETS_LIST.length - 1}
key={widget}
widgetId={widget}
isLastChild={index === orderedWidgets.length - 1}
handleDrop={handleDrop}
handleToggle={toggleWidget}
/>
))}
</div>
);
};
});
16 changes: 8 additions & 8 deletions web/core/components/home/widgets/manage/widget.helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
import { IFavorite, InstructionType, IPragmaticPayloadLocation, TDropTarget } from "@plane/types";
import { InstructionType, IPragmaticPayloadLocation, TDropTarget, TWidgetEntityData } from "@plane/types";

export type TargetData = {
id: string;
Expand All @@ -11,7 +11,7 @@ export type TargetData = {
/**
* extracts the Payload and translates the instruction for the current dropTarget based on drag and drop payload
* @param dropTarget dropTarget for which the instruction is required
* @param source the dragging favorite data that is being dragged on the dropTarget
* @param source the dragging widget data that is being dragged on the dropTarget
* @param location location includes the data of all the dropTargets the source is being dragged on
* @returns Instruction for dropTarget
*/
Expand All @@ -37,26 +37,26 @@ export const getInstructionFromPayload = (
instruction = dropTargetData.isChild ? "reorder-above" : "make-child";
}

// if source that is being dragged is a group. A group cannon be a child of any other favorite,
// if source that is being dragged is a group. A group cannon be a child of any other widget,
// hence if current instruction is to be a child of dropTarget then reorder-above instead
if (instruction === "make-child" && sourceData.isGroup) instruction = "reorder-above";

return instruction;
};

/**
* This provides a boolean to indicate if the favorite can be dropped onto the droptarget
* This provides a boolean to indicate if the widget can be dropped onto the droptarget
* @param source
* @param favorite
* @param widget
* @returns
*/
export const getCanDrop = (source: TDropTarget, favorite: IFavorite | undefined) => {
export const getCanDrop = (source: TDropTarget, widget: TWidgetEntityData | undefined) => {
const sourceData = source?.data;

if (!sourceData) return false;

// a favorite cannot be dropped on to itself
if (sourceData.id === favorite?.id) return false;
// a widget cannot be dropped on to itself
if (sourceData.id === widget?.key) return false;

return true;
};
2 changes: 1 addition & 1 deletion web/core/components/home/widgets/recents/filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { ChevronDown } from "lucide-react";
import { TRecentActivityFilterKeys } from "@plane/types";
import { CustomMenu } from "@plane/ui";
import { cn } from "@plane/utils";
import { TRecentActivityFilterKeys } from "@plane/types";

export type TFiltersDropdown = {
className?: string;
Expand Down
Loading
Loading