diff --git a/packages/types/src/home.d.ts b/packages/types/src/home.d.ts index 52d057a10c2..c93fb448008 100644 --- a/packages/types/src/home.d.ts +++ b/packages/types/src/home.d.ts @@ -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"; export type THomeWidgetProps = { workspaceSlug: string; @@ -69,7 +69,7 @@ export type TLinkIdMap = { }; export type TWidgetEntityData = { - key: string; + key: THomeWidgetKeys; name: string; is_enabled: boolean; sort_order: number; diff --git a/web/core/components/home/home-dashboard-widgets.tsx b/web/core/components/home/home-dashboard-widgets.tsx index c7daac656f1..6accd1853f3 100644 --- a/web/core/components/home/home-dashboard-widgets.tsx +++ b/web/core/components/home/home-dashboard-widgets.tsx @@ -12,18 +12,20 @@ import { DashboardQuickLinks } from "./widgets/links"; import { ManageWidgetsModal } from "./widgets/manage"; const WIDGETS_LIST: { - [key in THomeWidgetKeys]: { component: React.FC; fullWidth: boolean }; + [key in THomeWidgetKeys]: { component: React.FC | 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 }, }; export const DashboardWidgets = observer(() => { // router const { workspaceSlug } = useParams(); // store hooks - const { toggleWidgetSettings, showWidgetSettings } = useHome(); + const { toggleWidgetSettings, widgetsMap, showWidgetSettings, orderedWidgets } = useHome(); if (!workspaceSlug) return null; @@ -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 (
diff --git a/web/core/components/home/root.tsx b/web/core/components/home/root.tsx index cd836eced73..44cd9356614 100644 --- a/web/core/components/home/root.tsx +++ b/web/core/components/home/root.tsx @@ -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"; @@ -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"; @@ -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(() => { @@ -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 ( <> @@ -63,7 +64,7 @@ export const WorkspaceHomeView = observer(() => {
)} - {homeDashboardId && joinedProjectIds && ( + {joinedProjectIds && ( <> {joinedProjectIds.length > 0 || loader ? ( <> diff --git a/web/core/components/home/user-greetings.tsx b/web/core/components/home/user-greetings.tsx index c30f9cdd293..d9e68880123 100644 --- a/web/core/components/home/user-greetings.tsx +++ b/web/core/components/home/user-greetings.tsx @@ -51,13 +51,13 @@ export const UserGreetingsView: FC = (props) => { - {/* */} + ); }; diff --git a/web/core/components/home/widgets/links/links.tsx b/web/core/components/home/widgets/links/links.tsx index 260a2301f03..194b1dfc121 100644 --- a/web/core/components/home/widgets/links/links.tsx +++ b/web/core/components/home/widgets/links/links.tsx @@ -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; diff --git a/web/core/components/home/widgets/links/root.tsx b/web/core/components/home/widgets/links/root.tsx index e10c642e40f..b5d96678fc7 100644 --- a/web/core/components/home/widgets/links/root.tsx +++ b/web/core/components/home/widgets/links/root.tsx @@ -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; diff --git a/web/core/components/home/widgets/loaders/loader.tsx b/web/core/components/home/widgets/loaders/loader.tsx index 0edb95346b0..9fed9cafaaf 100644 --- a/web/core/components/home/widgets/loaders/loader.tsx +++ b/web/core/components/home/widgets/loaders/loader.tsx @@ -1,6 +1,6 @@ // components -import { RecentActivityWidgetLoader } from "./recent-activity"; import { QuickLinksWidgetLoader } from "./quick-links"; +import { RecentActivityWidgetLoader } from "./recent-activity"; // types diff --git a/web/core/components/home/widgets/manage/widget-item.tsx b/web/core/components/home/widgets/manage/widget-item.tsx index fb101a093f8..453a96588c0 100644 --- a/web/core/components/home/widgets/manage/widget-item.tsx +++ b/web/core/components/home/widgets/manage/widget-item.tsx @@ -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 = 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(undefined); - //ref const elementRef = useRef(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, @@ -62,7 +69,7 @@ export const WidgetItem: FC = observer((props) => { getOffset: pointerOutsideOfPreview({ x: "0px", y: "0px" }), render: ({ container }) => { const root = createRoot(container); - root.render(
{widget.title}
); + root.render(
{widget.key}
); return () => root.unmount(); }, nativeSetDragImage, @@ -104,7 +111,7 @@ export const WidgetItem: FC = observer((props) => { }) ); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [elementRef?.current, isDragging, isLastChild, widget.id]); + }, [elementRef?.current, isDragging, isLastChild, widget.key]); return (
@@ -120,9 +127,12 @@ export const WidgetItem: FC = observer((props) => { >
-
{widget.title}
+
{widget.key.replaceAll("_", " ")}
- {/* */} + handleToggle(workspaceSlug.toString(), widget.key, !widget.is_enabled)} + />
{isLastChild && } diff --git a/web/core/components/home/widgets/manage/widget-list.tsx b/web/core/components/home/widgets/manage/widget-list.tsx index 3f9004f5231..526d51ba913 100644 --- a/web/core/components/home/widgets/manage/widget-list.tsx +++ b/web/core/components/home/widgets/manage/widget-list.tsx @@ -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 ?? []; @@ -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 (
- {WIDGETS_LIST.map((widget, index) => ( + {orderedWidgets.map((widget, index) => ( ))}
); -}; +}); diff --git a/web/core/components/home/widgets/manage/widget.helpers.ts b/web/core/components/home/widgets/manage/widget.helpers.ts index c629f1fbccd..a72ee8028ae 100644 --- a/web/core/components/home/widgets/manage/widget.helpers.ts +++ b/web/core/components/home/widgets/manage/widget.helpers.ts @@ -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; @@ -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 */ @@ -37,7 +37,7 @@ 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"; @@ -45,18 +45,18 @@ export const getInstructionFromPayload = ( }; /** - * 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; }; diff --git a/web/core/components/home/widgets/recents/filters.tsx b/web/core/components/home/widgets/recents/filters.tsx index 84391775b16..da7e9e39ab3 100644 --- a/web/core/components/home/widgets/recents/filters.tsx +++ b/web/core/components/home/widgets/recents/filters.tsx @@ -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; diff --git a/web/core/components/home/widgets/recents/index.tsx b/web/core/components/home/widgets/recents/index.tsx index 9773f7bc7fc..4db8633961f 100644 --- a/web/core/components/home/widgets/recents/index.tsx +++ b/web/core/components/home/widgets/recents/index.tsx @@ -3,18 +3,18 @@ import { useRef, useState } from "react"; import { observer } from "mobx-react"; // types +import useSWR from "swr"; +import { Briefcase, FileText } from "lucide-react"; import { TActivityEntityData, THomeWidgetProps, TRecentActivityFilterKeys } from "@plane/types"; // components +import { LayersIcon } from "@plane/ui"; +import { WorkspaceService } from "@/plane-web/services"; +import { EmptyWorkspace } from "../empty-states"; +import { EWidgetKeys, WidgetLoader } from "../loaders"; import { FiltersDropdown } from "./filters"; import { RecentIssue } from "./issue"; -import { WorkspaceService } from "@/plane-web/services"; -import useSWR from "swr"; -import { RecentProject } from "./project"; import { RecentPage } from "./page"; -import { EWidgetKeys, WidgetLoader } from "../loaders"; -import { Briefcase, FileText } from "lucide-react"; -import { LayersIcon } from "@plane/ui"; -import { EmptyWorkspace } from "../empty-states"; +import { RecentProject } from "./project"; const WIDGET_KEY = EWidgetKeys.RECENT_ACTIVITY; const workspaceService = new WorkspaceService(); diff --git a/web/core/components/home/widgets/recents/page.tsx b/web/core/components/home/widgets/recents/page.tsx index f7361f0310f..91a350c9e89 100644 --- a/web/core/components/home/widgets/recents/page.tsx +++ b/web/core/components/home/widgets/recents/page.tsx @@ -1,11 +1,11 @@ +import { useRouter } from "next/navigation"; +import { FileText } from "lucide-react"; import { TActivityEntityData, TPageEntityData } from "@plane/types"; import { Avatar, Logo } from "@plane/ui"; +import { getFileURL } from "@plane/utils"; import { ListItem } from "@/components/core/list"; import { calculateTimeAgo } from "@/helpers/date-time.helper"; -import { FileText } from "lucide-react"; -import { getFileURL } from "@plane/utils"; import { useMember } from "@/hooks/store"; -import { useRouter } from "next/navigation"; type BlockProps = { activity: TActivityEntityData; diff --git a/web/core/components/home/widgets/recents/project.tsx b/web/core/components/home/widgets/recents/project.tsx index 18037d2701d..bfe2bd5cd1d 100644 --- a/web/core/components/home/widgets/recents/project.tsx +++ b/web/core/components/home/widgets/recents/project.tsx @@ -1,9 +1,9 @@ +import { useRouter } from "next/navigation"; import { TActivityEntityData, TProjectEntityData } from "@plane/types"; import { Logo } from "@plane/ui"; import { ListItem } from "@/components/core/list"; -import { calculateTimeAgo } from "@/helpers/date-time.helper"; import { MemberDropdown } from "@/components/dropdowns"; -import { useRouter } from "next/navigation"; +import { calculateTimeAgo } from "@/helpers/date-time.helper"; type BlockProps = { activity: TActivityEntityData; diff --git a/web/core/store/workspace/home.ts b/web/core/store/workspace/home.ts index 48166bf6545..4867e5f5bc2 100644 --- a/web/core/store/workspace/home.ts +++ b/web/core/store/workspace/home.ts @@ -1,7 +1,7 @@ import orderBy from "lodash/orderBy"; import set from "lodash/set"; -import { action, makeObservable, observable, runInAction } from "mobx"; -import { TWidgetEntityData } from "@plane/types"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import { THomeWidgetKeys, TWidgetEntityData } from "@plane/types"; import { WorkspaceService } from "@/plane-web/services"; import { IWorkspaceLinkStore, WorkspaceLinkStore } from "./link.store"; @@ -9,6 +9,9 @@ export interface IHomeStore { // observables showWidgetSettings: boolean; widgetsMap: Record; + widgets: THomeWidgetKeys[]; + // computed + orderedWidgets: THomeWidgetKeys[]; //stores quickLinks: IWorkspaceLinkStore; // actions @@ -22,7 +25,7 @@ export class HomeStore implements IHomeStore { // observables showWidgetSettings = false; widgetsMap: Record = {}; - widgets: string[] = []; + widgets: THomeWidgetKeys[] = []; // stores quickLinks: IWorkspaceLinkStore; // services @@ -34,6 +37,8 @@ export class HomeStore implements IHomeStore { showWidgetSettings: observable, widgetsMap: observable, widgets: observable, + // computed + orderedWidgets: computed, // actions toggleWidgetSettings: action, fetchWidgets: action, @@ -47,6 +52,10 @@ export class HomeStore implements IHomeStore { this.quickLinks = new WorkspaceLinkStore(); } + get orderedWidgets() { + return orderBy(Object.values(this.widgetsMap), "sort_order", "desc").map((widget) => widget.key); + } + toggleWidgetSettings = (value?: boolean) => { this.showWidgetSettings = value !== undefined ? value : !this.showWidgetSettings; }; @@ -55,7 +64,7 @@ export class HomeStore implements IHomeStore { try { const widgets = await this.workspaceService.fetchWorkspaceWidgets(workspaceSlug); runInAction(() => { - this.widgets = widgets.map((widget) => widget.key); + this.widgets = orderBy(Object.values(widgets), "sort_order", "desc").map((widget) => widget.key); widgets.forEach((widget) => { this.widgetsMap[widget.key] = widget; }); diff --git a/web/ee/components/home/header.tsx b/web/ee/components/home/header.tsx new file mode 100644 index 00000000000..c95736c9ebd --- /dev/null +++ b/web/ee/components/home/header.tsx @@ -0,0 +1 @@ +export const HomePageHeader = () => <>;