From 15ff3eea9bf771d5ab048a88299f864c6467c9ac Mon Sep 17 00:00:00 2001 From: gakshita Date: Thu, 19 Dec 2024 17:36:14 +0530 Subject: [PATCH 01/18] wip --- packages/types/src/dashboard.d.ts | 2 + packages/types/src/index.d.ts | 2 + packages/types/src/workspace/dashboard.d.ts | 4 + packages/types/src/workspace/index.ts | 2 + packages/types/src/workspace/link.d.ts | 22 ++ web/ce/components/stickies/index.ts | 1 + web/ce/components/stickies/widget.tsx | 1 + .../dashboard/home-dashboard-widgets.tsx | 32 +- .../components/dashboard/links/action.tsx | 13 + .../links/create-update-link-modal.tsx | 131 +++++++ web/core/components/dashboard/links/index.ts | 3 + .../dashboard/links/link-detail.tsx | 148 ++++++++ web/core/components/dashboard/links/links.tsx | 37 ++ web/core/components/dashboard/links/root.tsx | 22 ++ .../components/dashboard/links/use-links.tsx | 88 +++++ .../dashboard/widgets/assigned-issues.tsx | 165 -------- .../dashboard/widgets/created-issues.tsx | 162 -------- .../widgets/empty-states/assigned-issues.tsx | 30 -- .../widgets/empty-states/created-issues.tsx | 29 -- .../dashboard/widgets/empty-states/index.ts | 8 +- .../empty-states/issues-by-priority.tsx | 25 -- .../empty-states/issues-by-state-group.tsx | 25 -- .../widgets/empty-states/project.tsx | 108 ++++++ .../widgets/empty-states/recent-activity.tsx | 25 -- .../empty-states/recent-collaborators.tsx | 39 -- .../dashboard/widgets/empty-states/root.tsx | 108 ++++++ .../components/dashboard/widgets/index.ts | 11 +- .../dashboard/widgets/issue-panels/index.ts | 3 - .../widgets/issue-panels/issue-list-item.tsx | 352 ------------------ .../widgets/issue-panels/issues-list.tsx | 134 ------- .../widgets/issue-panels/tabs-list.tsx | 61 --- .../dashboard/widgets/issues-by-priority.tsx | 112 ------ .../widgets/issues-by-state-group.tsx | 221 ----------- .../dashboard/widgets/overview-stats.tsx | 100 ----- .../dashboard/widgets/recent-activity.tsx | 121 +++--- .../collaborators-list.tsx | 154 -------- .../widgets/recent-collaborators/index.ts | 1 - .../widgets/recent-collaborators/root.tsx | 36 -- .../dashboard/widgets/recent-pages.tsx | 145 ++++++++ .../dashboard/widgets/recent-projects.tsx | 52 ++- .../dashboard/widgets/stickies/index.tsx | 83 +++++ .../widgets/stickies/stickies-layout.tsx | 146 ++++++++ .../page-views/workspace-dashboard.tsx | 4 +- web/core/components/user/user-greetings.tsx | 4 +- web/core/services/workspace.service.ts | 39 ++ web/core/store/workspace/index.ts | 4 + web/core/store/workspace/link.store.ts | 128 +++++++ web/ee/components/stickies/index.ts | 1 + 48 files changed, 1353 insertions(+), 1791 deletions(-) create mode 100644 packages/types/src/workspace/dashboard.d.ts create mode 100644 packages/types/src/workspace/index.ts create mode 100644 packages/types/src/workspace/link.d.ts create mode 100644 web/ce/components/stickies/index.ts create mode 100644 web/ce/components/stickies/widget.tsx create mode 100644 web/core/components/dashboard/links/action.tsx create mode 100644 web/core/components/dashboard/links/create-update-link-modal.tsx create mode 100644 web/core/components/dashboard/links/index.ts create mode 100644 web/core/components/dashboard/links/link-detail.tsx create mode 100644 web/core/components/dashboard/links/links.tsx create mode 100644 web/core/components/dashboard/links/root.tsx create mode 100644 web/core/components/dashboard/links/use-links.tsx delete mode 100644 web/core/components/dashboard/widgets/assigned-issues.tsx delete mode 100644 web/core/components/dashboard/widgets/created-issues.tsx delete mode 100644 web/core/components/dashboard/widgets/empty-states/assigned-issues.tsx delete mode 100644 web/core/components/dashboard/widgets/empty-states/created-issues.tsx delete mode 100644 web/core/components/dashboard/widgets/empty-states/issues-by-priority.tsx delete mode 100644 web/core/components/dashboard/widgets/empty-states/issues-by-state-group.tsx create mode 100644 web/core/components/dashboard/widgets/empty-states/project.tsx delete mode 100644 web/core/components/dashboard/widgets/empty-states/recent-activity.tsx delete mode 100644 web/core/components/dashboard/widgets/empty-states/recent-collaborators.tsx create mode 100644 web/core/components/dashboard/widgets/empty-states/root.tsx delete mode 100644 web/core/components/dashboard/widgets/issue-panels/index.ts delete mode 100644 web/core/components/dashboard/widgets/issue-panels/issue-list-item.tsx delete mode 100644 web/core/components/dashboard/widgets/issue-panels/issues-list.tsx delete mode 100644 web/core/components/dashboard/widgets/issue-panels/tabs-list.tsx delete mode 100644 web/core/components/dashboard/widgets/issues-by-priority.tsx delete mode 100644 web/core/components/dashboard/widgets/issues-by-state-group.tsx delete mode 100644 web/core/components/dashboard/widgets/overview-stats.tsx delete mode 100644 web/core/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx delete mode 100644 web/core/components/dashboard/widgets/recent-collaborators/index.ts delete mode 100644 web/core/components/dashboard/widgets/recent-collaborators/root.tsx create mode 100644 web/core/components/dashboard/widgets/recent-pages.tsx create mode 100644 web/core/components/dashboard/widgets/stickies/index.tsx create mode 100644 web/core/components/dashboard/widgets/stickies/stickies-layout.tsx create mode 100644 web/core/store/workspace/link.store.ts create mode 100644 web/ee/components/stickies/index.ts diff --git a/packages/types/src/dashboard.d.ts b/packages/types/src/dashboard.d.ts index 96efea00706..b2bf6ca88e0 100644 --- a/packages/types/src/dashboard.d.ts +++ b/packages/types/src/dashboard.d.ts @@ -5,12 +5,14 @@ import { TStateGroups } from "./state"; import { TIssueRelationTypes } from "@/plane-web/types"; export type TWidgetKeys = + | "quick_links" | "overview_stats" | "assigned_issues" | "created_issues" | "issues_by_state_groups" | "issues_by_priority" | "recent_activity" + | "recent_pages" | "recent_projects" | "recent_collaborators"; diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index 9c66c629a78..c8369829636 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -33,3 +33,5 @@ export * from "./favorite"; export * from "./file"; export * from "./workspace-draft-issues/base"; export * from "./command-palette"; +export * from "./workspace/link"; +export * from "./workspace/dashboard"; diff --git a/packages/types/src/workspace/dashboard.d.ts b/packages/types/src/workspace/dashboard.d.ts new file mode 100644 index 00000000000..114891a8ff9 --- /dev/null +++ b/packages/types/src/workspace/dashboard.d.ts @@ -0,0 +1,4 @@ +export type WidgetProps = { + dashboardId: string; + workspaceSlug: string; +}; diff --git a/packages/types/src/workspace/index.ts b/packages/types/src/workspace/index.ts new file mode 100644 index 00000000000..318c703f90e --- /dev/null +++ b/packages/types/src/workspace/index.ts @@ -0,0 +1,2 @@ +export * from "./link"; +export * from "./dashboard"; diff --git a/packages/types/src/workspace/link.d.ts b/packages/types/src/workspace/link.d.ts new file mode 100644 index 00000000000..00bad1a58c8 --- /dev/null +++ b/packages/types/src/workspace/link.d.ts @@ -0,0 +1,22 @@ +export type TLinkEditableFields = { + title: string; + url: string; +}; + +export type TLink = TLinkEditableFields & { + created_by_id: string; + id: string; + metadata: any; + workspace_slug: string; + + //need + created_at: Date; +}; + +export type TLinkMap = { + [workspace_slug: string]: TLink; +}; + +export type TLinkIdMap = { + [workspace_slug: string]: string[]; +}; diff --git a/web/ce/components/stickies/index.ts b/web/ce/components/stickies/index.ts new file mode 100644 index 00000000000..97866ce19be --- /dev/null +++ b/web/ce/components/stickies/index.ts @@ -0,0 +1 @@ +export * from "./widget"; diff --git a/web/ce/components/stickies/widget.tsx b/web/ce/components/stickies/widget.tsx new file mode 100644 index 00000000000..56df281e1e4 --- /dev/null +++ b/web/ce/components/stickies/widget.tsx @@ -0,0 +1 @@ +export const StickiesWidget = () => <>; diff --git a/web/core/components/dashboard/home-dashboard-widgets.tsx b/web/core/components/dashboard/home-dashboard-widgets.tsx index fe64278dc99..6715a992585 100644 --- a/web/core/components/dashboard/home-dashboard-widgets.tsx +++ b/web/core/components/dashboard/home-dashboard-widgets.tsx @@ -1,33 +1,22 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // types -import { TWidgetKeys } from "@plane/types"; +import { TWidgetKeys, WidgetProps } from "@plane/types"; // components -import { - AssignedIssuesWidget, - CreatedIssuesWidget, - IssuesByPriorityWidget, - IssuesByStateGroupWidget, - OverviewStatsWidget, - RecentActivityWidget, - RecentCollaboratorsWidget, - RecentProjectsWidget, - WidgetProps, -} from "@/components/dashboard"; +import { RecentActivityWidget, RecentProjectsWidget, RecentPagesWidget, EmptyWorkspace } from "@/components/dashboard"; // hooks import { useDashboard } from "@/hooks/store"; +import { StickiesWidget } from "@/plane-web/components/stickies"; +import { DashboardQuickLinks } from "./links"; const WIDGETS_LIST: { [key in TWidgetKeys]: { component: React.FC; fullWidth: boolean }; } = { - overview_stats: { component: OverviewStatsWidget, fullWidth: true }, - assigned_issues: { component: AssignedIssuesWidget, fullWidth: false }, - created_issues: { component: CreatedIssuesWidget, fullWidth: false }, - issues_by_state_groups: { component: IssuesByStateGroupWidget, fullWidth: false }, - issues_by_priority: { component: IssuesByPriorityWidget, fullWidth: false }, recent_activity: { component: RecentActivityWidget, fullWidth: false }, + recent_pages: { component: RecentPagesWidget, fullWidth: false }, recent_projects: { component: RecentProjectsWidget, fullWidth: false }, - recent_collaborators: { component: RecentCollaboratorsWidget, fullWidth: true }, + // recent_collaborators: { component: RecentCollaboratorsWidget, fullWidth: true }, + my_stickies: { component: StickiesWidget, fullWidth: false }, }; export const DashboardWidgets = observer(() => { @@ -42,12 +31,15 @@ export const DashboardWidgets = observer(() => { if (!workspaceSlug || !homeDashboardId) return null; return ( -
+
+ + {Object.entries(WIDGETS_LIST).map(([key, widget]) => { const WidgetComponent = widget.component; // if the widget doesn't exist, return null - if (!doesWidgetExist(key as TWidgetKeys)) return null; + // if (!doesWidgetExist(key as TWidgetKeys)) return null; // if the widget is full width, return it in a 2 column grid + console.log({ widget, key }); if (widget.fullWidth) return (
diff --git a/web/core/components/dashboard/links/action.tsx b/web/core/components/dashboard/links/action.tsx new file mode 100644 index 00000000000..b395d757e55 --- /dev/null +++ b/web/core/components/dashboard/links/action.tsx @@ -0,0 +1,13 @@ +import { PlusIcon } from "lucide-react"; + +export const AddLink = () => { + console.log("AddLink"); + return ( + + ); +}; diff --git a/web/core/components/dashboard/links/create-update-link-modal.tsx b/web/core/components/dashboard/links/create-update-link-modal.tsx new file mode 100644 index 00000000000..3b78c23b4d7 --- /dev/null +++ b/web/core/components/dashboard/links/create-update-link-modal.tsx @@ -0,0 +1,131 @@ +"use client"; + +import { FC, useEffect } from "react"; +import { observer } from "mobx-react"; +import { Controller, useForm } from "react-hook-form"; +// plane types +// plane ui +import { TLink, TLinkEditableFields } from "@plane/types"; +import { Button, Input, ModalCore } from "@plane/ui"; +import { TLinkOperations } from "./use-links"; + +export type TLinkOperationsModal = Exclude; + +export type TLinkCreateFormFieldOptions = TLinkEditableFields & { + id?: string; +}; + +export type TLinkCreateEditModal = { + isModalOpen: boolean; + handleOnClose?: () => void; + linkOperations: TLinkOperationsModal; + preloadedData?: TLinkCreateFormFieldOptions; + setLinkData: (link: TLink | undefined) => void; +}; + +const defaultValues: TLinkCreateFormFieldOptions = { + title: "", + url: "", +}; + +export const LinkCreateUpdateModal: FC = observer((props) => { + // props + const { setLinkData, isModalOpen, handleOnClose, linkOperations, preloadedData } = props; + // react hook form + const { + formState: { errors, isSubmitting }, + handleSubmit, + control, + reset, + } = useForm({ + defaultValues, + }); + + const onClose = () => { + if (handleOnClose) handleOnClose(); + setLinkData(undefined); + }; + + const handleFormSubmit = async (formData: TLinkCreateFormFieldOptions) => { + const parsedUrl = formData.url.startsWith("http") ? formData.url : `http://${formData.url}`; + try { + if (!formData || !formData.id) await linkOperations.create({ title: formData.title, url: parsedUrl }); + else await linkOperations.update(formData.id, { title: formData.title, url: parsedUrl }); + onClose(); + } catch (error) { + console.error("error", error); + } + }; + + useEffect(() => { + if (isModalOpen) reset({ ...defaultValues, ...preloadedData }); + return () => reset(defaultValues); + }, [preloadedData, reset, isModalOpen]); + + return ( + +
+
+

{preloadedData?.id ? "Update" : "Add"} link

+
+
+ + ( + + )} + /> + {errors.url && URL is invalid} +
+
+ + ( + + )} + /> +
+
+
+
+ + +
+
+
+ ); +}); diff --git a/web/core/components/dashboard/links/index.ts b/web/core/components/dashboard/links/index.ts new file mode 100644 index 00000000000..380f7763c16 --- /dev/null +++ b/web/core/components/dashboard/links/index.ts @@ -0,0 +1,3 @@ +export * from "./root"; +export * from "./links"; +export * from "./link-detail"; diff --git a/web/core/components/dashboard/links/link-detail.tsx b/web/core/components/dashboard/links/link-detail.tsx new file mode 100644 index 00000000000..ff5df06de35 --- /dev/null +++ b/web/core/components/dashboard/links/link-detail.tsx @@ -0,0 +1,148 @@ +"use client"; + +import { FC } from "react"; +// hooks +// ui +import { observer } from "mobx-react"; +import { Pencil, Trash2, LinkIcon, ExternalLink, Paperclip, EllipsisVertical, Link } from "lucide-react"; +import { Tooltip, TOAST_TYPE, setToast, CustomMenu, TContextMenuItem } from "@plane/ui"; +// icons +// types +// helpers +import { cn } from "@plane/utils"; +import { calculateTimeAgo } from "@/helpers/date-time.helper"; +import { copyTextToClipboard, copyUrlToClipboard } from "@/helpers/string.helper"; +import { useMember, useWorkspace } from "@/hooks/store"; +import { usePlatformOS } from "@/hooks/use-platform-os"; + +import { TLinkOperationsModal } from "./create-update-link-modal"; +import { TLinkOperations } from "./use-links"; + +export type TProjectLinkDetail = { + linkId: string; + linkOperations: TLinkOperations; + isNotAllowed: boolean; +}; + +export const ProjectLinkDetail: FC = observer((props) => { + // props + const { linkId, linkOperations, isNotAllowed } = props; + // hooks + const { + links: { getLinkById, toggleLinkModal, setLinkData }, + } = useWorkspace(); + const { getUserDetails } = useMember(); + const { isMobile } = usePlatformOS(); + + const linkDetail = getLinkById(linkId); + if (!linkDetail) return <>; + + const viewLink = linkDetail.url; + + const toggleProjectLinkModal = (modalToggle: boolean) => { + toggleLinkModal(modalToggle); + setLinkData(linkDetail); + }; + + const handleCopyText = () => + copyUrlToClipboard(viewLink).then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Link Copied!", + message: "View link copied to clipboard.", + }); + }); + const handleOpenInNewTab = () => window.open(`/${viewLink}`, "_blank"); + + const MENU_ITEMS: TContextMenuItem[] = [ + { + key: "edit", + action: () => linkOperations.update(), + title: "Edit", + icon: Pencil, + shouldRender: isNotAllowed, + }, + { + key: "open-new-tab", + action: handleOpenInNewTab, + title: "Open in new tab", + icon: ExternalLink, + }, + { + key: "copy-link", + action: handleCopyText, + title: "Copy link", + icon: Link, + }, + { + key: "delete", + action: () => linkOperations.remove(), + title: "Delete", + icon: Trash2, + shouldRender: isNotAllowed, + }, + ]; + return ( +
+
+ +
+
+
Attachment
+
5 mins ago
+
+ + } + placement="bottom-end" + menuItemsClassName="z-20" + closeOnSelect + className=" my-auto" + > + { + e.preventDefault(); + e.stopPropagation(); + }} + className={cn("flex items-center gap-2")} + > + {MENU_ITEMS.map((item) => { + if (item.shouldRender === false) return null; + return ( + { + e.preventDefault(); + e.stopPropagation(); + item.action(); + }} + className={cn( + "flex items-center gap-2", + { + "text-custom-text-400": item.disabled, + }, + item.className + )} + disabled={item.disabled} + > + {item.icon && } +
+
{item.title}
+ {item.description && ( +

+ {item.description} +

+ )} +
+
+ ); + })} +
+
+
+ ); +}); diff --git a/web/core/components/dashboard/links/links.tsx b/web/core/components/dashboard/links/links.tsx new file mode 100644 index 00000000000..aa5f417dc89 --- /dev/null +++ b/web/core/components/dashboard/links/links.tsx @@ -0,0 +1,37 @@ +import { FC } from "react"; +import { observer } from "mobx-react"; +// computed +import { useWorkspace } from "@/hooks/store"; +import { ProjectLinkDetail } from "./link-detail"; +import { TLinkOperations } from "./use-links"; + +export type TLinkOperationsModal = Exclude; + +export type TProjectLinkList = { + linkOperations: TLinkOperationsModal; + disabled?: boolean; + workspaceSlug: string; +}; + +export const ProjectLinkList: FC = observer((props) => { + // props + const { linkOperations, workspaceSlug, disabled = false } = props; + // hooks + const { + links: { getLinksByWorkspaceId }, + } = useWorkspace(); + + const projectLinks = getLinksByWorkspaceId(workspaceSlug); + + if (!projectLinks) return <>; + + return ( +
+ {projectLinks && + projectLinks.length > 0 && + projectLinks.map((linkId) => ( + + ))} +
+ ); +}); diff --git a/web/core/components/dashboard/links/root.tsx b/web/core/components/dashboard/links/root.tsx new file mode 100644 index 00000000000..e4902da1bb2 --- /dev/null +++ b/web/core/components/dashboard/links/root.tsx @@ -0,0 +1,22 @@ +import { AddLink } from "./action"; +import { ProjectLinkList } from "./links"; +import { useLinks } from "./use-links"; + +type TProps = { + workspaceSlug: string; +}; +export const DashboardQuickLinks = (props: TProps) => { + const { workspaceSlug } = props; + const { linkOperations } = useLinks(workspaceSlug); + return ( + <> +
+ {/* rendering links */} + + + {/* Add new link */} + +
+ + ); +}; diff --git a/web/core/components/dashboard/links/use-links.tsx b/web/core/components/dashboard/links/use-links.tsx new file mode 100644 index 00000000000..d776f6bef02 --- /dev/null +++ b/web/core/components/dashboard/links/use-links.tsx @@ -0,0 +1,88 @@ +import { useMemo } from "react"; +import { TProjectLink } from "@plane/types"; +import { setToast, TOAST_TYPE } from "@plane/ui"; +import { useWorkspace } from "@/hooks/store"; + +export type TLinkOperations = { + create: (data: Partial) => Promise; + update: (linkId: string, data: Partial) => Promise; + remove: (linkId: string) => Promise; +}; +export type TProjectLinkRoot = { + workspaceSlug: string; +}; + +export const useLinks = (workspaceSlug: string) => { + // hooks + const { + links: { createLink, updateLink, removeLink, isLinkModalOpen, toggleLinkModal, linkData, setLinkData, fetchLinks }, + } = useWorkspace(); + + const linkOperations: TLinkOperations = useMemo( + () => ({ + create: async (data: Partial) => { + try { + if (!workspaceSlug) throw new Error("Missing required fields"); + await createLink(workspaceSlug, data); + setToast({ + message: "The link has been successfully created", + type: TOAST_TYPE.SUCCESS, + title: "Link created", + }); + toggleLinkModal(false); + } catch (error: any) { + setToast({ + message: error?.data?.error ?? "The link could not be created", + type: TOAST_TYPE.ERROR, + title: "Link not created", + }); + throw error; + } + }, + update: async (linkId: string, data: Partial) => { + try { + if (!workspaceSlug) throw new Error("Missing required fields"); + await updateLink(workspaceSlug, linkId, data); + setToast({ + message: "The link has been successfully updated", + type: TOAST_TYPE.SUCCESS, + title: "Link updated", + }); + toggleLinkModal(false); + } catch (error) { + setToast({ + message: "The link could not be updated", + type: TOAST_TYPE.ERROR, + title: "Link not updated", + }); + throw error; + } + }, + remove: async (linkId: string) => { + try { + if (!workspaceSlug) throw new Error("Missing required fields"); + await removeLink(workspaceSlug, linkId); + setToast({ + message: "The link has been successfully removed", + type: TOAST_TYPE.SUCCESS, + title: "Link removed", + }); + toggleLinkModal(false); + } catch (error) { + setToast({ + message: "The link could not be removed", + type: TOAST_TYPE.ERROR, + title: "Link not removed", + }); + } + }, + }), + [workspaceSlug] + ); + + const handleOnClose = () => { + toggleLinkModal(false); + }; + + return { linkOperations, handleOnClose, isLinkModalOpen, toggleLinkModal, linkData, setLinkData, fetchLinks }; +}; diff --git a/web/core/components/dashboard/widgets/assigned-issues.tsx b/web/core/components/dashboard/widgets/assigned-issues.tsx deleted file mode 100644 index 30bfbdad1b4..00000000000 --- a/web/core/components/dashboard/widgets/assigned-issues.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import { useEffect, useState } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import { Tab } from "@headlessui/react"; -import { TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types"; -// hooks -import { Card } from "@plane/ui"; -import { - DurationFilterDropdown, - IssuesErrorState, - TabsList, - WidgetIssuesList, - WidgetLoader, - WidgetProps, -} from "@/components/dashboard/widgets"; -import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "@/constants/dashboard"; -import { getCustomDates, getRedirectionFilters, getTabKey } from "@/helpers/dashboard.helper"; -import { useDashboard } from "@/hooks/store"; -// components -// helpers -// types -// constants - -const WIDGET_KEY = "assigned_issues"; - -export const AssignedIssuesWidget: React.FC = observer((props) => { - const { dashboardId, workspaceSlug } = props; - // states - const [fetching, setFetching] = useState(false); - // store hooks - const { fetchWidgetStats, getWidgetDetails, getWidgetStats, getWidgetStatsError, updateDashboardWidgetFilters } = - useDashboard(); - // derived values - const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); - const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const widgetStatsError = getWidgetStatsError(workspaceSlug, dashboardId, WIDGET_KEY); - const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; - const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab); - const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; - - const handleUpdateFilters = async (filters: Partial) => { - if (!widgetDetails) return; - - setFetching(true); - - await updateDashboardWidgetFilters(workspaceSlug, dashboardId, widgetDetails.id, { - widgetKey: WIDGET_KEY, - filters, - }); - - const filterDates = getCustomDates( - filters.duration ?? selectedDurationFilter, - filters.custom_dates ?? selectedCustomDates - ); - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - issue_type: filters.tab ?? selectedTab, - ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), - expand: "issue_relation", - }).finally(() => setFetching(false)); - }; - - useEffect(() => { - const filterDates = getCustomDates(selectedDurationFilter, selectedCustomDates); - - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - issue_type: selectedTab, - ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), - expand: "issue_relation", - }); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const filterParams = getRedirectionFilters(selectedTab); - const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST; - const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab); - - if ((!widgetDetails || !widgetStats) && !widgetStatsError) return ; - - return ( - - {widgetStatsError ? ( - - handleUpdateFilters({ - duration: EDurationFilters.NONE, - tab: "pending", - }) - } - /> - ) : ( - widgetStats && ( - <> -
- - Assigned to you - - { - if (val === "custom" && customDates) { - handleUpdateFilters({ - duration: val, - custom_dates: customDates, - }); - return; - } - - if (val === selectedDurationFilter) return; - - let newTab = selectedTab; - // switch to pending tab if target date is changed to none - if (val === "none" && selectedTab !== "completed") newTab = "pending"; - // switch to upcoming tab if target date is changed to other than none - if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") - newTab = "upcoming"; - - handleUpdateFilters({ - duration: val, - tab: newTab, - }); - }} - /> -
- { - const newSelectedTab = tabsList[i]; - handleUpdateFilters({ tab: newSelectedTab?.key ?? "completed" }); - }} - className="h-full flex flex-col" - > - - - {tabsList.map((tab) => { - if (tab.key !== selectedTab) return null; - - return ( - - - - ); - })} - - - - ) - )} -
- ); -}); diff --git a/web/core/components/dashboard/widgets/created-issues.tsx b/web/core/components/dashboard/widgets/created-issues.tsx deleted file mode 100644 index 083318d3103..00000000000 --- a/web/core/components/dashboard/widgets/created-issues.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { useEffect, useState } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import { Tab } from "@headlessui/react"; -import { TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types"; -// hooks -import { Card } from "@plane/ui"; -import { - DurationFilterDropdown, - IssuesErrorState, - TabsList, - WidgetIssuesList, - WidgetLoader, - WidgetProps, -} from "@/components/dashboard/widgets"; -import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "@/constants/dashboard"; -import { getCustomDates, getRedirectionFilters, getTabKey } from "@/helpers/dashboard.helper"; -import { useDashboard } from "@/hooks/store"; -// components -// helpers -// types -// constants - -const WIDGET_KEY = "created_issues"; - -export const CreatedIssuesWidget: React.FC = observer((props) => { - const { dashboardId, workspaceSlug } = props; - // states - const [fetching, setFetching] = useState(false); - // store hooks - const { fetchWidgetStats, getWidgetDetails, getWidgetStats, getWidgetStatsError, updateDashboardWidgetFilters } = - useDashboard(); - // derived values - const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); - const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const widgetStatsError = getWidgetStatsError(workspaceSlug, dashboardId, WIDGET_KEY); - const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; - const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab); - const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; - - const handleUpdateFilters = async (filters: Partial) => { - if (!widgetDetails) return; - - setFetching(true); - - await updateDashboardWidgetFilters(workspaceSlug, dashboardId, widgetDetails.id, { - widgetKey: WIDGET_KEY, - filters, - }); - - const filterDates = getCustomDates( - filters.duration ?? selectedDurationFilter, - filters.custom_dates ?? selectedCustomDates - ); - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - issue_type: filters.tab ?? selectedTab, - ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), - }).finally(() => setFetching(false)); - }; - - useEffect(() => { - const filterDates = getCustomDates(selectedDurationFilter, selectedCustomDates); - - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - issue_type: selectedTab, - ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const filterParams = getRedirectionFilters(selectedTab); - const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST; - const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab); - - if ((!widgetDetails || !widgetStats) && !widgetStatsError) return ; - - return ( - - {widgetStatsError ? ( - - handleUpdateFilters({ - duration: EDurationFilters.NONE, - tab: "pending", - }) - } - /> - ) : ( - widgetStats && ( - <> -
- - Created by you - - { - if (val === "custom" && customDates) { - handleUpdateFilters({ - duration: val, - custom_dates: customDates, - }); - return; - } - - if (val === selectedDurationFilter) return; - - let newTab = selectedTab; - // switch to pending tab if target date is changed to none - if (val === "none" && selectedTab !== "completed") newTab = "pending"; - // switch to upcoming tab if target date is changed to other than none - if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") - newTab = "upcoming"; - - handleUpdateFilters({ - duration: val, - tab: newTab, - }); - }} - /> -
- { - const newSelectedTab = tabsList[i]; - handleUpdateFilters({ tab: newSelectedTab.key ?? "completed" }); - }} - className="h-full flex flex-col" - > - - - {tabsList.map((tab) => { - if (tab.key !== selectedTab) return null; - - return ( - - - - ); - })} - - - - ) - )} -
- ); -}); diff --git a/web/core/components/dashboard/widgets/empty-states/assigned-issues.tsx b/web/core/components/dashboard/widgets/empty-states/assigned-issues.tsx deleted file mode 100644 index c9a25cfd390..00000000000 --- a/web/core/components/dashboard/widgets/empty-states/assigned-issues.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import Image from "next/image"; -import { useTheme } from "next-themes"; -import { TIssuesListTypes } from "@plane/types"; -// types -import { ASSIGNED_ISSUES_EMPTY_STATES } from "@/constants/dashboard"; -// constants - -type Props = { - type: TIssuesListTypes; -}; - -export const AssignedIssuesEmptyState: React.FC = (props) => { - const { type } = props; - // next-themes - const { resolvedTheme } = useTheme(); - - const typeDetails = ASSIGNED_ISSUES_EMPTY_STATES[type]; - - const image = resolvedTheme === "dark" ? typeDetails.darkImage : typeDetails.lightImage; - - // TODO: update empty state logic to use a general component - return ( -
-
- Assigned issues -
-

{typeDetails.title}

-
- ); -}; diff --git a/web/core/components/dashboard/widgets/empty-states/created-issues.tsx b/web/core/components/dashboard/widgets/empty-states/created-issues.tsx deleted file mode 100644 index bc812de3549..00000000000 --- a/web/core/components/dashboard/widgets/empty-states/created-issues.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import Image from "next/image"; -import { useTheme } from "next-themes"; -import { TIssuesListTypes } from "@plane/types"; -// types -import { CREATED_ISSUES_EMPTY_STATES } from "@/constants/dashboard"; -// constants - -type Props = { - type: TIssuesListTypes; -}; - -export const CreatedIssuesEmptyState: React.FC = (props) => { - const { type } = props; - // next-themes - const { resolvedTheme } = useTheme(); - - const typeDetails = CREATED_ISSUES_EMPTY_STATES[type]; - - const image = resolvedTheme === "dark" ? typeDetails.darkImage : typeDetails.lightImage; - - return ( -
-
- Assigned issues -
-

{typeDetails.title}

-
- ); -}; diff --git a/web/core/components/dashboard/widgets/empty-states/index.ts b/web/core/components/dashboard/widgets/empty-states/index.ts index 72ca1dbb2dc..b861b3ee9bc 100644 --- a/web/core/components/dashboard/widgets/empty-states/index.ts +++ b/web/core/components/dashboard/widgets/empty-states/index.ts @@ -1,6 +1,2 @@ -export * from "./assigned-issues"; -export * from "./created-issues"; -export * from "./issues-by-priority"; -export * from "./issues-by-state-group"; -export * from "./recent-activity"; -export * from "./recent-collaborators"; +export * from "./project"; +export * from "./root"; diff --git a/web/core/components/dashboard/widgets/empty-states/issues-by-priority.tsx b/web/core/components/dashboard/widgets/empty-states/issues-by-priority.tsx deleted file mode 100644 index 262c68d699a..00000000000 --- a/web/core/components/dashboard/widgets/empty-states/issues-by-priority.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import Image from "next/image"; -import { useTheme } from "next-themes"; -// assets -import DarkImage from "@/public/empty-state/dashboard/dark/issues-by-priority.svg"; -import LightImage from "@/public/empty-state/dashboard/light/issues-by-priority.svg"; - -export const IssuesByPriorityEmptyState = () => { - // next-themes - const { resolvedTheme } = useTheme(); - - const image = resolvedTheme === "dark" ? DarkImage : LightImage; - - return ( -
-
- Issues by state group -
-

- Issues assigned to you, broken down by -
- priority will show up here. -

-
- ); -}; diff --git a/web/core/components/dashboard/widgets/empty-states/issues-by-state-group.tsx b/web/core/components/dashboard/widgets/empty-states/issues-by-state-group.tsx deleted file mode 100644 index ad7ac82b82a..00000000000 --- a/web/core/components/dashboard/widgets/empty-states/issues-by-state-group.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import Image from "next/image"; -import { useTheme } from "next-themes"; -// assets -import DarkImage from "@/public/empty-state/dashboard/dark/issues-by-state-group.svg"; -import LightImage from "@/public/empty-state/dashboard/light/issues-by-state-group.svg"; - -export const IssuesByStateGroupEmptyState = () => { - // next-themes - const { resolvedTheme } = useTheme(); - - const image = resolvedTheme === "dark" ? DarkImage : LightImage; - - return ( -
-
- Issues by state group -
-

- Issue assigned to you, broken down by state, -
- will show up here. -

-
- ); -}; diff --git a/web/core/components/dashboard/widgets/empty-states/project.tsx b/web/core/components/dashboard/widgets/empty-states/project.tsx new file mode 100644 index 00000000000..cd6e557de19 --- /dev/null +++ b/web/core/components/dashboard/widgets/empty-states/project.tsx @@ -0,0 +1,108 @@ +import React from "react"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +import { Briefcase, Hotel, Users } from "lucide-react"; +import { getFileURL } from "@/helpers/file.helper"; +import { useCommandPalette, useEventTracker, useUser, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants"; + +export const NoProjects = () => { + const { workspaceSlug } = useParams(); + const { allowPermissions } = useUserPermissions(); + const { toggleCreateProjectModal } = useCommandPalette(); + const { setTrackElement } = useEventTracker(); + const { data: currentUser } = useUser(); + + const canCreateProject = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + + const EMPTY_STATE_DATA = [ + { + id: "create-project", + title: "Create a project", + description: "Create your first project now to get started", + icon: , + cta: { + text: "Create Project", + link: "#", + + onClick: (e: React.MouseEvent) => { + if (!canCreateProject) return; + e.preventDefault(); + e.stopPropagation(); + setTrackElement("Sidebar"); + toggleCreateProjectModal(true); + }, + }, + }, + { + id: "invite-team", + title: "Invite your team", + description: "The sub text will be of two lines and that will be placed.", + icon: , + cta: { + text: "Invite now", + link: "#", + }, + }, + { + id: "configure-workspace", + title: "Configure your workspace", + description: "The sub text will be of two lines and that will be placed.", + icon: , + cta: { + text: "Configure workspace", + link: "#", + }, + }, + { + id: "personalize-account", + title: "Personalize your account", + description: "The sub text will be of two lines and that will be placed.", + icon: + currentUser?.avatar_url && currentUser?.avatar_url.trim() !== "" ? ( + + + {currentUser?.display_name + + + ) : ( + + + {(currentUser?.email ?? currentUser?.display_name ?? "?")[0]} + + + ), + cta: { + text: "Personalize account", + link: "#", + }, + }, + ]; + return ( +
+ {EMPTY_STATE_DATA.map((item) => ( +
+
+ {item.icon} +
+

{item.title}

+

{item.description}

+ + +
+ ))} +
+ ); +}; diff --git a/web/core/components/dashboard/widgets/empty-states/recent-activity.tsx b/web/core/components/dashboard/widgets/empty-states/recent-activity.tsx deleted file mode 100644 index 6daeae5a719..00000000000 --- a/web/core/components/dashboard/widgets/empty-states/recent-activity.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import Image from "next/image"; -import { useTheme } from "next-themes"; -// assets -import DarkImage from "@/public/empty-state/dashboard/dark/recent-activity.svg"; -import LightImage from "@/public/empty-state/dashboard/light/recent-activity.svg"; - -export const RecentActivityEmptyState = () => { - // next-themes - const { resolvedTheme } = useTheme(); - - const image = resolvedTheme === "dark" ? DarkImage : LightImage; - - return ( -
-
- Issues by state group -
-

- All your issue activities across -
- projects will show up here. -

-
- ); -}; diff --git a/web/core/components/dashboard/widgets/empty-states/recent-collaborators.tsx b/web/core/components/dashboard/widgets/empty-states/recent-collaborators.tsx deleted file mode 100644 index d1a1200aa59..00000000000 --- a/web/core/components/dashboard/widgets/empty-states/recent-collaborators.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import Image from "next/image"; -import { useTheme } from "next-themes"; -// assets -import DarkImage1 from "@/public/empty-state/dashboard/dark/recent-collaborators-1.svg"; -import DarkImage2 from "@/public/empty-state/dashboard/dark/recent-collaborators-2.svg"; -import DarkImage3 from "@/public/empty-state/dashboard/dark/recent-collaborators-3.svg"; -import LightImage1 from "@/public/empty-state/dashboard/light/recent-collaborators-1.svg"; -import LightImage2 from "@/public/empty-state/dashboard/light/recent-collaborators-2.svg"; -import LightImage3 from "@/public/empty-state/dashboard/light/recent-collaborators-3.svg"; - -export const RecentCollaboratorsEmptyState = () => { - // next-themes - const { resolvedTheme } = useTheme(); - - const image1 = resolvedTheme === "dark" ? DarkImage1 : LightImage1; - const image2 = resolvedTheme === "dark" ? DarkImage2 : LightImage2; - const image3 = resolvedTheme === "dark" ? DarkImage3 : LightImage3; - - return ( -
-

- Compare your activities with the top -
- seven in your project. -

-
-
- Recent collaborators -
-
- Recent collaborators -
-
- Recent collaborators -
-
-
- ); -}; diff --git a/web/core/components/dashboard/widgets/empty-states/root.tsx b/web/core/components/dashboard/widgets/empty-states/root.tsx new file mode 100644 index 00000000000..d97c1e6195a --- /dev/null +++ b/web/core/components/dashboard/widgets/empty-states/root.tsx @@ -0,0 +1,108 @@ +import React from "react"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +import { Briefcase, Hotel, Users } from "lucide-react"; +import { getFileURL } from "@/helpers/file.helper"; +import { useCommandPalette, useEventTracker, useUser, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants"; + +export const EmptyWorkspace = () => { + const { workspaceSlug } = useParams(); + const { allowPermissions } = useUserPermissions(); + const { toggleCreateProjectModal } = useCommandPalette(); + const { setTrackElement } = useEventTracker(); + const { data: currentUser } = useUser(); + + const canCreateProject = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + + const EMPTY_STATE_DATA = [ + { + id: "create-project", + title: "Create a project", + description: "Create your first project now to get started", + icon: , + cta: { + text: "Create Project", + link: "#", + + onClick: (e: React.MouseEvent) => { + if (!canCreateProject) return; + e.preventDefault(); + e.stopPropagation(); + setTrackElement("Sidebar"); + toggleCreateProjectModal(true); + }, + }, + }, + { + id: "invite-team", + title: "Invite your team", + description: "The sub text will be of two lines and that will be placed.", + icon: , + cta: { + text: "Invite now", + link: "#", + }, + }, + { + id: "configure-workspace", + title: "Configure your workspace", + description: "The sub text will be of two lines and that will be placed.", + icon: , + cta: { + text: "Configure workspace", + link: "#", + }, + }, + { + id: "personalize-account", + title: "Personalize your account", + description: "The sub text will be of two lines and that will be placed.", + icon: + currentUser?.avatar_url && currentUser?.avatar_url.trim() !== "" ? ( + + + {currentUser?.display_name + + + ) : ( + + + {(currentUser?.email ?? currentUser?.display_name ?? "?")[0]} + + + ), + cta: { + text: "Personalize account", + link: "#", + }, + }, + ]; + return ( +
+ {EMPTY_STATE_DATA.map((item) => ( +
+
+ {item.icon} +
+

{item.title}

+

{item.description}

+ + +
+ ))} +
+ ); +}; diff --git a/web/core/components/dashboard/widgets/index.ts b/web/core/components/dashboard/widgets/index.ts index 31fc645d410..f74e0d02fb8 100644 --- a/web/core/components/dashboard/widgets/index.ts +++ b/web/core/components/dashboard/widgets/index.ts @@ -1,13 +1,10 @@ export * from "./dropdowns"; export * from "./empty-states"; export * from "./error-states"; -export * from "./issue-panels"; export * from "./loaders"; -export * from "./assigned-issues"; -export * from "./created-issues"; -export * from "./issues-by-priority"; -export * from "./issues-by-state-group"; -export * from "./overview-stats"; export * from "./recent-activity"; -export * from "./recent-collaborators"; export * from "./recent-projects"; +export * from "./recent-activity"; +export * from "./recent-pages"; +export * from "./stickies"; +export * from "./empty-states"; diff --git a/web/core/components/dashboard/widgets/issue-panels/index.ts b/web/core/components/dashboard/widgets/issue-panels/index.ts deleted file mode 100644 index f5b7d53d49e..00000000000 --- a/web/core/components/dashboard/widgets/issue-panels/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./issue-list-item"; -export * from "./issues-list"; -export * from "./tabs-list"; diff --git a/web/core/components/dashboard/widgets/issue-panels/issue-list-item.tsx b/web/core/components/dashboard/widgets/issue-panels/issue-list-item.tsx deleted file mode 100644 index a54fd312d99..00000000000 --- a/web/core/components/dashboard/widgets/issue-panels/issue-list-item.tsx +++ /dev/null @@ -1,352 +0,0 @@ -"use client"; - -import isToday from "date-fns/isToday"; -import { observer } from "mobx-react"; -// types -import { TIssue, TWidgetIssue } from "@plane/types"; -// ui -import { Avatar, AvatarGroup, ControlLink, PriorityIcon } from "@plane/ui"; -// helpers -import { findTotalDaysInRange, getDate, renderFormattedDate } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; -// hooks -import { useIssueDetail, useMember, useProject } from "@/hooks/store"; -// plane web components -import { IssueIdentifier } from "@/plane-web/components/issues"; - -export type IssueListItemProps = { - issueId: string; - onClick: (issue: TIssue) => void; - workspaceSlug: string; -}; - -export const AssignedUpcomingIssueListItem: React.FC = observer((props) => { - const { issueId, onClick, workspaceSlug } = props; - // store hooks - const { getProjectById } = useProject(); - const { - issue: { getIssueById }, - } = useIssueDetail(); - // derived values - const issueDetails = getIssueById(issueId) as TWidgetIssue | undefined; - - if (!issueDetails || !issueDetails.project_id) return null; - - const projectDetails = getProjectById(issueDetails.project_id); - - const blockedByIssues = issueDetails.issue_relation?.filter((issue) => issue.relation_type === "blocked_by") ?? []; - - const blockedByIssueProjectDetails = - blockedByIssues.length === 1 ? getProjectById(blockedByIssues[0]?.project_id ?? "") : null; - - const targetDate = getDate(issueDetails.target_date); - - return ( - onClick(issueDetails)} - className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" - > -
- {projectDetails && ( - - )} -
{issueDetails.name}
-
-
- -
-
- {targetDate ? (isToday(targetDate) ? "Today" : renderFormattedDate(targetDate)) : "-"} -
-
- {blockedByIssues.length > 0 - ? blockedByIssues.length > 1 - ? `${blockedByIssues.length} blockers` - : blockedByIssueProjectDetails && ( - - ) - : "-"} -
-
- ); -}); - -export const AssignedOverdueIssueListItem: React.FC = observer((props) => { - const { issueId, onClick, workspaceSlug } = props; - // store hooks - const { getProjectById } = useProject(); - const { - issue: { getIssueById }, - } = useIssueDetail(); - // derived values - const issueDetails = getIssueById(issueId) as TWidgetIssue | undefined; - - if (!issueDetails || !issueDetails.project_id) return null; - - const projectDetails = getProjectById(issueDetails.project_id); - const blockedByIssues = issueDetails.issue_relation?.filter((issue) => issue.relation_type === "blocked_by") ?? []; - - const blockedByIssueProjectDetails = - blockedByIssues.length === 1 ? getProjectById(blockedByIssues[0]?.project_id ?? "") : null; - - const dueBy = findTotalDaysInRange(getDate(issueDetails.target_date), new Date(), false) ?? 0; - - return ( - onClick(issueDetails)} - className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" - > -
- {projectDetails && ( - - )} -
{issueDetails.name}
-
-
- -
-
- {dueBy} {`day${dueBy > 1 ? "s" : ""}`} -
-
- {blockedByIssues.length > 0 - ? blockedByIssues.length > 1 - ? `${blockedByIssues.length} blockers` - : blockedByIssueProjectDetails && ( - - ) - : "-"} -
-
- ); -}); - -export const AssignedCompletedIssueListItem: React.FC = observer((props) => { - const { issueId, onClick, workspaceSlug } = props; - // store hooks - const { - issue: { getIssueById }, - } = useIssueDetail(); - const { getProjectById } = useProject(); - // derived values - const issueDetails = getIssueById(issueId); - - if (!issueDetails || !issueDetails.project_id) return null; - - const projectDetails = getProjectById(issueDetails.project_id); - - return ( - onClick(issueDetails)} - className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" - > -
- {projectDetails && ( - - )} -
{issueDetails.name}
-
-
- -
-
- ); -}); - -export const CreatedUpcomingIssueListItem: React.FC = observer((props) => { - const { issueId, onClick, workspaceSlug } = props; - // store hooks - const { getUserDetails } = useMember(); - const { - issue: { getIssueById }, - } = useIssueDetail(); - const { getProjectById } = useProject(); - // derived values - const issue = getIssueById(issueId); - - if (!issue || !issue.project_id) return null; - - const projectDetails = getProjectById(issue.project_id); - const targetDate = getDate(issue.target_date); - - return ( - onClick(issue)} - className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" - > -
- {projectDetails && ( - - )} -
{issue.name}
-
-
- -
-
- {targetDate ? (isToday(targetDate) ? "Today" : renderFormattedDate(targetDate)) : "-"} -
-
- {issue.assignee_ids && issue.assignee_ids?.length > 0 ? ( - - {issue.assignee_ids?.map((assigneeId) => { - const userDetails = getUserDetails(assigneeId); - - if (!userDetails) return null; - - return ( - - ); - })} - - ) : ( - "-" - )} -
-
- ); -}); - -export const CreatedOverdueIssueListItem: React.FC = observer((props) => { - const { issueId, onClick, workspaceSlug } = props; - // store hooks - const { getUserDetails } = useMember(); - const { - issue: { getIssueById }, - } = useIssueDetail(); - const { getProjectById } = useProject(); - // derived values - const issue = getIssueById(issueId); - - if (!issue || !issue.project_id) return null; - - const projectDetails = getProjectById(issue.project_id); - - const dueBy: number = findTotalDaysInRange(getDate(issue.target_date), new Date(), false) ?? 0; - - return ( - onClick(issue)} - className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" - > -
- {projectDetails && ( - - )} -
{issue.name}
-
-
- -
-
- {dueBy} {`day${dueBy > 1 ? "s" : ""}`} -
-
- {issue.assignee_ids.length > 0 ? ( - - {issue.assignee_ids?.map((assigneeId) => { - const userDetails = getUserDetails(assigneeId); - - if (!userDetails) return null; - - return ( - - ); - })} - - ) : ( - "-" - )} -
-
- ); -}); - -export const CreatedCompletedIssueListItem: React.FC = observer((props) => { - const { issueId, onClick, workspaceSlug } = props; - // store hooks - const { getUserDetails } = useMember(); - const { - issue: { getIssueById }, - } = useIssueDetail(); - const { getProjectById } = useProject(); - // derived values - const issue = getIssueById(issueId); - - if (!issue || !issue.project_id) return null; - - const projectDetails = getProjectById(issue.project_id); - - return ( - onClick(issue)} - className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" - > -
- {projectDetails && ( - - )} -
{issue.name}
-
-
- -
-
- {issue.assignee_ids.length > 0 ? ( - - {issue.assignee_ids?.map((assigneeId) => { - const userDetails = getUserDetails(assigneeId); - - if (!userDetails) return null; - - return ( - - ); - })} - - ) : ( - "-" - )} -
-
- ); -}); diff --git a/web/core/components/dashboard/widgets/issue-panels/issues-list.tsx b/web/core/components/dashboard/widgets/issue-panels/issues-list.tsx deleted file mode 100644 index 2b31d8f27b3..00000000000 --- a/web/core/components/dashboard/widgets/issue-panels/issues-list.tsx +++ /dev/null @@ -1,134 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { TAssignedIssuesWidgetResponse, TCreatedIssuesWidgetResponse, TIssue, TIssuesListTypes } from "@plane/types"; -// hooks -// components -import { Loader, getButtonStyling } from "@plane/ui"; -import { - AssignedCompletedIssueListItem, - AssignedIssuesEmptyState, - AssignedOverdueIssueListItem, - AssignedUpcomingIssueListItem, - CreatedCompletedIssueListItem, - CreatedIssuesEmptyState, - CreatedOverdueIssueListItem, - CreatedUpcomingIssueListItem, - IssueListItemProps, -} from "@/components/dashboard/widgets"; -// ui -// helpers -import { cn } from "@/helpers/common.helper"; -import { getRedirectionFilters } from "@/helpers/dashboard.helper"; -// hooks -import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-redirection"; -import { usePlatformOS } from "@/hooks/use-platform-os"; - -export type WidgetIssuesListProps = { - isLoading: boolean; - tab: TIssuesListTypes; - type: "assigned" | "created"; - widgetStats: TAssignedIssuesWidgetResponse | TCreatedIssuesWidgetResponse; - workspaceSlug: string; -}; - -export const WidgetIssuesList: React.FC = (props) => { - const { isLoading, tab, type, widgetStats, workspaceSlug } = props; - // hooks - const { isMobile } = usePlatformOS(); - const { handleRedirection } = useIssuePeekOverviewRedirection(); - - // handlers - const handleIssuePeekOverview = (issue: TIssue) => handleRedirection(workspaceSlug, issue, isMobile); - - const filterParams = getRedirectionFilters(tab); - - const ISSUE_LIST_ITEM: { - [key: string]: { - [key in TIssuesListTypes]: React.FC; - }; - } = { - assigned: { - pending: AssignedUpcomingIssueListItem, - upcoming: AssignedUpcomingIssueListItem, - overdue: AssignedOverdueIssueListItem, - completed: AssignedCompletedIssueListItem, - }, - created: { - pending: CreatedUpcomingIssueListItem, - upcoming: CreatedUpcomingIssueListItem, - overdue: CreatedOverdueIssueListItem, - completed: CreatedCompletedIssueListItem, - }, - }; - - const issuesList = widgetStats.issues; - - return ( - <> -
- {isLoading ? ( - - - - - - - ) : issuesList.length > 0 ? ( - <> -
-
- Issues - - {widgetStats.count} - -
-
Priority
- {["upcoming", "pending"].includes(tab) &&
Due date
} - {tab === "overdue" &&
Due by
} - {type === "assigned" && tab !== "completed" &&
Blocked by
} - {type === "created" &&
Assigned to
} -
-
- {issuesList.map((issue) => { - const IssueListItem = ISSUE_LIST_ITEM[type][tab]; - - if (!IssueListItem) return null; - - return ( - - ); - })} -
- - ) : ( -
- {type === "assigned" && } - {type === "created" && } -
- )} -
- {!isLoading && issuesList.length > 0 && ( - - View all issues - - )} - - ); -}; diff --git a/web/core/components/dashboard/widgets/issue-panels/tabs-list.tsx b/web/core/components/dashboard/widgets/issue-panels/tabs-list.tsx deleted file mode 100644 index d2b7df8ed0f..00000000000 --- a/web/core/components/dashboard/widgets/issue-panels/tabs-list.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { observer } from "mobx-react"; -import { Tab } from "@headlessui/react"; -import { TIssuesListTypes } from "@plane/types"; -// helpers -import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "@/constants/dashboard"; -import { cn } from "@/helpers/common.helper"; -// types -// constants - -type Props = { - durationFilter: EDurationFilters; - selectedTab: TIssuesListTypes; -}; - -export const TabsList: React.FC = observer((props) => { - const { durationFilter, selectedTab } = props; - - const tabsList = durationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST; - const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab); - - return ( - -
- {tabsList.map((tab) => ( - - {tab.label} - - ))} - - ); -}); diff --git a/web/core/components/dashboard/widgets/issues-by-priority.tsx b/web/core/components/dashboard/widgets/issues-by-priority.tsx deleted file mode 100644 index 9da47bc8481..00000000000 --- a/web/core/components/dashboard/widgets/issues-by-priority.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { useEffect } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -// types -import { TIssuesByPriorityWidgetFilters, TIssuesByPriorityWidgetResponse } from "@plane/types"; -// components -import { Card } from "@plane/ui"; -import { - DurationFilterDropdown, - IssuesByPriorityEmptyState, - WidgetLoader, - WidgetProps, -} from "@/components/dashboard/widgets"; -import { IssuesByPriorityGraph } from "@/components/graphs"; -// constants -import { EDurationFilters } from "@/constants/dashboard"; -// helpers -import { getCustomDates } from "@/helpers/dashboard.helper"; -// hooks -import { useDashboard } from "@/hooks/store"; -import { useAppRouter } from "@/hooks/use-app-router"; - -const WIDGET_KEY = "issues_by_priority"; - -export const IssuesByPriorityWidget: React.FC = observer((props) => { - const { dashboardId, workspaceSlug } = props; - // router - const router = useAppRouter(); - // store hooks - const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard(); - // derived values - const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); - const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const selectedDuration = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; - const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; - - const handleUpdateFilters = async (filters: Partial) => { - if (!widgetDetails) return; - - await updateDashboardWidgetFilters(workspaceSlug, dashboardId, widgetDetails.id, { - widgetKey: WIDGET_KEY, - filters, - }); - - const filterDates = getCustomDates( - filters.duration ?? selectedDuration, - filters.custom_dates ?? selectedCustomDates - ); - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), - }); - }; - - useEffect(() => { - const filterDates = getCustomDates(selectedDuration, selectedCustomDates); - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (!widgetDetails || !widgetStats) return ; - - const totalCount = widgetStats.reduce((acc, item) => acc + item?.count, 0); - const chartData = widgetStats.map((item) => ({ - priority: item?.priority, - priority_count: item?.count, - })); - - return ( - -
- - Assigned by priority - - - handleUpdateFilters({ - duration: val, - ...(val === "custom" ? { custom_dates: customDates } : {}), - }) - } - /> -
- {totalCount > 0 ? ( -
-
- { - router.push( - `/${workspaceSlug}/workspace-views/assigned?priority=${`${datum.data.priority}`.toLowerCase()}` - ); - }} - /> -
-
- ) : ( -
- -
- )} -
- ); -}); diff --git a/web/core/components/dashboard/widgets/issues-by-state-group.tsx b/web/core/components/dashboard/widgets/issues-by-state-group.tsx deleted file mode 100644 index 53437ca1185..00000000000 --- a/web/core/components/dashboard/widgets/issues-by-state-group.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import { useEffect, useState } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -// types -import { TIssuesByStateGroupsWidgetFilters, TIssuesByStateGroupsWidgetResponse, TStateGroups } from "@plane/types"; -// components -import { Card } from "@plane/ui"; -import { - DurationFilterDropdown, - IssuesByStateGroupEmptyState, - WidgetLoader, - WidgetProps, -} from "@/components/dashboard/widgets"; -import { PieGraph } from "@/components/ui"; -// constants -import { EDurationFilters, STATE_GROUP_GRAPH_COLORS, STATE_GROUP_GRAPH_GRADIENTS } from "@/constants/dashboard"; -import { STATE_GROUPS } from "@/constants/state"; -// helpers -import { getCustomDates } from "@/helpers/dashboard.helper"; -// hooks -import { useDashboard } from "@/hooks/store"; -import { useAppRouter } from "@/hooks/use-app-router"; - -const WIDGET_KEY = "issues_by_state_groups"; - -export const IssuesByStateGroupWidget: React.FC = observer((props) => { - const { dashboardId, workspaceSlug } = props; - // states - const [defaultStateGroup, setDefaultStateGroup] = useState(null); - const [activeStateGroup, setActiveStateGroup] = useState(null); - // router - const router = useAppRouter(); - // store hooks - const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard(); - // derived values - const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); - const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const selectedDuration = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; - const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; - - const handleUpdateFilters = async (filters: Partial) => { - if (!widgetDetails) return; - - await updateDashboardWidgetFilters(workspaceSlug, dashboardId, widgetDetails.id, { - widgetKey: WIDGET_KEY, - filters, - }); - - const filterDates = getCustomDates( - filters.duration ?? selectedDuration, - filters.custom_dates ?? selectedCustomDates - ); - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), - }); - }; - - // fetch widget stats - useEffect(() => { - const filterDates = getCustomDates(selectedDuration, selectedCustomDates); - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // set active group for center metric - useEffect(() => { - if (!widgetStats) return; - - const startedCount = widgetStats?.find((item) => item?.state === "started")?.count ?? 0; - const unStartedCount = widgetStats?.find((item) => item?.state === "unstarted")?.count ?? 0; - const backlogCount = widgetStats?.find((item) => item?.state === "backlog")?.count ?? 0; - const completedCount = widgetStats?.find((item) => item?.state === "completed")?.count ?? 0; - const canceledCount = widgetStats?.find((item) => item?.state === "cancelled")?.count ?? 0; - - const stateGroup = - startedCount > 0 - ? "started" - : unStartedCount > 0 - ? "unstarted" - : backlogCount > 0 - ? "backlog" - : completedCount > 0 - ? "completed" - : canceledCount > 0 - ? "cancelled" - : null; - - setActiveStateGroup(stateGroup); - setDefaultStateGroup(stateGroup); - }, [widgetStats]); - - if (!widgetDetails || !widgetStats) return ; - - const totalCount = widgetStats?.reduce((acc, item) => acc + item?.count, 0); - const chartData = widgetStats?.map((item) => ({ - color: STATE_GROUP_GRAPH_COLORS[item?.state as keyof typeof STATE_GROUP_GRAPH_COLORS], - id: item?.state, - label: item?.state, - value: (item?.count / totalCount) * 100, - })); - - const CenteredMetric = ({ dataWithArc, centerX, centerY }: any) => { - const data = dataWithArc?.find((datum: any) => datum?.id === activeStateGroup); - const percentage = chartData?.find((item) => item.id === activeStateGroup)?.value?.toFixed(0); - - return ( - - - {percentage}% - - - {data?.id} - - - ); - }; - - return ( - -
- - Assigned by state - - - handleUpdateFilters({ - duration: val, - ...(val === "custom" ? { custom_dates: customDates } : {}), - }) - } - /> -
- {totalCount > 0 ? ( -
-
-
- datum.data.color} - padAngle={1} - enableArcLinkLabels={false} - enableArcLabels={false} - activeOuterRadiusOffset={5} - tooltip={() => <>} - margin={{ - top: 0, - right: 5, - bottom: 0, - left: 5, - }} - defs={STATE_GROUP_GRAPH_GRADIENTS} - fill={Object.values(STATE_GROUPS).map((p) => ({ - match: { - id: p.key, - }, - id: `gradient${p.label}`, - }))} - onClick={(datum, e) => { - e.preventDefault(); - e.stopPropagation(); - router.push(`/${workspaceSlug}/workspace-views/assigned/?state_group=${datum.id}`); - }} - onMouseEnter={(datum) => setActiveStateGroup(datum.id as TStateGroups)} - onMouseLeave={() => setActiveStateGroup(defaultStateGroup)} - layers={["arcs", CenteredMetric]} - /> -
-
- {chartData.map((item) => ( -
-
-
- {item.label} -
- {item.value.toFixed(0)}% -
- ))} -
-
-
- ) : ( -
- -
- )} - - ); -}); diff --git a/web/core/components/dashboard/widgets/overview-stats.tsx b/web/core/components/dashboard/widgets/overview-stats.tsx deleted file mode 100644 index 62200102777..00000000000 --- a/web/core/components/dashboard/widgets/overview-stats.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { useEffect } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import { TOverviewStatsWidgetResponse } from "@plane/types"; -// hooks -import { Card, ECardSpacing } from "@plane/ui"; -import { WidgetLoader } from "@/components/dashboard/widgets"; -import { cn } from "@/helpers/common.helper"; -import { renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -import { useDashboard } from "@/hooks/store"; -// components -// helpers -// types - -export type WidgetProps = { - dashboardId: string; - workspaceSlug: string; -}; - -const WIDGET_KEY = "overview_stats"; - -export const OverviewStatsWidget: React.FC = observer((props) => { - const { dashboardId, workspaceSlug } = props; - // store hooks - const { fetchWidgetStats, getWidgetStats } = useDashboard(); - // derived values - const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - - const today = renderFormattedPayloadDate(new Date()); - const STATS_LIST = [ - { - key: "assigned", - title: "Issues assigned", - count: widgetStats?.assigned_issues_count, - link: `/${workspaceSlug}/workspace-views/assigned`, - }, - { - key: "overdue", - title: "Issues overdue", - count: widgetStats?.pending_issues_count, - link: `/${workspaceSlug}/workspace-views/assigned/?state_group=backlog,unstarted,started&target_date=${today};before`, - }, - { - key: "created", - title: "Issues created", - count: widgetStats?.created_issues_count, - link: `/${workspaceSlug}/workspace-views/created`, - }, - { - key: "completed", - title: "Issues completed", - count: widgetStats?.completed_issues_count, - link: `/${workspaceSlug}/workspace-views/assigned?state_group=completed`, - }, - ]; - - useEffect(() => { - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (!widgetStats) return ; - - return ( - - {STATS_LIST.map((stat, index) => ( -
- -
-
-
{stat.count}
-

{stat.title}

-
-
- -
- ))} -
- ); -}); diff --git a/web/core/components/dashboard/widgets/recent-activity.tsx b/web/core/components/dashboard/widgets/recent-activity.tsx index dd21815ccce..9f7f041bf88 100644 --- a/web/core/components/dashboard/widgets/recent-activity.tsx +++ b/web/core/components/dashboard/widgets/recent-activity.tsx @@ -1,26 +1,64 @@ "use client"; -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; +import { IssueIdentifier } from "ee/components/issues"; import { observer } from "mobx-react"; import Link from "next/link"; import { History } from "lucide-react"; // types import { TRecentActivityWidgetResponse } from "@plane/types"; // components -import { Card, Avatar, getButtonStyling } from "@plane/ui"; -import { ActivityIcon, ActivityMessage, IssueLink } from "@/components/core"; +import { getButtonStyling, PriorityIcon } from "@plane/ui"; +import { ListItem } from "@/components/core/list"; import { RecentActivityEmptyState, WidgetLoader, WidgetProps } from "@/components/dashboard/widgets"; // helpers import { cn } from "@/helpers/common.helper"; -import { calculateTimeAgo } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useDashboard, useUser } from "@/hooks/store"; const WIDGET_KEY = "recent_activity"; +type BlockProps = { + activity: TRecentActivityWidgetResponse; + ref: React.RefObject; +}; +const Block = (props: BlockProps) => { + const { activity, ref } = props; + console.log({ ...activity.issue_detail }); + return ( + + +
+ {activity.issue_detail?.name} +
+
+ } + quickActionElement={ +
+ +
+ } + parentRef={ref} + disableLink={false} + className="bg-transparent my-auto !px-2 border-custom-border-200/40 py-3" + /> + ); +}; + export const RecentActivityWidget: React.FC = observer((props) => { const { dashboardId, workspaceSlug } = props; + const ref = useRef(null); // store hooks const { data: currentUser } = useUser(); // derived values @@ -38,72 +76,29 @@ export const RecentActivityWidget: React.FC = observer((props) => { if (!widgetStats) return ; return ( - - - Your issue activities - +
+
+ + Recent issues{" "} + + + View all + +
{widgetStats.length > 0 ? ( -
+
{widgetStats.map((activity) => ( -
-
- {activity.field ? ( - activity.new_value === "restore" ? ( - - ) : ( -
- -
- ) - ) : activity.actor_detail.avatar_url && activity.actor_detail.avatar_url !== "" ? ( - - ) : ( -
- {activity.actor_detail.is_bot - ? activity.actor_detail.first_name.charAt(0) - : activity.actor_detail.display_name.charAt(0)} -
- )} -
-
-

- - {currentUser?.id === activity.actor_detail.id ? "You" : activity.actor_detail?.display_name}{" "} - - {activity.field ? ( - - ) : ( - - created - - )} -

-

- {calculateTimeAgo(activity.created_at)} -

-
-
+ ))} - - View all -
) : (
)} - +
); }); diff --git a/web/core/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx b/web/core/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx deleted file mode 100644 index 2dc1f0d3d99..00000000000 --- a/web/core/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx +++ /dev/null @@ -1,154 +0,0 @@ -"use client"; -import { useState } from "react"; -import sortBy from "lodash/sortBy"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import useSWR from "swr"; -// types -import { TRecentCollaboratorsWidgetResponse } from "@plane/types"; -// ui -import { Avatar } from "@plane/ui"; -// helpers -import { getFileURL } from "@/helpers/file.helper"; -// hooks -import { useDashboard, useMember, useUser } from "@/hooks/store"; -// components -import { WidgetLoader } from "../loaders"; - -type CollaboratorListItemProps = { - issueCount: number; - userId: string; - workspaceSlug: string; -}; - -const CollaboratorListItem: React.FC = observer((props) => { - const { issueCount, userId, workspaceSlug } = props; - // store hooks - const { data: currentUser } = useUser(); - const { getUserDetails } = useMember(); - // derived values - const userDetails = getUserDetails(userId); - const isCurrentUser = userId === currentUser?.id; - - if (!userDetails || userDetails.is_bot) return null; - - return ( - -
- -
-
- {isCurrentUser ? "You" : userDetails?.display_name} -
-

- {issueCount} active issue{issueCount > 1 ? "s" : ""} -

- - ); -}); - -type CollaboratorsListProps = { - dashboardId: string; - searchQuery?: string; - workspaceSlug: string; -}; - -const WIDGET_KEY = "recent_collaborators"; - -export const CollaboratorsList: React.FC = (props) => { - const { dashboardId, searchQuery = "", workspaceSlug } = props; - - // state - const [visibleItems, setVisibleItems] = useState(16); - const [isExpanded, setIsExpanded] = useState(false); - // store hooks - const { fetchWidgetStats } = useDashboard(); - const { getUserDetails } = useMember(); - const { data: currentUser } = useUser(); - - const { data: widgetStats } = useSWR( - workspaceSlug && dashboardId ? `WIDGET_STATS_${workspaceSlug}_${dashboardId}` : null, - workspaceSlug && dashboardId - ? () => - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - }) - : null - ) as { - data: TRecentCollaboratorsWidgetResponse[] | undefined; - }; - - if (!widgetStats) - return ( -
- -
- ); - - const sortedStats = sortBy(widgetStats, [(user) => user?.user_id !== currentUser?.id]); - - const filteredStats = sortedStats.filter((user) => { - if (!user) return false; - const userDetails = getUserDetails(user?.user_id); - if (!userDetails || userDetails.is_bot) return false; - const { display_name, first_name, last_name } = userDetails; - const searchLower = searchQuery.toLowerCase(); - return ( - display_name?.toLowerCase().includes(searchLower) || - first_name?.toLowerCase().includes(searchLower) || - last_name?.toLowerCase().includes(searchLower) - ); - }); - - // Update the displayedStats to always use the visibleItems limit - const handleLoadMore = () => { - setVisibleItems((prev) => { - const newValue = prev + 16; - if (newValue >= filteredStats.length) { - setIsExpanded(true); - return filteredStats.length; - } - return newValue; - }); - }; - - const handleHide = () => { - setVisibleItems(16); - setIsExpanded(false); - }; - - const displayedStats = filteredStats.slice(0, visibleItems); - - return ( - <> -
- {displayedStats?.map((user) => ( - - ))} -
- {filteredStats.length > visibleItems && !isExpanded && ( -
-
- Load more -
-
- )} - {isExpanded && ( -
-
Hide
-
- )} - - ); -}; diff --git a/web/core/components/dashboard/widgets/recent-collaborators/index.ts b/web/core/components/dashboard/widgets/recent-collaborators/index.ts deleted file mode 100644 index 1efe34c51ec..00000000000 --- a/web/core/components/dashboard/widgets/recent-collaborators/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./root"; diff --git a/web/core/components/dashboard/widgets/recent-collaborators/root.tsx b/web/core/components/dashboard/widgets/recent-collaborators/root.tsx deleted file mode 100644 index 4d30642071b..00000000000 --- a/web/core/components/dashboard/widgets/recent-collaborators/root.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useState } from "react"; -import { Search } from "lucide-react"; -// types -import { Card } from "@plane/ui"; -import { WidgetProps } from "@/components/dashboard/widgets"; -// components -import { CollaboratorsList } from "./collaborators-list"; - -export const RecentCollaboratorsWidget: React.FC = (props) => { - const { dashboardId, workspaceSlug } = props; - // states - const [searchQuery, setSearchQuery] = useState(""); - - return ( - -
-
-

Collaborators

-

- View and find all members you collaborate with across projects -

-
-
- - setSearchQuery(e.target.value)} - /> -
-
- -
- ); -}; diff --git a/web/core/components/dashboard/widgets/recent-pages.tsx b/web/core/components/dashboard/widgets/recent-pages.tsx new file mode 100644 index 00000000000..270e42857ac --- /dev/null +++ b/web/core/components/dashboard/widgets/recent-pages.tsx @@ -0,0 +1,145 @@ +"use client"; + +import { useEffect } from "react"; +import { observer } from "mobx-react"; +import Link from "next/link"; +import { Plus } from "lucide-react"; +// plane types +import { TRecentProjectsWidgetResponse } from "@plane/types"; +// plane ui +import { Avatar, AvatarGroup, Card } from "@plane/ui"; +// components +import { Logo } from "@/components/common"; +import { WidgetLoader, WidgetProps } from "@/components/dashboard/widgets"; +// constants +import { PROJECT_BACKGROUND_COLORS } from "@/constants/dashboard"; +// helpers +import { getFileURL } from "@/helpers/file.helper"; +// hooks +import { useEventTracker, useDashboard, useProject, useCommandPalette, useUserPermissions } from "@/hooks/store"; +// plane web constants +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; + +import Tiles from "./my-stickies"; + +const WIDGET_KEY = "recent_projects"; + +type PageListItemProps = { + pageId: string; + workspaceSlug: string; +}; + +const PageListItem: React.FC = observer((props) => { + const { pageId, workspaceSlug } = props; + // store hooks + const { getProjectById } = useProject(); + const pageDetails = getProjectById(pageId); + + const randomBgColor = PROJECT_BACKGROUND_COLORS[Math.floor(Math.random() * PROJECT_BACKGROUND_COLORS.length)]; + + if (!pageDetails) return null; + + return ( +
+ {/* */} + +
Pulse
+
Plane Check point meetings - December 2025
+
+ The below are the required design components. @sibira @shivangi @shrabani @bhavesh please take a look at them.{" "} +
+
+
+ + {pageDetails.members?.map((member) => ( + + ))} + +
+
Last updated 2h ago
+
+
+ ); +}); + +export const RecentPagesWidget: React.FC = observer((props) => { + const { dashboardId, workspaceSlug } = props; + // store hooks + const { toggleCreateProjectModal } = useCommandPalette(); + const { setTrackElement } = useEventTracker(); + const { allowPermissions } = useUserPermissions(); + const { fetchWidgetStats, getWidgetStats } = useDashboard(); + // derived values + const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); + const canCreateProject = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + + useEffect(() => { + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (!widgetStats) return ; + + return ( +
+
+ + Recent pages + +
+ {canCreateProject && ( + + )} + +
+
+ +
+ {widgetStats.map((pageId) => ( + + ))} +
+
+ ); +}); diff --git a/web/core/components/dashboard/widgets/recent-projects.tsx b/web/core/components/dashboard/widgets/recent-projects.tsx index 5255908717e..9d5c19b6ec3 100644 --- a/web/core/components/dashboard/widgets/recent-projects.tsx +++ b/web/core/components/dashboard/widgets/recent-projects.tsx @@ -38,7 +38,10 @@ const ProjectListItem: React.FC = observer((props) => { if (!projectDetails) return null; return ( - +
@@ -90,15 +93,31 @@ export const RecentProjectsWidget: React.FC = observer((props) => { if (!widgetStats) return ; return ( - - - Recent projects - -
- {canCreateProject && ( +
+
+ + Recent projects + +
+ {canCreateProject && ( + + )} - )} +
+
+
{widgetStats.map((projectId) => ( ))}
- +
); }); diff --git a/web/core/components/dashboard/widgets/stickies/index.tsx b/web/core/components/dashboard/widgets/stickies/index.tsx new file mode 100644 index 00000000000..9c91656c3ae --- /dev/null +++ b/web/core/components/dashboard/widgets/stickies/index.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { useEffect } from "react"; +import { observer } from "mobx-react"; +import Link from "next/link"; +import { Plus } from "lucide-react"; +// plane types +import { TRecentProjectsWidgetResponse } from "@plane/types"; +// plane ui +import { Avatar, AvatarGroup, Card } from "@plane/ui"; +// components +import { Logo } from "@/components/common"; +import { WidgetLoader } from "@/components/dashboard/widgets"; +// constants +import { PROJECT_BACKGROUND_COLORS } from "@/constants/dashboard"; +// helpers +import { getFileURL } from "@/helpers/file.helper"; +// hooks +import { useEventTracker, useDashboard, useProject, useCommandPalette, useUserPermissions } from "@/hooks/store"; +// plane web constants +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; +import { StickiesLayout } from "./stickies-layout"; + +const WIDGET_KEY = "recent_projects"; + +export const StickiesWidget: React.FC = observer((props) => { + const { dashboardId, workspaceSlug } = props; + // store hooks + const { toggleCreateProjectModal } = useCommandPalette(); + const { setTrackElement } = useEventTracker(); + const { allowPermissions } = useUserPermissions(); + const { fetchWidgetStats, getWidgetStats } = useDashboard(); + // derived values + const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); + const canCreateProject = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + + useEffect(() => { + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (!widgetStats) return ; + + return ( +
+
+ + My Stickies{" "} + +
+ {canCreateProject && ( + + )} +
+
+
+ + +
+
+ ); +}); diff --git a/web/core/components/dashboard/widgets/stickies/stickies-layout.tsx b/web/core/components/dashboard/widgets/stickies/stickies-layout.tsx new file mode 100644 index 00000000000..fb80030784f --- /dev/null +++ b/web/core/components/dashboard/widgets/stickies/stickies-layout.tsx @@ -0,0 +1,146 @@ +import React, { cloneElement, useState, useEffect } from "react"; + +interface TileContent { + title: string; + description: string; +} + +export const TILES: TileContent[] = [ + { + title: "Product Roadmap", + description: + "Q1 2024 product strategy and milestones. This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments.This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments.", + }, + { + title: "Design System", + description: "Component library and design tokens documentation with guidelines for implementation.", + }, + { + title: "User Research", + description: + "Customer interviews and feedback analysis for new features. Includes synthesis of user testing sessions and recommendations for product improvements.", + }, + { + title: "Sprint Planning", + description: "Next sprint goals and task allocation with detailed breakdown of upcoming work items.", + }, + { + title: "Team Updates", + description: "Weekly progress updates and current blockers affecting development timeline.", + }, + { + title: "Release Notes", + description: + "Latest features and bug fixes documentation with detailed changelog and migration guides for developers.", + }, + { + title: "Performance Metrics", + description: + "Key performance indicators and system metrics tracking. This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments.", + }, + { + title: "Documentation", + description: "Technical documentation and API references for the development team.", + }, + { + title: "Product Roadmap", + description: + "Q1 2024 product strategy and milestones. This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments.This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments.", + }, + { + title: "Design System", + description: "Component library and design tokens documentation with guidelines for implementation.", + }, + { + title: "User Research", + description: + "Customer interviews and feedback analysis for new features. Includes synthesis of user testing sessions and recommendations for product improvements.", + }, + { + title: "Sprint Planning", + description: "Next sprint goals and task allocation with detailed breakdown of upcoming work items.", + }, + { + title: "Team Updates", + description: "Weekly progress updates and current blockers affecting development timeline.", + }, + { + title: "Release Notes", + description: + "Latest features and bug fixes documentation with detailed changelog and migration guides for developers. Component library and design tokens documentation with guidelines for implementation. Component library and design tokens documentation with guidelines for implementation. Component library and design tokens documentation with guidelines for implementation.", + }, + { + title: "Performance Metrics", + description: + "Key performance indicators and system metrics tracking. This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments.", + }, + { + title: "Documentation", + description: "Technical documentation and API references for the development team.", + }, +]; + +interface GridProps { + children: React.ReactNode; + columnCount: number; +} + +const Grid: React.FC = ({ children, columnCount }) => { + const cols = [...Array(columnCount).keys()]; + const rows = [...Array(Math.ceil(React.Children.count(children) / columnCount)).keys()]; + + return ( +
+ {cols.map((col: number, index: number) => ( +
+ {rows.map((row: number) => { + const child = React.Children.toArray(children)[row * columnCount + col]; + return child && cloneElement(child as React.ReactElement); + })} +
+ ))} +
+ ); +}; + +export const StickiesLayout = () => { + const [columnCount, setColumnCount] = useState(4); + + useEffect(() => { + const updateColumnCount = () => { + if (window.matchMedia("(min-width: 1024px)").matches) { + setColumnCount(4); // lg screens + } else if (window.matchMedia("(min-width: 768px)").matches) { + setColumnCount(3); // md screens + } else if (window.matchMedia("(min-width: 640px)").matches) { + setColumnCount(2); // sm screens + } else { + setColumnCount(1); // mobile + } + }; + + // Initial check + updateColumnCount(); + + // Add event listener for window resize + window.addEventListener("resize", updateColumnCount); + + // Cleanup + return () => window.removeEventListener("resize", updateColumnCount); + }, []); + + return ( +
+ + {TILES.map((content: TileContent, index) => ( +
+

+ {index} {content.title} +

+

{content.description}

+
+ ))} +
+
+ ); +}; diff --git a/web/core/components/page-views/workspace-dashboard.tsx b/web/core/components/page-views/workspace-dashboard.tsx index 05546af56fc..095ffab941d 100644 --- a/web/core/components/page-views/workspace-dashboard.tsx +++ b/web/core/components/page-views/workspace-dashboard.tsx @@ -65,9 +65,9 @@ export const WorkspaceDashboardView = observer(() => { <> {joinedProjectIds.length > 0 || loader ? ( <> - + {/* */} = 768, })} > diff --git a/web/core/components/user/user-greetings.tsx b/web/core/components/user/user-greetings.tsx index 216da7a5292..d498fa51542 100644 --- a/web/core/components/user/user-greetings.tsx +++ b/web/core/components/user/user-greetings.tsx @@ -38,10 +38,10 @@ export const UserGreetingsView: FC = (props) => { return (
-

+

Good {greeting}, {user?.first_name} {user?.last_name}

-
+
{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}
{weekDay}, {date} {timeString} diff --git a/web/core/services/workspace.service.ts b/web/core/services/workspace.service.ts index e1aa9b7cd2b..585e7d0f76f 100644 --- a/web/core/services/workspace.service.ts +++ b/web/core/services/workspace.service.ts @@ -12,6 +12,7 @@ import { IUserProjectsRole, IWorkspaceView, TIssuesResponse, + TLink, } from "@plane/types"; import { APIService } from "@/services/api.service"; // helpers @@ -277,4 +278,42 @@ export class WorkspaceService extends APIService { throw error?.response?.data; }); } + + // quick links + async fetchWorkspaceLinks(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/links/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async createWorkspaceLink(workspaceSlug: string, projectId: string, data: Partial): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/links/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async updateWorkspaceLink( + workspaceSlug: string, + projectId: string, + linkId: string, + data: Partial + ): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/links/${linkId}/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async deleteWorkspaceLink(workspaceSlug: string, projectId: string, linkId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/links/${linkId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } } diff --git a/web/core/store/workspace/index.ts b/web/core/store/workspace/index.ts index 950fc8ddbc5..8de02944fd2 100644 --- a/web/core/store/workspace/index.ts +++ b/web/core/store/workspace/index.ts @@ -8,6 +8,7 @@ import { WorkspaceService } from "@/plane-web/services"; import { CoreRootStore } from "@/store/root.store"; // sub-stores import { ApiTokenStore, IApiTokenStore } from "./api-token.store"; +import { IWorkspaceLinkStore, WorkspaceLinkStore } from "./link.store"; import { IWebhookStore, WebhookStore } from "./webhook.store"; export interface IWorkspaceRootStore { @@ -30,6 +31,7 @@ export interface IWorkspaceRootStore { // sub-stores webhook: IWebhookStore; apiToken: IApiTokenStore; + links: IWorkspaceLinkStore; } export class WorkspaceRootStore implements IWorkspaceRootStore { @@ -41,6 +43,7 @@ export class WorkspaceRootStore implements IWorkspaceRootStore { // root store router; user; + links; // sub-stores webhook: IWebhookStore; apiToken: IApiTokenStore; @@ -69,6 +72,7 @@ export class WorkspaceRootStore implements IWorkspaceRootStore { // root store this.router = _rootStore.router; this.user = _rootStore.user; + this.links = new WorkspaceLinkStore(); // sub-stores this.webhook = new WebhookStore(_rootStore); this.apiToken = new ApiTokenStore(_rootStore); diff --git a/web/core/store/workspace/link.store.ts b/web/core/store/workspace/link.store.ts new file mode 100644 index 00000000000..60af48284bc --- /dev/null +++ b/web/core/store/workspace/link.store.ts @@ -0,0 +1,128 @@ +import set from "lodash/set"; +import { action, makeObservable, observable, runInAction } from "mobx"; +// types +import { TLink, TLinkIdMap, TLinkMap } from "@plane/types"; +// services +import { WorkspaceService } from "@/plane-web/services"; + +export interface IWorkspaceLinkStoreActions { + addLinks: (projectId: string, links: TLink[]) => void; + fetchLinks: (workspaceSlug: string, projectId: string) => Promise; + createLink: (workspaceSlug: string, data: Partial) => Promise; + updateLink: (workspaceSlug: string, linkId: string, data: Partial) => Promise; + removeLink: (workspaceSlug: string, linkId: string) => Promise; + setLinkData: (link: TLink | undefined) => void; + toggleLinkModal: (isOpen: boolean) => void; +} + +export interface IWorkspaceLinkStore extends IWorkspaceLinkStoreActions { + // observables + links: TLinkIdMap; + linkMap: TLinkMap; + linkData: TLink | undefined; + isLinkModalOpen: boolean; + // helper methods + getLinksByWorkspaceId: (projectId: string) => string[] | undefined; + getLinkById: (linkId: string) => TLink | undefined; +} + +export class WorkspaceLinkStore implements IWorkspaceLinkStore { + // observables + links: TLinkIdMap = {}; + linkMap: TLinkMap = {}; + linkData: TLink | undefined = undefined; + isLinkModalOpen = false; + // services + workspaceService: WorkspaceService; + + constructor() { + makeObservable(this, { + // observables + links: observable, + linkMap: observable, + linkData: observable, + isLinkModalOpen: observable, + // actions + addLinks: action.bound, + fetchLinks: action, + createLink: action, + updateLink: action, + removeLink: action, + setLinkData: action, + toggleLinkModal: action, + }); + // services + this.workspaceService = new WorkspaceService(); + } + + // helper methods + getLinksByWorkspaceId = (projectId: string) => { + if (!projectId) return undefined; + return this.links[projectId] ?? undefined; + }; + + getLinkById = (linkId: string) => { + if (!linkId) return undefined; + return this.linkMap[linkId] ?? undefined; + }; + + // actions + setLinkData = (link: TLink | undefined) => { + runInAction(() => { + this.linkData = link; + }); + }; + + toggleLinkModal = (isOpen: boolean) => { + runInAction(() => { + this.isLinkModalOpen = isOpen; + }); + }; + + addLinks = (projectId: string, links: TLink[]) => { + runInAction(() => { + this.links[projectId] = links.map((link) => link.id); + links.forEach((link) => set(this.linkMap, link.id, link)); + }); + }; + + fetchLinks = async (workspaceSlug: string, projectId: string) => { + const response = await this.workspaceService.fetchWorkspaceLinks(workspaceSlug, projectId); + this.addLinks(projectId, response); + return response; + }; + + createLink = async (workspaceSlug: string, data: Partial) => { + // const response = await this.workspaceService.createWorkspaceLink(workspaceSlug, projectId, data); + // const issueLinkCount = this.getLinksByWorkspaceId(projectId)?.length ?? 0; + // runInAction(() => { + // this.links[workspaceSlug] = [response.id, ...this.links[workspaceSlug]]; + // set(this.linkMap, response.id, response); + // }); + // return response; + }; + + updateLink = async (workspaceSlug: string, linkId: string, data: Partial) => { + runInAction(() => { + Object.keys(data).forEach((key) => { + set(this.linkMap, [linkId, key], data[key as keyof TLink]); + }); + }); + + // const response = await this.workspaceService.updateWorkspaceLink(workspaceSlug, projectId, linkId, data); + + // return response; + }; + + removeLink = async (workspaceSlug: string, linkId: string) => { + // const issueLinkCount = this.getLinksByWorkspaceId(projectId)?.length ?? 0; + // await this.workspaceService.deleteWorkspaceLink(workspaceSlug, projectId, linkId); + + const linkIndex = this.links[workspaceSlug].findIndex((link) => link === linkId); + if (linkIndex >= 0) + runInAction(() => { + this.links[workspaceSlug].splice(linkIndex, 1); + delete this.linkMap[linkId]; + }); + }; +} diff --git a/web/ee/components/stickies/index.ts b/web/ee/components/stickies/index.ts new file mode 100644 index 00000000000..81560ba6812 --- /dev/null +++ b/web/ee/components/stickies/index.ts @@ -0,0 +1 @@ +export * from "ce/components/stickies"; From ae89bed07b9c28856ae42cb5f6b3c9c47f160ccf Mon Sep 17 00:00:00 2001 From: gakshita Date: Tue, 24 Dec 2024 15:56:24 +0530 Subject: [PATCH 02/18] chore: wip --- packages/types/src/dashboard.d.ts | 5 +++ web/ce/components/home/header.tsx | 1 + .../dashboard/home-dashboard-widgets.tsx | 44 +++++++++++-------- .../components/dashboard/links/action.tsx | 11 ++++- .../links/create-update-link-modal.tsx | 10 +++-- .../dashboard/links/link-detail.tsx | 20 +++------ web/core/components/dashboard/links/root.tsx | 20 +++++++-- .../dashboard/widgets/empty-states/root.tsx | 26 +++++++---- .../dashboard/widgets/recent-activity.tsx | 8 +--- .../dashboard/widgets/recent-pages.tsx | 32 -------------- .../dashboard/widgets/recent-projects.tsx | 16 ++----- 11 files changed, 92 insertions(+), 101 deletions(-) create mode 100644 web/ce/components/home/header.tsx diff --git a/packages/types/src/dashboard.d.ts b/packages/types/src/dashboard.d.ts index b2bf6ca88e0..2019b44392e 100644 --- a/packages/types/src/dashboard.d.ts +++ b/packages/types/src/dashboard.d.ts @@ -4,6 +4,11 @@ import { TIssue } from "./issues/issue"; import { TStateGroups } from "./state"; import { TIssueRelationTypes } from "@/plane-web/types"; +export type WidgetProps = { + dashboardId: string; + workspaceSlug: string; +}; + export type TWidgetKeys = | "quick_links" | "overview_stats" diff --git a/web/ce/components/home/header.tsx b/web/ce/components/home/header.tsx new file mode 100644 index 00000000000..c95736c9ebd --- /dev/null +++ b/web/ce/components/home/header.tsx @@ -0,0 +1 @@ +export const HomePageHeader = () => <>; diff --git a/web/core/components/dashboard/home-dashboard-widgets.tsx b/web/core/components/dashboard/home-dashboard-widgets.tsx index 6715a992585..3753163b8de 100644 --- a/web/core/components/dashboard/home-dashboard-widgets.tsx +++ b/web/core/components/dashboard/home-dashboard-widgets.tsx @@ -5,8 +5,8 @@ import { TWidgetKeys, WidgetProps } from "@plane/types"; // components import { RecentActivityWidget, RecentProjectsWidget, RecentPagesWidget, EmptyWorkspace } from "@/components/dashboard"; // hooks -import { useDashboard } from "@/hooks/store"; -import { StickiesWidget } from "@/plane-web/components/stickies"; +import { useDashboard, useProject } from "@/hooks/store"; +import { HomePageHeader } from "@/plane-web/components/home/header"; import { DashboardQuickLinks } from "./links"; const WIDGETS_LIST: { @@ -16,12 +16,13 @@ const WIDGETS_LIST: { recent_pages: { component: RecentPagesWidget, fullWidth: false }, recent_projects: { component: RecentProjectsWidget, fullWidth: false }, // recent_collaborators: { component: RecentCollaboratorsWidget, fullWidth: true }, - my_stickies: { component: StickiesWidget, fullWidth: false }, + // my_stickies: { component: StickiesWidget, fullWidth: false }, }; export const DashboardWidgets = observer(() => { // router const { workspaceSlug } = useParams(); + const { totalProjectIds } = useProject(); // store hooks const { homeDashboardId, homeDashboardWidgets } = useDashboard(); @@ -32,23 +33,28 @@ export const DashboardWidgets = observer(() => { return (
+ - - {Object.entries(WIDGETS_LIST).map(([key, widget]) => { - const WidgetComponent = widget.component; - // if the widget doesn't exist, return null - // if (!doesWidgetExist(key as TWidgetKeys)) return null; - // if the widget is full width, return it in a 2 column grid - console.log({ widget, key }); - if (widget.fullWidth) - return ( -
- -
- ); - else - return ; - })} + + {totalProjectIds?.length === 0 ? ( + + ) : ( + Object.entries(WIDGETS_LIST).map(([key, widget]) => { + const WidgetComponent = widget.component; + // if the widget doesn't exist, return null + // if (!doesWidgetExist(key as TWidgetKeys)) return null; + // if the widget is full width, return it in a 2 column grid + console.log({ widget, key }); + if (widget.fullWidth) + return ( +
+ +
+ ); + else + return ; + }) + )}
); }); diff --git a/web/core/components/dashboard/links/action.tsx b/web/core/components/dashboard/links/action.tsx index b395d757e55..e572a414147 100644 --- a/web/core/components/dashboard/links/action.tsx +++ b/web/core/components/dashboard/links/action.tsx @@ -1,9 +1,16 @@ import { PlusIcon } from "lucide-react"; -export const AddLink = () => { +type TProps = { + onClick: () => void; +}; +export const AddLink = (props: TProps) => { + const { onClick } = props; console.log("AddLink"); return ( -
diff --git a/web/core/components/dashboard/links/link-detail.tsx b/web/core/components/dashboard/links/link-detail.tsx index ff5df06de35..bbb0e07ddc4 100644 --- a/web/core/components/dashboard/links/link-detail.tsx +++ b/web/core/components/dashboard/links/link-detail.tsx @@ -4,18 +4,12 @@ import { FC } from "react"; // hooks // ui import { observer } from "mobx-react"; -import { Pencil, Trash2, LinkIcon, ExternalLink, Paperclip, EllipsisVertical, Link } from "lucide-react"; -import { Tooltip, TOAST_TYPE, setToast, CustomMenu, TContextMenuItem } from "@plane/ui"; -// icons -// types +import { Pencil, Trash2, ExternalLink, Paperclip, EllipsisVertical, Link } from "lucide-react"; +import { TOAST_TYPE, setToast, CustomMenu, TContextMenuItem } from "@plane/ui"; // helpers import { cn } from "@plane/utils"; -import { calculateTimeAgo } from "@/helpers/date-time.helper"; -import { copyTextToClipboard, copyUrlToClipboard } from "@/helpers/string.helper"; -import { useMember, useWorkspace } from "@/hooks/store"; -import { usePlatformOS } from "@/hooks/use-platform-os"; - -import { TLinkOperationsModal } from "./create-update-link-modal"; +import { copyUrlToClipboard } from "@/helpers/string.helper"; +import { useWorkspace } from "@/hooks/store"; import { TLinkOperations } from "./use-links"; export type TProjectLinkDetail = { @@ -31,15 +25,13 @@ export const ProjectLinkDetail: FC = observer((props) => { const { links: { getLinkById, toggleLinkModal, setLinkData }, } = useWorkspace(); - const { getUserDetails } = useMember(); - const { isMobile } = usePlatformOS(); const linkDetail = getLinkById(linkId); if (!linkDetail) return <>; const viewLink = linkDetail.url; - const toggleProjectLinkModal = (modalToggle: boolean) => { + const handleEdit = (modalToggle: boolean) => { toggleLinkModal(modalToggle); setLinkData(linkDetail); }; @@ -57,7 +49,7 @@ export const ProjectLinkDetail: FC = observer((props) => { const MENU_ITEMS: TContextMenuItem[] = [ { key: "edit", - action: () => linkOperations.update(), + action: () => handleEdit(true), title: "Edit", icon: Pencil, shouldRender: isNotAllowed, diff --git a/web/core/components/dashboard/links/root.tsx b/web/core/components/dashboard/links/root.tsx index e4902da1bb2..7d274bca3c9 100644 --- a/web/core/components/dashboard/links/root.tsx +++ b/web/core/components/dashboard/links/root.tsx @@ -1,22 +1,36 @@ +import { observer } from "mobx-react"; +import { useWorkspace } from "@/hooks/store"; import { AddLink } from "./action"; +import { LinkCreateUpdateModal } from "./create-update-link-modal"; import { ProjectLinkList } from "./links"; import { useLinks } from "./use-links"; type TProps = { workspaceSlug: string; }; -export const DashboardQuickLinks = (props: TProps) => { +export const DashboardQuickLinks = observer((props: TProps) => { const { workspaceSlug } = props; const { linkOperations } = useLinks(workspaceSlug); + const { + links: { isLinkModalOpen, toggleLinkModal, linkData, setLinkData }, + } = useWorkspace(); + return ( <> + toggleLinkModal(false)} + linkOperations={linkOperations} + preloadedData={linkData} + setLinkData={setLinkData} + />
{/* rendering links */} {/* Add new link */} - + toggleLinkModal(true)} />
); -}; +}); diff --git a/web/core/components/dashboard/widgets/empty-states/root.tsx b/web/core/components/dashboard/widgets/empty-states/root.tsx index d97c1e6195a..fe6c8e5f3b2 100644 --- a/web/core/components/dashboard/widgets/empty-states/root.tsx +++ b/web/core/components/dashboard/widgets/empty-states/root.tsx @@ -26,8 +26,6 @@ export const EmptyWorkspace = () => { icon: , cta: { text: "Create Project", - link: "#", - onClick: (e: React.MouseEvent) => { if (!canCreateProject) return; e.preventDefault(); @@ -44,7 +42,7 @@ export const EmptyWorkspace = () => { icon: , cta: { text: "Invite now", - link: "#", + link: "settings/members", }, }, { @@ -54,7 +52,7 @@ export const EmptyWorkspace = () => { icon: , cta: { text: "Configure workspace", - link: "#", + link: "settings", }, }, { @@ -81,7 +79,7 @@ export const EmptyWorkspace = () => { ), cta: { text: "Personalize account", - link: "#", + link: "/profile", }, }, ]; @@ -98,9 +96,21 @@ export const EmptyWorkspace = () => {

{item.title}

{item.description}

- + {item.cta.link ? ( + + {item.cta.text} + + ) : ( + + )}
))}
diff --git a/web/core/components/dashboard/widgets/recent-activity.tsx b/web/core/components/dashboard/widgets/recent-activity.tsx index 9f7f041bf88..ba4947f2b14 100644 --- a/web/core/components/dashboard/widgets/recent-activity.tsx +++ b/web/core/components/dashboard/widgets/recent-activity.tsx @@ -10,7 +10,7 @@ import { TRecentActivityWidgetResponse } from "@plane/types"; // components import { getButtonStyling, PriorityIcon } from "@plane/ui"; import { ListItem } from "@/components/core/list"; -import { RecentActivityEmptyState, WidgetLoader, WidgetProps } from "@/components/dashboard/widgets"; +import { WidgetLoader, WidgetProps } from "@/components/dashboard/widgets"; // helpers import { cn } from "@/helpers/common.helper"; // hooks @@ -88,16 +88,12 @@ export const RecentActivityWidget: React.FC = observer((props) => { View all
- {widgetStats.length > 0 ? ( + {widgetStats.length > 0 && (
{widgetStats.map((activity) => ( ))}
- ) : ( -
- -
)}
); diff --git a/web/core/components/dashboard/widgets/recent-pages.tsx b/web/core/components/dashboard/widgets/recent-pages.tsx index 270e42857ac..3d295975b2e 100644 --- a/web/core/components/dashboard/widgets/recent-pages.tsx +++ b/web/core/components/dashboard/widgets/recent-pages.tsx @@ -101,38 +101,6 @@ export const RecentPagesWidget: React.FC = observer((props) => { > Recent pages -
- {canCreateProject && ( - - )} - -
diff --git a/web/core/components/dashboard/widgets/recent-projects.tsx b/web/core/components/dashboard/widgets/recent-projects.tsx index 9d5c19b6ec3..aa146884bcf 100644 --- a/web/core/components/dashboard/widgets/recent-projects.tsx +++ b/web/core/components/dashboard/widgets/recent-projects.tsx @@ -3,11 +3,10 @@ import { useEffect } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; -import { Plus } from "lucide-react"; // plane types import { TRecentProjectsWidgetResponse } from "@plane/types"; // plane ui -import { Avatar, AvatarGroup, Card } from "@plane/ui"; +import { Avatar, AvatarGroup } from "@plane/ui"; // components import { Logo } from "@/components/common"; import { WidgetLoader, WidgetProps } from "@/components/dashboard/widgets"; @@ -118,20 +117,11 @@ export const RecentProjectsWidget: React.FC = observer((props) => {

)} - +
From c2412bb3ea4514530bf19fe532c4c6b031c8fd7f Mon Sep 17 00:00:00 2001 From: gakshita Date: Fri, 3 Jan 2025 14:39:07 +0530 Subject: [PATCH 03/18] fix: preserved old component --- packages/types/src/dashboard.d.ts | 4 +- web/core/components/core/list/list-item.tsx | 4 +- .../dashboard/home-dashboard-widgets.tsx | 66 ++-- .../dashboard/links/link-detail.tsx | 140 ------- web/core/components/dashboard/links/links.tsx | 37 -- .../dashboard/widgets/assigned-issues.tsx | 165 ++++++++ .../dashboard/widgets/created-issues.tsx | 162 ++++++++ .../widgets/empty-states/assigned-issues.tsx | 30 ++ .../widgets/empty-states/created-issues.tsx | 29 ++ .../dashboard/widgets/empty-states/index.ts | 8 +- .../empty-states/issues-by-priority.tsx | 25 ++ .../empty-states/issues-by-state-group.tsx | 25 ++ .../widgets/empty-states/project.tsx | 108 ------ .../widgets/empty-states/recent-activity.tsx | 25 ++ .../empty-states/recent-collaborators.tsx | 39 ++ .../components/dashboard/widgets/index.ts | 11 +- .../dashboard/widgets/issue-panels/index.ts | 3 + .../widgets/issue-panels/issue-list-item.tsx | 352 ++++++++++++++++++ .../widgets/issue-panels/issues-list.tsx | 134 +++++++ .../widgets/issue-panels/tabs-list.tsx | 61 +++ .../dashboard/widgets/issues-by-priority.tsx | 112 ++++++ .../widgets/issues-by-state-group.tsx | 221 +++++++++++ .../dashboard/widgets/overview-stats.tsx | 100 +++++ .../dashboard/widgets/recent-activity.tsx | 129 ++++--- .../collaborators-list.tsx | 154 ++++++++ .../widgets/recent-collaborators/index.ts | 1 + .../widgets/recent-collaborators/root.tsx | 36 ++ .../dashboard/widgets/recent-pages.tsx | 113 ------ .../dashboard/widgets/recent-projects.tsx | 68 ++-- .../dashboard/widgets/stickies/index.tsx | 83 ----- .../widgets/stickies/stickies-layout.tsx | 146 -------- .../home/home-dashboard-widgets.tsx | 66 ++++ web/core/components/home/index.ts | 3 + .../{dashboard => home}/links/action.tsx | 2 +- .../links/create-update-link-modal.tsx | 0 .../{dashboard => home}/links/index.ts | 0 .../components/home/links/link-detail.tsx | 126 +++++++ web/core/components/home/links/links.tsx | 77 ++++ .../{dashboard => home}/links/root.tsx | 22 +- .../{dashboard => home}/links/use-links.tsx | 18 +- .../components/home/project-empty-state.tsx | 46 +++ .../home/widgets/empty-states/index.ts | 1 + .../widgets/empty-states/root.tsx | 0 web/core/components/home/widgets/index.ts | 4 + .../home/widgets/loaders/assigned-issues.tsx | 24 ++ .../components/home/widgets/loaders/index.ts | 1 + .../widgets/loaders/issues-by-priority.tsx | 17 + .../widgets/loaders/issues-by-state-group.tsx | 24 ++ .../home/widgets/loaders/loader.tsx | 31 ++ .../home/widgets/loaders/overview-stats.tsx | 16 + .../home/widgets/loaders/recent-activity.tsx | 22 ++ .../widgets/loaders/recent-collaborators.tsx | 20 + .../home/widgets/loaders/recent-projects.tsx | 22 ++ .../components/home/widgets/manage/index.tsx | 36 ++ .../manage/widget-item-drag-handle.tsx | 26 ++ .../home/widgets/manage/widget-item.tsx | 128 +++++++ .../home/widgets/manage/widget-list.tsx | 49 +++ .../home/widgets/manage/widget.helpers.ts | 62 +++ .../home/widgets/recents/filters.tsx | 50 +++ .../components/home/widgets/recents/index.tsx | 59 +++ .../components/home/widgets/recents/issue.tsx | 68 ++++ .../page-views/workspace-dashboard.tsx | 7 +- web/core/components/user/user-greetings.tsx | 33 +- web/core/hooks/store/use-home.ts | 11 + web/core/services/workspace.service.ts | 21 +- web/core/store/dashboard.store.ts | 10 + web/core/store/workspace/home.ts | 72 ++++ web/core/store/workspace/index.ts | 8 +- web/core/store/workspace/link.store.ts | 34 +- 69 files changed, 2981 insertions(+), 826 deletions(-) delete mode 100644 web/core/components/dashboard/links/link-detail.tsx delete mode 100644 web/core/components/dashboard/links/links.tsx create mode 100644 web/core/components/dashboard/widgets/assigned-issues.tsx create mode 100644 web/core/components/dashboard/widgets/created-issues.tsx create mode 100644 web/core/components/dashboard/widgets/empty-states/assigned-issues.tsx create mode 100644 web/core/components/dashboard/widgets/empty-states/created-issues.tsx create mode 100644 web/core/components/dashboard/widgets/empty-states/issues-by-priority.tsx create mode 100644 web/core/components/dashboard/widgets/empty-states/issues-by-state-group.tsx delete mode 100644 web/core/components/dashboard/widgets/empty-states/project.tsx create mode 100644 web/core/components/dashboard/widgets/empty-states/recent-activity.tsx create mode 100644 web/core/components/dashboard/widgets/empty-states/recent-collaborators.tsx create mode 100644 web/core/components/dashboard/widgets/issue-panels/index.ts create mode 100644 web/core/components/dashboard/widgets/issue-panels/issue-list-item.tsx create mode 100644 web/core/components/dashboard/widgets/issue-panels/issues-list.tsx create mode 100644 web/core/components/dashboard/widgets/issue-panels/tabs-list.tsx create mode 100644 web/core/components/dashboard/widgets/issues-by-priority.tsx create mode 100644 web/core/components/dashboard/widgets/issues-by-state-group.tsx create mode 100644 web/core/components/dashboard/widgets/overview-stats.tsx create mode 100644 web/core/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx create mode 100644 web/core/components/dashboard/widgets/recent-collaborators/index.ts create mode 100644 web/core/components/dashboard/widgets/recent-collaborators/root.tsx delete mode 100644 web/core/components/dashboard/widgets/recent-pages.tsx delete mode 100644 web/core/components/dashboard/widgets/stickies/index.tsx delete mode 100644 web/core/components/dashboard/widgets/stickies/stickies-layout.tsx create mode 100644 web/core/components/home/home-dashboard-widgets.tsx create mode 100644 web/core/components/home/index.ts rename web/core/components/{dashboard => home}/links/action.tsx (95%) rename web/core/components/{dashboard => home}/links/create-update-link-modal.tsx (100%) rename web/core/components/{dashboard => home}/links/index.ts (100%) create mode 100644 web/core/components/home/links/link-detail.tsx create mode 100644 web/core/components/home/links/links.tsx rename web/core/components/{dashboard => home}/links/root.tsx (60%) rename web/core/components/{dashboard => home}/links/use-links.tsx (88%) create mode 100644 web/core/components/home/project-empty-state.tsx create mode 100644 web/core/components/home/widgets/empty-states/index.ts rename web/core/components/{dashboard => home}/widgets/empty-states/root.tsx (100%) create mode 100644 web/core/components/home/widgets/index.ts create mode 100644 web/core/components/home/widgets/loaders/assigned-issues.tsx create mode 100644 web/core/components/home/widgets/loaders/index.ts create mode 100644 web/core/components/home/widgets/loaders/issues-by-priority.tsx create mode 100644 web/core/components/home/widgets/loaders/issues-by-state-group.tsx create mode 100644 web/core/components/home/widgets/loaders/loader.tsx create mode 100644 web/core/components/home/widgets/loaders/overview-stats.tsx create mode 100644 web/core/components/home/widgets/loaders/recent-activity.tsx create mode 100644 web/core/components/home/widgets/loaders/recent-collaborators.tsx create mode 100644 web/core/components/home/widgets/loaders/recent-projects.tsx create mode 100644 web/core/components/home/widgets/manage/index.tsx create mode 100644 web/core/components/home/widgets/manage/widget-item-drag-handle.tsx create mode 100644 web/core/components/home/widgets/manage/widget-item.tsx create mode 100644 web/core/components/home/widgets/manage/widget-list.tsx create mode 100644 web/core/components/home/widgets/manage/widget.helpers.ts create mode 100644 web/core/components/home/widgets/recents/filters.tsx create mode 100644 web/core/components/home/widgets/recents/index.tsx create mode 100644 web/core/components/home/widgets/recents/issue.tsx create mode 100644 web/core/hooks/store/use-home.ts create mode 100644 web/core/store/workspace/home.ts diff --git a/packages/types/src/dashboard.d.ts b/packages/types/src/dashboard.d.ts index 2019b44392e..b42b5506fbd 100644 --- a/packages/types/src/dashboard.d.ts +++ b/packages/types/src/dashboard.d.ts @@ -10,20 +10,20 @@ export type WidgetProps = { }; export type TWidgetKeys = - | "quick_links" | "overview_stats" | "assigned_issues" | "created_issues" | "issues_by_state_groups" | "issues_by_priority" | "recent_activity" - | "recent_pages" | "recent_projects" | "recent_collaborators"; export type TIssuesListTypes = "pending" | "upcoming" | "overdue" | "completed"; // widget filters +export type TRecentActivityWidgetFilters = "all" | "projects" | "pages" | "issues"; + export type TAssignedIssuesWidgetFilters = { custom_dates?: string[]; duration?: EDurationFilters; diff --git a/web/core/components/core/list/list-item.tsx b/web/core/components/core/list/list-item.tsx index 930f630cc04..223879334b8 100644 --- a/web/core/components/core/list/list-item.tsx +++ b/web/core/components/core/list/list-item.tsx @@ -18,6 +18,7 @@ interface IListItemProps { parentRef: React.RefObject; disableLink?: boolean; className?: string; + itemClassName?: string; actionItemContainerClassName?: string; isSidebarOpen?: boolean; quickActionElement?: JSX.Element; @@ -38,6 +39,7 @@ export const ListItem: FC = (props) => { actionItemContainerClassName = "", isSidebarOpen = false, quickActionElement, + itemClassName = "", } = props; // router @@ -61,7 +63,7 @@ export const ListItem: FC = (props) => { className )} > -
+
; fullWidth: boolean }; } = { + overview_stats: { component: OverviewStatsWidget, fullWidth: true }, + assigned_issues: { component: AssignedIssuesWidget, fullWidth: false }, + created_issues: { component: CreatedIssuesWidget, fullWidth: false }, + issues_by_state_groups: { component: IssuesByStateGroupWidget, fullWidth: false }, + issues_by_priority: { component: IssuesByPriorityWidget, fullWidth: false }, recent_activity: { component: RecentActivityWidget, fullWidth: false }, - recent_pages: { component: RecentPagesWidget, fullWidth: false }, recent_projects: { component: RecentProjectsWidget, fullWidth: false }, - // recent_collaborators: { component: RecentCollaboratorsWidget, fullWidth: true }, - // my_stickies: { component: StickiesWidget, fullWidth: false }, + recent_collaborators: { component: RecentCollaboratorsWidget, fullWidth: true }, }; export const DashboardWidgets = observer(() => { // router const { workspaceSlug } = useParams(); - const { totalProjectIds } = useProject(); // store hooks const { homeDashboardId, homeDashboardWidgets } = useDashboard(); @@ -32,29 +42,21 @@ export const DashboardWidgets = observer(() => { if (!workspaceSlug || !homeDashboardId) return null; return ( -
- - - - {totalProjectIds?.length === 0 ? ( - - ) : ( - Object.entries(WIDGETS_LIST).map(([key, widget]) => { - const WidgetComponent = widget.component; - // if the widget doesn't exist, return null - // if (!doesWidgetExist(key as TWidgetKeys)) return null; - // if the widget is full width, return it in a 2 column grid - console.log({ widget, key }); - if (widget.fullWidth) - return ( -
- -
- ); - else - return ; - }) - )} +
+ {Object.entries(WIDGETS_LIST).map(([key, widget]) => { + const WidgetComponent = widget.component; + // if the widget doesn't exist, return null + if (!doesWidgetExist(key as TWidgetKeys)) return null; + // if the widget is full width, return it in a 2 column grid + if (widget.fullWidth) + return ( +
+ +
+ ); + else + return ; + })}
); }); diff --git a/web/core/components/dashboard/links/link-detail.tsx b/web/core/components/dashboard/links/link-detail.tsx deleted file mode 100644 index bbb0e07ddc4..00000000000 --- a/web/core/components/dashboard/links/link-detail.tsx +++ /dev/null @@ -1,140 +0,0 @@ -"use client"; - -import { FC } from "react"; -// hooks -// ui -import { observer } from "mobx-react"; -import { Pencil, Trash2, ExternalLink, Paperclip, EllipsisVertical, Link } from "lucide-react"; -import { TOAST_TYPE, setToast, CustomMenu, TContextMenuItem } from "@plane/ui"; -// helpers -import { cn } from "@plane/utils"; -import { copyUrlToClipboard } from "@/helpers/string.helper"; -import { useWorkspace } from "@/hooks/store"; -import { TLinkOperations } from "./use-links"; - -export type TProjectLinkDetail = { - linkId: string; - linkOperations: TLinkOperations; - isNotAllowed: boolean; -}; - -export const ProjectLinkDetail: FC = observer((props) => { - // props - const { linkId, linkOperations, isNotAllowed } = props; - // hooks - const { - links: { getLinkById, toggleLinkModal, setLinkData }, - } = useWorkspace(); - - const linkDetail = getLinkById(linkId); - if (!linkDetail) return <>; - - const viewLink = linkDetail.url; - - const handleEdit = (modalToggle: boolean) => { - toggleLinkModal(modalToggle); - setLinkData(linkDetail); - }; - - const handleCopyText = () => - copyUrlToClipboard(viewLink).then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Link Copied!", - message: "View link copied to clipboard.", - }); - }); - const handleOpenInNewTab = () => window.open(`/${viewLink}`, "_blank"); - - const MENU_ITEMS: TContextMenuItem[] = [ - { - key: "edit", - action: () => handleEdit(true), - title: "Edit", - icon: Pencil, - shouldRender: isNotAllowed, - }, - { - key: "open-new-tab", - action: handleOpenInNewTab, - title: "Open in new tab", - icon: ExternalLink, - }, - { - key: "copy-link", - action: handleCopyText, - title: "Copy link", - icon: Link, - }, - { - key: "delete", - action: () => linkOperations.remove(), - title: "Delete", - icon: Trash2, - shouldRender: isNotAllowed, - }, - ]; - return ( -
-
- -
-
-
Attachment
-
5 mins ago
-
- - } - placement="bottom-end" - menuItemsClassName="z-20" - closeOnSelect - className=" my-auto" - > - { - e.preventDefault(); - e.stopPropagation(); - }} - className={cn("flex items-center gap-2")} - > - {MENU_ITEMS.map((item) => { - if (item.shouldRender === false) return null; - return ( - { - e.preventDefault(); - e.stopPropagation(); - item.action(); - }} - className={cn( - "flex items-center gap-2", - { - "text-custom-text-400": item.disabled, - }, - item.className - )} - disabled={item.disabled} - > - {item.icon && } -
-
{item.title}
- {item.description && ( -

- {item.description} -

- )} -
-
- ); - })} -
-
-
- ); -}); diff --git a/web/core/components/dashboard/links/links.tsx b/web/core/components/dashboard/links/links.tsx deleted file mode 100644 index aa5f417dc89..00000000000 --- a/web/core/components/dashboard/links/links.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react"; -// computed -import { useWorkspace } from "@/hooks/store"; -import { ProjectLinkDetail } from "./link-detail"; -import { TLinkOperations } from "./use-links"; - -export type TLinkOperationsModal = Exclude; - -export type TProjectLinkList = { - linkOperations: TLinkOperationsModal; - disabled?: boolean; - workspaceSlug: string; -}; - -export const ProjectLinkList: FC = observer((props) => { - // props - const { linkOperations, workspaceSlug, disabled = false } = props; - // hooks - const { - links: { getLinksByWorkspaceId }, - } = useWorkspace(); - - const projectLinks = getLinksByWorkspaceId(workspaceSlug); - - if (!projectLinks) return <>; - - return ( -
- {projectLinks && - projectLinks.length > 0 && - projectLinks.map((linkId) => ( - - ))} -
- ); -}); diff --git a/web/core/components/dashboard/widgets/assigned-issues.tsx b/web/core/components/dashboard/widgets/assigned-issues.tsx new file mode 100644 index 00000000000..30bfbdad1b4 --- /dev/null +++ b/web/core/components/dashboard/widgets/assigned-issues.tsx @@ -0,0 +1,165 @@ +import { useEffect, useState } from "react"; +import { observer } from "mobx-react"; +import Link from "next/link"; +import { Tab } from "@headlessui/react"; +import { TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types"; +// hooks +import { Card } from "@plane/ui"; +import { + DurationFilterDropdown, + IssuesErrorState, + TabsList, + WidgetIssuesList, + WidgetLoader, + WidgetProps, +} from "@/components/dashboard/widgets"; +import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "@/constants/dashboard"; +import { getCustomDates, getRedirectionFilters, getTabKey } from "@/helpers/dashboard.helper"; +import { useDashboard } from "@/hooks/store"; +// components +// helpers +// types +// constants + +const WIDGET_KEY = "assigned_issues"; + +export const AssignedIssuesWidget: React.FC = observer((props) => { + const { dashboardId, workspaceSlug } = props; + // states + const [fetching, setFetching] = useState(false); + // store hooks + const { fetchWidgetStats, getWidgetDetails, getWidgetStats, getWidgetStatsError, updateDashboardWidgetFilters } = + useDashboard(); + // derived values + const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); + const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); + const widgetStatsError = getWidgetStatsError(workspaceSlug, dashboardId, WIDGET_KEY); + const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; + const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab); + const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; + + const handleUpdateFilters = async (filters: Partial) => { + if (!widgetDetails) return; + + setFetching(true); + + await updateDashboardWidgetFilters(workspaceSlug, dashboardId, widgetDetails.id, { + widgetKey: WIDGET_KEY, + filters, + }); + + const filterDates = getCustomDates( + filters.duration ?? selectedDurationFilter, + filters.custom_dates ?? selectedCustomDates + ); + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + issue_type: filters.tab ?? selectedTab, + ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), + expand: "issue_relation", + }).finally(() => setFetching(false)); + }; + + useEffect(() => { + const filterDates = getCustomDates(selectedDurationFilter, selectedCustomDates); + + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + issue_type: selectedTab, + ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), + expand: "issue_relation", + }); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const filterParams = getRedirectionFilters(selectedTab); + const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST; + const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab); + + if ((!widgetDetails || !widgetStats) && !widgetStatsError) return ; + + return ( + + {widgetStatsError ? ( + + handleUpdateFilters({ + duration: EDurationFilters.NONE, + tab: "pending", + }) + } + /> + ) : ( + widgetStats && ( + <> +
+ + Assigned to you + + { + if (val === "custom" && customDates) { + handleUpdateFilters({ + duration: val, + custom_dates: customDates, + }); + return; + } + + if (val === selectedDurationFilter) return; + + let newTab = selectedTab; + // switch to pending tab if target date is changed to none + if (val === "none" && selectedTab !== "completed") newTab = "pending"; + // switch to upcoming tab if target date is changed to other than none + if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") + newTab = "upcoming"; + + handleUpdateFilters({ + duration: val, + tab: newTab, + }); + }} + /> +
+ { + const newSelectedTab = tabsList[i]; + handleUpdateFilters({ tab: newSelectedTab?.key ?? "completed" }); + }} + className="h-full flex flex-col" + > + + + {tabsList.map((tab) => { + if (tab.key !== selectedTab) return null; + + return ( + + + + ); + })} + + + + ) + )} +
+ ); +}); diff --git a/web/core/components/dashboard/widgets/created-issues.tsx b/web/core/components/dashboard/widgets/created-issues.tsx new file mode 100644 index 00000000000..083318d3103 --- /dev/null +++ b/web/core/components/dashboard/widgets/created-issues.tsx @@ -0,0 +1,162 @@ +import { useEffect, useState } from "react"; +import { observer } from "mobx-react"; +import Link from "next/link"; +import { Tab } from "@headlessui/react"; +import { TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types"; +// hooks +import { Card } from "@plane/ui"; +import { + DurationFilterDropdown, + IssuesErrorState, + TabsList, + WidgetIssuesList, + WidgetLoader, + WidgetProps, +} from "@/components/dashboard/widgets"; +import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "@/constants/dashboard"; +import { getCustomDates, getRedirectionFilters, getTabKey } from "@/helpers/dashboard.helper"; +import { useDashboard } from "@/hooks/store"; +// components +// helpers +// types +// constants + +const WIDGET_KEY = "created_issues"; + +export const CreatedIssuesWidget: React.FC = observer((props) => { + const { dashboardId, workspaceSlug } = props; + // states + const [fetching, setFetching] = useState(false); + // store hooks + const { fetchWidgetStats, getWidgetDetails, getWidgetStats, getWidgetStatsError, updateDashboardWidgetFilters } = + useDashboard(); + // derived values + const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); + const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); + const widgetStatsError = getWidgetStatsError(workspaceSlug, dashboardId, WIDGET_KEY); + const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; + const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab); + const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; + + const handleUpdateFilters = async (filters: Partial) => { + if (!widgetDetails) return; + + setFetching(true); + + await updateDashboardWidgetFilters(workspaceSlug, dashboardId, widgetDetails.id, { + widgetKey: WIDGET_KEY, + filters, + }); + + const filterDates = getCustomDates( + filters.duration ?? selectedDurationFilter, + filters.custom_dates ?? selectedCustomDates + ); + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + issue_type: filters.tab ?? selectedTab, + ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), + }).finally(() => setFetching(false)); + }; + + useEffect(() => { + const filterDates = getCustomDates(selectedDurationFilter, selectedCustomDates); + + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + issue_type: selectedTab, + ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const filterParams = getRedirectionFilters(selectedTab); + const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST; + const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab); + + if ((!widgetDetails || !widgetStats) && !widgetStatsError) return ; + + return ( + + {widgetStatsError ? ( + + handleUpdateFilters({ + duration: EDurationFilters.NONE, + tab: "pending", + }) + } + /> + ) : ( + widgetStats && ( + <> +
+ + Created by you + + { + if (val === "custom" && customDates) { + handleUpdateFilters({ + duration: val, + custom_dates: customDates, + }); + return; + } + + if (val === selectedDurationFilter) return; + + let newTab = selectedTab; + // switch to pending tab if target date is changed to none + if (val === "none" && selectedTab !== "completed") newTab = "pending"; + // switch to upcoming tab if target date is changed to other than none + if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") + newTab = "upcoming"; + + handleUpdateFilters({ + duration: val, + tab: newTab, + }); + }} + /> +
+ { + const newSelectedTab = tabsList[i]; + handleUpdateFilters({ tab: newSelectedTab.key ?? "completed" }); + }} + className="h-full flex flex-col" + > + + + {tabsList.map((tab) => { + if (tab.key !== selectedTab) return null; + + return ( + + + + ); + })} + + + + ) + )} +
+ ); +}); diff --git a/web/core/components/dashboard/widgets/empty-states/assigned-issues.tsx b/web/core/components/dashboard/widgets/empty-states/assigned-issues.tsx new file mode 100644 index 00000000000..c9a25cfd390 --- /dev/null +++ b/web/core/components/dashboard/widgets/empty-states/assigned-issues.tsx @@ -0,0 +1,30 @@ +import Image from "next/image"; +import { useTheme } from "next-themes"; +import { TIssuesListTypes } from "@plane/types"; +// types +import { ASSIGNED_ISSUES_EMPTY_STATES } from "@/constants/dashboard"; +// constants + +type Props = { + type: TIssuesListTypes; +}; + +export const AssignedIssuesEmptyState: React.FC = (props) => { + const { type } = props; + // next-themes + const { resolvedTheme } = useTheme(); + + const typeDetails = ASSIGNED_ISSUES_EMPTY_STATES[type]; + + const image = resolvedTheme === "dark" ? typeDetails.darkImage : typeDetails.lightImage; + + // TODO: update empty state logic to use a general component + return ( +
+
+ Assigned issues +
+

{typeDetails.title}

+
+ ); +}; diff --git a/web/core/components/dashboard/widgets/empty-states/created-issues.tsx b/web/core/components/dashboard/widgets/empty-states/created-issues.tsx new file mode 100644 index 00000000000..bc812de3549 --- /dev/null +++ b/web/core/components/dashboard/widgets/empty-states/created-issues.tsx @@ -0,0 +1,29 @@ +import Image from "next/image"; +import { useTheme } from "next-themes"; +import { TIssuesListTypes } from "@plane/types"; +// types +import { CREATED_ISSUES_EMPTY_STATES } from "@/constants/dashboard"; +// constants + +type Props = { + type: TIssuesListTypes; +}; + +export const CreatedIssuesEmptyState: React.FC = (props) => { + const { type } = props; + // next-themes + const { resolvedTheme } = useTheme(); + + const typeDetails = CREATED_ISSUES_EMPTY_STATES[type]; + + const image = resolvedTheme === "dark" ? typeDetails.darkImage : typeDetails.lightImage; + + return ( +
+
+ Assigned issues +
+

{typeDetails.title}

+
+ ); +}; diff --git a/web/core/components/dashboard/widgets/empty-states/index.ts b/web/core/components/dashboard/widgets/empty-states/index.ts index b861b3ee9bc..72ca1dbb2dc 100644 --- a/web/core/components/dashboard/widgets/empty-states/index.ts +++ b/web/core/components/dashboard/widgets/empty-states/index.ts @@ -1,2 +1,6 @@ -export * from "./project"; -export * from "./root"; +export * from "./assigned-issues"; +export * from "./created-issues"; +export * from "./issues-by-priority"; +export * from "./issues-by-state-group"; +export * from "./recent-activity"; +export * from "./recent-collaborators"; diff --git a/web/core/components/dashboard/widgets/empty-states/issues-by-priority.tsx b/web/core/components/dashboard/widgets/empty-states/issues-by-priority.tsx new file mode 100644 index 00000000000..262c68d699a --- /dev/null +++ b/web/core/components/dashboard/widgets/empty-states/issues-by-priority.tsx @@ -0,0 +1,25 @@ +import Image from "next/image"; +import { useTheme } from "next-themes"; +// assets +import DarkImage from "@/public/empty-state/dashboard/dark/issues-by-priority.svg"; +import LightImage from "@/public/empty-state/dashboard/light/issues-by-priority.svg"; + +export const IssuesByPriorityEmptyState = () => { + // next-themes + const { resolvedTheme } = useTheme(); + + const image = resolvedTheme === "dark" ? DarkImage : LightImage; + + return ( +
+
+ Issues by state group +
+

+ Issues assigned to you, broken down by +
+ priority will show up here. +

+
+ ); +}; diff --git a/web/core/components/dashboard/widgets/empty-states/issues-by-state-group.tsx b/web/core/components/dashboard/widgets/empty-states/issues-by-state-group.tsx new file mode 100644 index 00000000000..ad7ac82b82a --- /dev/null +++ b/web/core/components/dashboard/widgets/empty-states/issues-by-state-group.tsx @@ -0,0 +1,25 @@ +import Image from "next/image"; +import { useTheme } from "next-themes"; +// assets +import DarkImage from "@/public/empty-state/dashboard/dark/issues-by-state-group.svg"; +import LightImage from "@/public/empty-state/dashboard/light/issues-by-state-group.svg"; + +export const IssuesByStateGroupEmptyState = () => { + // next-themes + const { resolvedTheme } = useTheme(); + + const image = resolvedTheme === "dark" ? DarkImage : LightImage; + + return ( +
+
+ Issues by state group +
+

+ Issue assigned to you, broken down by state, +
+ will show up here. +

+
+ ); +}; diff --git a/web/core/components/dashboard/widgets/empty-states/project.tsx b/web/core/components/dashboard/widgets/empty-states/project.tsx deleted file mode 100644 index cd6e557de19..00000000000 --- a/web/core/components/dashboard/widgets/empty-states/project.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React from "react"; -import Link from "next/link"; -import { useParams } from "next/navigation"; -import { Briefcase, Hotel, Users } from "lucide-react"; -import { getFileURL } from "@/helpers/file.helper"; -import { useCommandPalette, useEventTracker, useUser, useUserPermissions } from "@/hooks/store"; -import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants"; - -export const NoProjects = () => { - const { workspaceSlug } = useParams(); - const { allowPermissions } = useUserPermissions(); - const { toggleCreateProjectModal } = useCommandPalette(); - const { setTrackElement } = useEventTracker(); - const { data: currentUser } = useUser(); - - const canCreateProject = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.WORKSPACE - ); - - const EMPTY_STATE_DATA = [ - { - id: "create-project", - title: "Create a project", - description: "Create your first project now to get started", - icon: , - cta: { - text: "Create Project", - link: "#", - - onClick: (e: React.MouseEvent) => { - if (!canCreateProject) return; - e.preventDefault(); - e.stopPropagation(); - setTrackElement("Sidebar"); - toggleCreateProjectModal(true); - }, - }, - }, - { - id: "invite-team", - title: "Invite your team", - description: "The sub text will be of two lines and that will be placed.", - icon: , - cta: { - text: "Invite now", - link: "#", - }, - }, - { - id: "configure-workspace", - title: "Configure your workspace", - description: "The sub text will be of two lines and that will be placed.", - icon: , - cta: { - text: "Configure workspace", - link: "#", - }, - }, - { - id: "personalize-account", - title: "Personalize your account", - description: "The sub text will be of two lines and that will be placed.", - icon: - currentUser?.avatar_url && currentUser?.avatar_url.trim() !== "" ? ( - - - {currentUser?.display_name - - - ) : ( - - - {(currentUser?.email ?? currentUser?.display_name ?? "?")[0]} - - - ), - cta: { - text: "Personalize account", - link: "#", - }, - }, - ]; - return ( -
- {EMPTY_STATE_DATA.map((item) => ( -
-
- {item.icon} -
-

{item.title}

-

{item.description}

- - -
- ))} -
- ); -}; diff --git a/web/core/components/dashboard/widgets/empty-states/recent-activity.tsx b/web/core/components/dashboard/widgets/empty-states/recent-activity.tsx new file mode 100644 index 00000000000..6daeae5a719 --- /dev/null +++ b/web/core/components/dashboard/widgets/empty-states/recent-activity.tsx @@ -0,0 +1,25 @@ +import Image from "next/image"; +import { useTheme } from "next-themes"; +// assets +import DarkImage from "@/public/empty-state/dashboard/dark/recent-activity.svg"; +import LightImage from "@/public/empty-state/dashboard/light/recent-activity.svg"; + +export const RecentActivityEmptyState = () => { + // next-themes + const { resolvedTheme } = useTheme(); + + const image = resolvedTheme === "dark" ? DarkImage : LightImage; + + return ( +
+
+ Issues by state group +
+

+ All your issue activities across +
+ projects will show up here. +

+
+ ); +}; diff --git a/web/core/components/dashboard/widgets/empty-states/recent-collaborators.tsx b/web/core/components/dashboard/widgets/empty-states/recent-collaborators.tsx new file mode 100644 index 00000000000..d1a1200aa59 --- /dev/null +++ b/web/core/components/dashboard/widgets/empty-states/recent-collaborators.tsx @@ -0,0 +1,39 @@ +import Image from "next/image"; +import { useTheme } from "next-themes"; +// assets +import DarkImage1 from "@/public/empty-state/dashboard/dark/recent-collaborators-1.svg"; +import DarkImage2 from "@/public/empty-state/dashboard/dark/recent-collaborators-2.svg"; +import DarkImage3 from "@/public/empty-state/dashboard/dark/recent-collaborators-3.svg"; +import LightImage1 from "@/public/empty-state/dashboard/light/recent-collaborators-1.svg"; +import LightImage2 from "@/public/empty-state/dashboard/light/recent-collaborators-2.svg"; +import LightImage3 from "@/public/empty-state/dashboard/light/recent-collaborators-3.svg"; + +export const RecentCollaboratorsEmptyState = () => { + // next-themes + const { resolvedTheme } = useTheme(); + + const image1 = resolvedTheme === "dark" ? DarkImage1 : LightImage1; + const image2 = resolvedTheme === "dark" ? DarkImage2 : LightImage2; + const image3 = resolvedTheme === "dark" ? DarkImage3 : LightImage3; + + return ( +
+

+ Compare your activities with the top +
+ seven in your project. +

+
+
+ Recent collaborators +
+
+ Recent collaborators +
+
+ Recent collaborators +
+
+
+ ); +}; diff --git a/web/core/components/dashboard/widgets/index.ts b/web/core/components/dashboard/widgets/index.ts index f74e0d02fb8..31fc645d410 100644 --- a/web/core/components/dashboard/widgets/index.ts +++ b/web/core/components/dashboard/widgets/index.ts @@ -1,10 +1,13 @@ export * from "./dropdowns"; export * from "./empty-states"; export * from "./error-states"; +export * from "./issue-panels"; export * from "./loaders"; +export * from "./assigned-issues"; +export * from "./created-issues"; +export * from "./issues-by-priority"; +export * from "./issues-by-state-group"; +export * from "./overview-stats"; export * from "./recent-activity"; +export * from "./recent-collaborators"; export * from "./recent-projects"; -export * from "./recent-activity"; -export * from "./recent-pages"; -export * from "./stickies"; -export * from "./empty-states"; diff --git a/web/core/components/dashboard/widgets/issue-panels/index.ts b/web/core/components/dashboard/widgets/issue-panels/index.ts new file mode 100644 index 00000000000..f5b7d53d49e --- /dev/null +++ b/web/core/components/dashboard/widgets/issue-panels/index.ts @@ -0,0 +1,3 @@ +export * from "./issue-list-item"; +export * from "./issues-list"; +export * from "./tabs-list"; diff --git a/web/core/components/dashboard/widgets/issue-panels/issue-list-item.tsx b/web/core/components/dashboard/widgets/issue-panels/issue-list-item.tsx new file mode 100644 index 00000000000..3ca83f56544 --- /dev/null +++ b/web/core/components/dashboard/widgets/issue-panels/issue-list-item.tsx @@ -0,0 +1,352 @@ +"use client"; + +import { isToday } from "date-fns/isToday"; +import { observer } from "mobx-react"; +// types +import { TIssue, TWidgetIssue } from "@plane/types"; +// ui +import { Avatar, AvatarGroup, ControlLink, PriorityIcon } from "@plane/ui"; +// helpers +import { findTotalDaysInRange, getDate, renderFormattedDate } from "@/helpers/date-time.helper"; +import { getFileURL } from "@/helpers/file.helper"; +// hooks +import { useIssueDetail, useMember, useProject } from "@/hooks/store"; +// plane web components +import { IssueIdentifier } from "@/plane-web/components/issues"; + +export type IssueListItemProps = { + issueId: string; + onClick: (issue: TIssue) => void; + workspaceSlug: string; +}; + +export const AssignedUpcomingIssueListItem: React.FC = observer((props) => { + const { issueId, onClick, workspaceSlug } = props; + // store hooks + const { getProjectById } = useProject(); + const { + issue: { getIssueById }, + } = useIssueDetail(); + // derived values + const issueDetails = getIssueById(issueId) as TWidgetIssue | undefined; + + if (!issueDetails || !issueDetails.project_id) return null; + + const projectDetails = getProjectById(issueDetails.project_id); + + const blockedByIssues = issueDetails.issue_relation?.filter((issue) => issue.relation_type === "blocked_by") ?? []; + + const blockedByIssueProjectDetails = + blockedByIssues.length === 1 ? getProjectById(blockedByIssues[0]?.project_id ?? "") : null; + + const targetDate = getDate(issueDetails.target_date); + + return ( + onClick(issueDetails)} + className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" + > +
+ {projectDetails && ( + + )} +
{issueDetails.name}
+
+
+ +
+
+ {targetDate ? (isToday(targetDate) ? "Today" : renderFormattedDate(targetDate)) : "-"} +
+
+ {blockedByIssues.length > 0 + ? blockedByIssues.length > 1 + ? `${blockedByIssues.length} blockers` + : blockedByIssueProjectDetails && ( + + ) + : "-"} +
+
+ ); +}); + +export const AssignedOverdueIssueListItem: React.FC = observer((props) => { + const { issueId, onClick, workspaceSlug } = props; + // store hooks + const { getProjectById } = useProject(); + const { + issue: { getIssueById }, + } = useIssueDetail(); + // derived values + const issueDetails = getIssueById(issueId) as TWidgetIssue | undefined; + + if (!issueDetails || !issueDetails.project_id) return null; + + const projectDetails = getProjectById(issueDetails.project_id); + const blockedByIssues = issueDetails.issue_relation?.filter((issue) => issue.relation_type === "blocked_by") ?? []; + + const blockedByIssueProjectDetails = + blockedByIssues.length === 1 ? getProjectById(blockedByIssues[0]?.project_id ?? "") : null; + + const dueBy = findTotalDaysInRange(getDate(issueDetails.target_date), new Date(), false) ?? 0; + + return ( + onClick(issueDetails)} + className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" + > +
+ {projectDetails && ( + + )} +
{issueDetails.name}
+
+
+ +
+
+ {dueBy} {`day${dueBy > 1 ? "s" : ""}`} +
+
+ {blockedByIssues.length > 0 + ? blockedByIssues.length > 1 + ? `${blockedByIssues.length} blockers` + : blockedByIssueProjectDetails && ( + + ) + : "-"} +
+
+ ); +}); + +export const AssignedCompletedIssueListItem: React.FC = observer((props) => { + const { issueId, onClick, workspaceSlug } = props; + // store hooks + const { + issue: { getIssueById }, + } = useIssueDetail(); + const { getProjectById } = useProject(); + // derived values + const issueDetails = getIssueById(issueId); + + if (!issueDetails || !issueDetails.project_id) return null; + + const projectDetails = getProjectById(issueDetails.project_id); + + return ( + onClick(issueDetails)} + className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" + > +
+ {projectDetails && ( + + )} +
{issueDetails.name}
+
+
+ +
+
+ ); +}); + +export const CreatedUpcomingIssueListItem: React.FC = observer((props) => { + const { issueId, onClick, workspaceSlug } = props; + // store hooks + const { getUserDetails } = useMember(); + const { + issue: { getIssueById }, + } = useIssueDetail(); + const { getProjectById } = useProject(); + // derived values + const issue = getIssueById(issueId); + + if (!issue || !issue.project_id) return null; + + const projectDetails = getProjectById(issue.project_id); + const targetDate = getDate(issue.target_date); + + return ( + onClick(issue)} + className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" + > +
+ {projectDetails && ( + + )} +
{issue.name}
+
+
+ +
+
+ {targetDate ? (isToday(targetDate) ? "Today" : renderFormattedDate(targetDate)) : "-"} +
+
+ {issue.assignee_ids && issue.assignee_ids?.length > 0 ? ( + + {issue.assignee_ids?.map((assigneeId) => { + const userDetails = getUserDetails(assigneeId); + + if (!userDetails) return null; + + return ( + + ); + })} + + ) : ( + "-" + )} +
+
+ ); +}); + +export const CreatedOverdueIssueListItem: React.FC = observer((props) => { + const { issueId, onClick, workspaceSlug } = props; + // store hooks + const { getUserDetails } = useMember(); + const { + issue: { getIssueById }, + } = useIssueDetail(); + const { getProjectById } = useProject(); + // derived values + const issue = getIssueById(issueId); + + if (!issue || !issue.project_id) return null; + + const projectDetails = getProjectById(issue.project_id); + + const dueBy: number = findTotalDaysInRange(getDate(issue.target_date), new Date(), false) ?? 0; + + return ( + onClick(issue)} + className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" + > +
+ {projectDetails && ( + + )} +
{issue.name}
+
+
+ +
+
+ {dueBy} {`day${dueBy > 1 ? "s" : ""}`} +
+
+ {issue.assignee_ids.length > 0 ? ( + + {issue.assignee_ids?.map((assigneeId) => { + const userDetails = getUserDetails(assigneeId); + + if (!userDetails) return null; + + return ( + + ); + })} + + ) : ( + "-" + )} +
+
+ ); +}); + +export const CreatedCompletedIssueListItem: React.FC = observer((props) => { + const { issueId, onClick, workspaceSlug } = props; + // store hooks + const { getUserDetails } = useMember(); + const { + issue: { getIssueById }, + } = useIssueDetail(); + const { getProjectById } = useProject(); + // derived values + const issue = getIssueById(issueId); + + if (!issue || !issue.project_id) return null; + + const projectDetails = getProjectById(issue.project_id); + + return ( + onClick(issue)} + className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" + > +
+ {projectDetails && ( + + )} +
{issue.name}
+
+
+ +
+
+ {issue.assignee_ids.length > 0 ? ( + + {issue.assignee_ids?.map((assigneeId) => { + const userDetails = getUserDetails(assigneeId); + + if (!userDetails) return null; + + return ( + + ); + })} + + ) : ( + "-" + )} +
+
+ ); +}); diff --git a/web/core/components/dashboard/widgets/issue-panels/issues-list.tsx b/web/core/components/dashboard/widgets/issue-panels/issues-list.tsx new file mode 100644 index 00000000000..2b31d8f27b3 --- /dev/null +++ b/web/core/components/dashboard/widgets/issue-panels/issues-list.tsx @@ -0,0 +1,134 @@ +"use client"; + +import Link from "next/link"; +import { TAssignedIssuesWidgetResponse, TCreatedIssuesWidgetResponse, TIssue, TIssuesListTypes } from "@plane/types"; +// hooks +// components +import { Loader, getButtonStyling } from "@plane/ui"; +import { + AssignedCompletedIssueListItem, + AssignedIssuesEmptyState, + AssignedOverdueIssueListItem, + AssignedUpcomingIssueListItem, + CreatedCompletedIssueListItem, + CreatedIssuesEmptyState, + CreatedOverdueIssueListItem, + CreatedUpcomingIssueListItem, + IssueListItemProps, +} from "@/components/dashboard/widgets"; +// ui +// helpers +import { cn } from "@/helpers/common.helper"; +import { getRedirectionFilters } from "@/helpers/dashboard.helper"; +// hooks +import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-redirection"; +import { usePlatformOS } from "@/hooks/use-platform-os"; + +export type WidgetIssuesListProps = { + isLoading: boolean; + tab: TIssuesListTypes; + type: "assigned" | "created"; + widgetStats: TAssignedIssuesWidgetResponse | TCreatedIssuesWidgetResponse; + workspaceSlug: string; +}; + +export const WidgetIssuesList: React.FC = (props) => { + const { isLoading, tab, type, widgetStats, workspaceSlug } = props; + // hooks + const { isMobile } = usePlatformOS(); + const { handleRedirection } = useIssuePeekOverviewRedirection(); + + // handlers + const handleIssuePeekOverview = (issue: TIssue) => handleRedirection(workspaceSlug, issue, isMobile); + + const filterParams = getRedirectionFilters(tab); + + const ISSUE_LIST_ITEM: { + [key: string]: { + [key in TIssuesListTypes]: React.FC; + }; + } = { + assigned: { + pending: AssignedUpcomingIssueListItem, + upcoming: AssignedUpcomingIssueListItem, + overdue: AssignedOverdueIssueListItem, + completed: AssignedCompletedIssueListItem, + }, + created: { + pending: CreatedUpcomingIssueListItem, + upcoming: CreatedUpcomingIssueListItem, + overdue: CreatedOverdueIssueListItem, + completed: CreatedCompletedIssueListItem, + }, + }; + + const issuesList = widgetStats.issues; + + return ( + <> +
+ {isLoading ? ( + + + + + + + ) : issuesList.length > 0 ? ( + <> +
+
+ Issues + + {widgetStats.count} + +
+
Priority
+ {["upcoming", "pending"].includes(tab) &&
Due date
} + {tab === "overdue" &&
Due by
} + {type === "assigned" && tab !== "completed" &&
Blocked by
} + {type === "created" &&
Assigned to
} +
+
+ {issuesList.map((issue) => { + const IssueListItem = ISSUE_LIST_ITEM[type][tab]; + + if (!IssueListItem) return null; + + return ( + + ); + })} +
+ + ) : ( +
+ {type === "assigned" && } + {type === "created" && } +
+ )} +
+ {!isLoading && issuesList.length > 0 && ( + + View all issues + + )} + + ); +}; diff --git a/web/core/components/dashboard/widgets/issue-panels/tabs-list.tsx b/web/core/components/dashboard/widgets/issue-panels/tabs-list.tsx new file mode 100644 index 00000000000..d2b7df8ed0f --- /dev/null +++ b/web/core/components/dashboard/widgets/issue-panels/tabs-list.tsx @@ -0,0 +1,61 @@ +import { observer } from "mobx-react"; +import { Tab } from "@headlessui/react"; +import { TIssuesListTypes } from "@plane/types"; +// helpers +import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "@/constants/dashboard"; +import { cn } from "@/helpers/common.helper"; +// types +// constants + +type Props = { + durationFilter: EDurationFilters; + selectedTab: TIssuesListTypes; +}; + +export const TabsList: React.FC = observer((props) => { + const { durationFilter, selectedTab } = props; + + const tabsList = durationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST; + const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab); + + return ( + +
+ {tabsList.map((tab) => ( + + {tab.label} + + ))} + + ); +}); diff --git a/web/core/components/dashboard/widgets/issues-by-priority.tsx b/web/core/components/dashboard/widgets/issues-by-priority.tsx new file mode 100644 index 00000000000..9da47bc8481 --- /dev/null +++ b/web/core/components/dashboard/widgets/issues-by-priority.tsx @@ -0,0 +1,112 @@ +import { useEffect } from "react"; +import { observer } from "mobx-react"; +import Link from "next/link"; +// types +import { TIssuesByPriorityWidgetFilters, TIssuesByPriorityWidgetResponse } from "@plane/types"; +// components +import { Card } from "@plane/ui"; +import { + DurationFilterDropdown, + IssuesByPriorityEmptyState, + WidgetLoader, + WidgetProps, +} from "@/components/dashboard/widgets"; +import { IssuesByPriorityGraph } from "@/components/graphs"; +// constants +import { EDurationFilters } from "@/constants/dashboard"; +// helpers +import { getCustomDates } from "@/helpers/dashboard.helper"; +// hooks +import { useDashboard } from "@/hooks/store"; +import { useAppRouter } from "@/hooks/use-app-router"; + +const WIDGET_KEY = "issues_by_priority"; + +export const IssuesByPriorityWidget: React.FC = observer((props) => { + const { dashboardId, workspaceSlug } = props; + // router + const router = useAppRouter(); + // store hooks + const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard(); + // derived values + const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); + const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); + const selectedDuration = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; + const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; + + const handleUpdateFilters = async (filters: Partial) => { + if (!widgetDetails) return; + + await updateDashboardWidgetFilters(workspaceSlug, dashboardId, widgetDetails.id, { + widgetKey: WIDGET_KEY, + filters, + }); + + const filterDates = getCustomDates( + filters.duration ?? selectedDuration, + filters.custom_dates ?? selectedCustomDates + ); + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), + }); + }; + + useEffect(() => { + const filterDates = getCustomDates(selectedDuration, selectedCustomDates); + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (!widgetDetails || !widgetStats) return ; + + const totalCount = widgetStats.reduce((acc, item) => acc + item?.count, 0); + const chartData = widgetStats.map((item) => ({ + priority: item?.priority, + priority_count: item?.count, + })); + + return ( + +
+ + Assigned by priority + + + handleUpdateFilters({ + duration: val, + ...(val === "custom" ? { custom_dates: customDates } : {}), + }) + } + /> +
+ {totalCount > 0 ? ( +
+
+ { + router.push( + `/${workspaceSlug}/workspace-views/assigned?priority=${`${datum.data.priority}`.toLowerCase()}` + ); + }} + /> +
+
+ ) : ( +
+ +
+ )} +
+ ); +}); diff --git a/web/core/components/dashboard/widgets/issues-by-state-group.tsx b/web/core/components/dashboard/widgets/issues-by-state-group.tsx new file mode 100644 index 00000000000..53437ca1185 --- /dev/null +++ b/web/core/components/dashboard/widgets/issues-by-state-group.tsx @@ -0,0 +1,221 @@ +import { useEffect, useState } from "react"; +import { observer } from "mobx-react"; +import Link from "next/link"; +// types +import { TIssuesByStateGroupsWidgetFilters, TIssuesByStateGroupsWidgetResponse, TStateGroups } from "@plane/types"; +// components +import { Card } from "@plane/ui"; +import { + DurationFilterDropdown, + IssuesByStateGroupEmptyState, + WidgetLoader, + WidgetProps, +} from "@/components/dashboard/widgets"; +import { PieGraph } from "@/components/ui"; +// constants +import { EDurationFilters, STATE_GROUP_GRAPH_COLORS, STATE_GROUP_GRAPH_GRADIENTS } from "@/constants/dashboard"; +import { STATE_GROUPS } from "@/constants/state"; +// helpers +import { getCustomDates } from "@/helpers/dashboard.helper"; +// hooks +import { useDashboard } from "@/hooks/store"; +import { useAppRouter } from "@/hooks/use-app-router"; + +const WIDGET_KEY = "issues_by_state_groups"; + +export const IssuesByStateGroupWidget: React.FC = observer((props) => { + const { dashboardId, workspaceSlug } = props; + // states + const [defaultStateGroup, setDefaultStateGroup] = useState(null); + const [activeStateGroup, setActiveStateGroup] = useState(null); + // router + const router = useAppRouter(); + // store hooks + const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard(); + // derived values + const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); + const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); + const selectedDuration = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; + const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; + + const handleUpdateFilters = async (filters: Partial) => { + if (!widgetDetails) return; + + await updateDashboardWidgetFilters(workspaceSlug, dashboardId, widgetDetails.id, { + widgetKey: WIDGET_KEY, + filters, + }); + + const filterDates = getCustomDates( + filters.duration ?? selectedDuration, + filters.custom_dates ?? selectedCustomDates + ); + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), + }); + }; + + // fetch widget stats + useEffect(() => { + const filterDates = getCustomDates(selectedDuration, selectedCustomDates); + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // set active group for center metric + useEffect(() => { + if (!widgetStats) return; + + const startedCount = widgetStats?.find((item) => item?.state === "started")?.count ?? 0; + const unStartedCount = widgetStats?.find((item) => item?.state === "unstarted")?.count ?? 0; + const backlogCount = widgetStats?.find((item) => item?.state === "backlog")?.count ?? 0; + const completedCount = widgetStats?.find((item) => item?.state === "completed")?.count ?? 0; + const canceledCount = widgetStats?.find((item) => item?.state === "cancelled")?.count ?? 0; + + const stateGroup = + startedCount > 0 + ? "started" + : unStartedCount > 0 + ? "unstarted" + : backlogCount > 0 + ? "backlog" + : completedCount > 0 + ? "completed" + : canceledCount > 0 + ? "cancelled" + : null; + + setActiveStateGroup(stateGroup); + setDefaultStateGroup(stateGroup); + }, [widgetStats]); + + if (!widgetDetails || !widgetStats) return ; + + const totalCount = widgetStats?.reduce((acc, item) => acc + item?.count, 0); + const chartData = widgetStats?.map((item) => ({ + color: STATE_GROUP_GRAPH_COLORS[item?.state as keyof typeof STATE_GROUP_GRAPH_COLORS], + id: item?.state, + label: item?.state, + value: (item?.count / totalCount) * 100, + })); + + const CenteredMetric = ({ dataWithArc, centerX, centerY }: any) => { + const data = dataWithArc?.find((datum: any) => datum?.id === activeStateGroup); + const percentage = chartData?.find((item) => item.id === activeStateGroup)?.value?.toFixed(0); + + return ( + + + {percentage}% + + + {data?.id} + + + ); + }; + + return ( + +
+ + Assigned by state + + + handleUpdateFilters({ + duration: val, + ...(val === "custom" ? { custom_dates: customDates } : {}), + }) + } + /> +
+ {totalCount > 0 ? ( +
+
+
+ datum.data.color} + padAngle={1} + enableArcLinkLabels={false} + enableArcLabels={false} + activeOuterRadiusOffset={5} + tooltip={() => <>} + margin={{ + top: 0, + right: 5, + bottom: 0, + left: 5, + }} + defs={STATE_GROUP_GRAPH_GRADIENTS} + fill={Object.values(STATE_GROUPS).map((p) => ({ + match: { + id: p.key, + }, + id: `gradient${p.label}`, + }))} + onClick={(datum, e) => { + e.preventDefault(); + e.stopPropagation(); + router.push(`/${workspaceSlug}/workspace-views/assigned/?state_group=${datum.id}`); + }} + onMouseEnter={(datum) => setActiveStateGroup(datum.id as TStateGroups)} + onMouseLeave={() => setActiveStateGroup(defaultStateGroup)} + layers={["arcs", CenteredMetric]} + /> +
+
+ {chartData.map((item) => ( +
+
+
+ {item.label} +
+ {item.value.toFixed(0)}% +
+ ))} +
+
+
+ ) : ( +
+ +
+ )} + + ); +}); diff --git a/web/core/components/dashboard/widgets/overview-stats.tsx b/web/core/components/dashboard/widgets/overview-stats.tsx new file mode 100644 index 00000000000..62200102777 --- /dev/null +++ b/web/core/components/dashboard/widgets/overview-stats.tsx @@ -0,0 +1,100 @@ +import { useEffect } from "react"; +import { observer } from "mobx-react"; +import Link from "next/link"; +import { TOverviewStatsWidgetResponse } from "@plane/types"; +// hooks +import { Card, ECardSpacing } from "@plane/ui"; +import { WidgetLoader } from "@/components/dashboard/widgets"; +import { cn } from "@/helpers/common.helper"; +import { renderFormattedPayloadDate } from "@/helpers/date-time.helper"; +import { useDashboard } from "@/hooks/store"; +// components +// helpers +// types + +export type WidgetProps = { + dashboardId: string; + workspaceSlug: string; +}; + +const WIDGET_KEY = "overview_stats"; + +export const OverviewStatsWidget: React.FC = observer((props) => { + const { dashboardId, workspaceSlug } = props; + // store hooks + const { fetchWidgetStats, getWidgetStats } = useDashboard(); + // derived values + const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); + + const today = renderFormattedPayloadDate(new Date()); + const STATS_LIST = [ + { + key: "assigned", + title: "Issues assigned", + count: widgetStats?.assigned_issues_count, + link: `/${workspaceSlug}/workspace-views/assigned`, + }, + { + key: "overdue", + title: "Issues overdue", + count: widgetStats?.pending_issues_count, + link: `/${workspaceSlug}/workspace-views/assigned/?state_group=backlog,unstarted,started&target_date=${today};before`, + }, + { + key: "created", + title: "Issues created", + count: widgetStats?.created_issues_count, + link: `/${workspaceSlug}/workspace-views/created`, + }, + { + key: "completed", + title: "Issues completed", + count: widgetStats?.completed_issues_count, + link: `/${workspaceSlug}/workspace-views/assigned?state_group=completed`, + }, + ]; + + useEffect(() => { + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (!widgetStats) return ; + + return ( + + {STATS_LIST.map((stat, index) => ( +
+ +
+
+
{stat.count}
+

{stat.title}

+
+
+ +
+ ))} +
+ ); +}); diff --git a/web/core/components/dashboard/widgets/recent-activity.tsx b/web/core/components/dashboard/widgets/recent-activity.tsx index ba4947f2b14..dd21815ccce 100644 --- a/web/core/components/dashboard/widgets/recent-activity.tsx +++ b/web/core/components/dashboard/widgets/recent-activity.tsx @@ -1,64 +1,26 @@ "use client"; -import { useEffect, useRef } from "react"; -import { IssueIdentifier } from "ee/components/issues"; +import { useEffect } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; import { History } from "lucide-react"; // types import { TRecentActivityWidgetResponse } from "@plane/types"; // components -import { getButtonStyling, PriorityIcon } from "@plane/ui"; -import { ListItem } from "@/components/core/list"; -import { WidgetLoader, WidgetProps } from "@/components/dashboard/widgets"; +import { Card, Avatar, getButtonStyling } from "@plane/ui"; +import { ActivityIcon, ActivityMessage, IssueLink } from "@/components/core"; +import { RecentActivityEmptyState, WidgetLoader, WidgetProps } from "@/components/dashboard/widgets"; // helpers import { cn } from "@/helpers/common.helper"; +import { calculateTimeAgo } from "@/helpers/date-time.helper"; +import { getFileURL } from "@/helpers/file.helper"; // hooks import { useDashboard, useUser } from "@/hooks/store"; const WIDGET_KEY = "recent_activity"; -type BlockProps = { - activity: TRecentActivityWidgetResponse; - ref: React.RefObject; -}; -const Block = (props: BlockProps) => { - const { activity, ref } = props; - console.log({ ...activity.issue_detail }); - return ( - - -
- {activity.issue_detail?.name} -
-
- } - quickActionElement={ -
- -
- } - parentRef={ref} - disableLink={false} - className="bg-transparent my-auto !px-2 border-custom-border-200/40 py-3" - /> - ); -}; - export const RecentActivityWidget: React.FC = observer((props) => { const { dashboardId, workspaceSlug } = props; - const ref = useRef(null); // store hooks const { data: currentUser } = useUser(); // derived values @@ -76,25 +38,72 @@ export const RecentActivityWidget: React.FC = observer((props) => { if (!widgetStats) return ; return ( -
-
- - Recent issues{" "} - - - View all - -
- {widgetStats.length > 0 && ( -
+ + + Your issue activities + + {widgetStats.length > 0 ? ( +
{widgetStats.map((activity) => ( - +
+
+ {activity.field ? ( + activity.new_value === "restore" ? ( + + ) : ( +
+ +
+ ) + ) : activity.actor_detail.avatar_url && activity.actor_detail.avatar_url !== "" ? ( + + ) : ( +
+ {activity.actor_detail.is_bot + ? activity.actor_detail.first_name.charAt(0) + : activity.actor_detail.display_name.charAt(0)} +
+ )} +
+
+

+ + {currentUser?.id === activity.actor_detail.id ? "You" : activity.actor_detail?.display_name}{" "} + + {activity.field ? ( + + ) : ( + + created + + )} +

+

+ {calculateTimeAgo(activity.created_at)} +

+
+
))} + + View all + +
+ ) : ( +
+
)} -
+ ); }); diff --git a/web/core/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx b/web/core/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx new file mode 100644 index 00000000000..2dc1f0d3d99 --- /dev/null +++ b/web/core/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx @@ -0,0 +1,154 @@ +"use client"; +import { useState } from "react"; +import sortBy from "lodash/sortBy"; +import { observer } from "mobx-react"; +import Link from "next/link"; +import useSWR from "swr"; +// types +import { TRecentCollaboratorsWidgetResponse } from "@plane/types"; +// ui +import { Avatar } from "@plane/ui"; +// helpers +import { getFileURL } from "@/helpers/file.helper"; +// hooks +import { useDashboard, useMember, useUser } from "@/hooks/store"; +// components +import { WidgetLoader } from "../loaders"; + +type CollaboratorListItemProps = { + issueCount: number; + userId: string; + workspaceSlug: string; +}; + +const CollaboratorListItem: React.FC = observer((props) => { + const { issueCount, userId, workspaceSlug } = props; + // store hooks + const { data: currentUser } = useUser(); + const { getUserDetails } = useMember(); + // derived values + const userDetails = getUserDetails(userId); + const isCurrentUser = userId === currentUser?.id; + + if (!userDetails || userDetails.is_bot) return null; + + return ( + +
+ +
+
+ {isCurrentUser ? "You" : userDetails?.display_name} +
+

+ {issueCount} active issue{issueCount > 1 ? "s" : ""} +

+ + ); +}); + +type CollaboratorsListProps = { + dashboardId: string; + searchQuery?: string; + workspaceSlug: string; +}; + +const WIDGET_KEY = "recent_collaborators"; + +export const CollaboratorsList: React.FC = (props) => { + const { dashboardId, searchQuery = "", workspaceSlug } = props; + + // state + const [visibleItems, setVisibleItems] = useState(16); + const [isExpanded, setIsExpanded] = useState(false); + // store hooks + const { fetchWidgetStats } = useDashboard(); + const { getUserDetails } = useMember(); + const { data: currentUser } = useUser(); + + const { data: widgetStats } = useSWR( + workspaceSlug && dashboardId ? `WIDGET_STATS_${workspaceSlug}_${dashboardId}` : null, + workspaceSlug && dashboardId + ? () => + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + }) + : null + ) as { + data: TRecentCollaboratorsWidgetResponse[] | undefined; + }; + + if (!widgetStats) + return ( +
+ +
+ ); + + const sortedStats = sortBy(widgetStats, [(user) => user?.user_id !== currentUser?.id]); + + const filteredStats = sortedStats.filter((user) => { + if (!user) return false; + const userDetails = getUserDetails(user?.user_id); + if (!userDetails || userDetails.is_bot) return false; + const { display_name, first_name, last_name } = userDetails; + const searchLower = searchQuery.toLowerCase(); + return ( + display_name?.toLowerCase().includes(searchLower) || + first_name?.toLowerCase().includes(searchLower) || + last_name?.toLowerCase().includes(searchLower) + ); + }); + + // Update the displayedStats to always use the visibleItems limit + const handleLoadMore = () => { + setVisibleItems((prev) => { + const newValue = prev + 16; + if (newValue >= filteredStats.length) { + setIsExpanded(true); + return filteredStats.length; + } + return newValue; + }); + }; + + const handleHide = () => { + setVisibleItems(16); + setIsExpanded(false); + }; + + const displayedStats = filteredStats.slice(0, visibleItems); + + return ( + <> +
+ {displayedStats?.map((user) => ( + + ))} +
+ {filteredStats.length > visibleItems && !isExpanded && ( +
+
+ Load more +
+
+ )} + {isExpanded && ( +
+
Hide
+
+ )} + + ); +}; diff --git a/web/core/components/dashboard/widgets/recent-collaborators/index.ts b/web/core/components/dashboard/widgets/recent-collaborators/index.ts new file mode 100644 index 00000000000..1efe34c51ec --- /dev/null +++ b/web/core/components/dashboard/widgets/recent-collaborators/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/web/core/components/dashboard/widgets/recent-collaborators/root.tsx b/web/core/components/dashboard/widgets/recent-collaborators/root.tsx new file mode 100644 index 00000000000..4d30642071b --- /dev/null +++ b/web/core/components/dashboard/widgets/recent-collaborators/root.tsx @@ -0,0 +1,36 @@ +import { useState } from "react"; +import { Search } from "lucide-react"; +// types +import { Card } from "@plane/ui"; +import { WidgetProps } from "@/components/dashboard/widgets"; +// components +import { CollaboratorsList } from "./collaborators-list"; + +export const RecentCollaboratorsWidget: React.FC = (props) => { + const { dashboardId, workspaceSlug } = props; + // states + const [searchQuery, setSearchQuery] = useState(""); + + return ( + +
+
+

Collaborators

+

+ View and find all members you collaborate with across projects +

+
+
+ + setSearchQuery(e.target.value)} + /> +
+
+ +
+ ); +}; diff --git a/web/core/components/dashboard/widgets/recent-pages.tsx b/web/core/components/dashboard/widgets/recent-pages.tsx deleted file mode 100644 index 3d295975b2e..00000000000 --- a/web/core/components/dashboard/widgets/recent-pages.tsx +++ /dev/null @@ -1,113 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import { Plus } from "lucide-react"; -// plane types -import { TRecentProjectsWidgetResponse } from "@plane/types"; -// plane ui -import { Avatar, AvatarGroup, Card } from "@plane/ui"; -// components -import { Logo } from "@/components/common"; -import { WidgetLoader, WidgetProps } from "@/components/dashboard/widgets"; -// constants -import { PROJECT_BACKGROUND_COLORS } from "@/constants/dashboard"; -// helpers -import { getFileURL } from "@/helpers/file.helper"; -// hooks -import { useEventTracker, useDashboard, useProject, useCommandPalette, useUserPermissions } from "@/hooks/store"; -// plane web constants -import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; - -import Tiles from "./my-stickies"; - -const WIDGET_KEY = "recent_projects"; - -type PageListItemProps = { - pageId: string; - workspaceSlug: string; -}; - -const PageListItem: React.FC = observer((props) => { - const { pageId, workspaceSlug } = props; - // store hooks - const { getProjectById } = useProject(); - const pageDetails = getProjectById(pageId); - - const randomBgColor = PROJECT_BACKGROUND_COLORS[Math.floor(Math.random() * PROJECT_BACKGROUND_COLORS.length)]; - - if (!pageDetails) return null; - - return ( -
- {/* */} - -
Pulse
-
Plane Check point meetings - December 2025
-
- The below are the required design components. @sibira @shivangi @shrabani @bhavesh please take a look at them.{" "} -
-
-
- - {pageDetails.members?.map((member) => ( - - ))} - -
-
Last updated 2h ago
-
-
- ); -}); - -export const RecentPagesWidget: React.FC = observer((props) => { - const { dashboardId, workspaceSlug } = props; - // store hooks - const { toggleCreateProjectModal } = useCommandPalette(); - const { setTrackElement } = useEventTracker(); - const { allowPermissions } = useUserPermissions(); - const { fetchWidgetStats, getWidgetStats } = useDashboard(); - // derived values - const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const canCreateProject = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.WORKSPACE - ); - - useEffect(() => { - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (!widgetStats) return ; - - return ( -
-
- - Recent pages - -
- -
- {widgetStats.map((pageId) => ( - - ))} -
-
- ); -}); diff --git a/web/core/components/dashboard/widgets/recent-projects.tsx b/web/core/components/dashboard/widgets/recent-projects.tsx index aa146884bcf..5255908717e 100644 --- a/web/core/components/dashboard/widgets/recent-projects.tsx +++ b/web/core/components/dashboard/widgets/recent-projects.tsx @@ -3,10 +3,11 @@ import { useEffect } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; +import { Plus } from "lucide-react"; // plane types import { TRecentProjectsWidgetResponse } from "@plane/types"; // plane ui -import { Avatar, AvatarGroup } from "@plane/ui"; +import { Avatar, AvatarGroup, Card } from "@plane/ui"; // components import { Logo } from "@/components/common"; import { WidgetLoader, WidgetProps } from "@/components/dashboard/widgets"; @@ -37,10 +38,7 @@ const ProjectListItem: React.FC = observer((props) => { if (!projectDetails) return null; return ( - +
@@ -92,43 +90,37 @@ export const RecentProjectsWidget: React.FC = observer((props) => { if (!widgetStats) return ; return ( -
-
- - Recent projects - -
- {canCreateProject && ( - - )} - -

- View all{" "} -

- -
-
+ + + Recent projects +
+ {canCreateProject && ( + + )} {widgetStats.map((projectId) => ( ))}
-
+ ); }); diff --git a/web/core/components/dashboard/widgets/stickies/index.tsx b/web/core/components/dashboard/widgets/stickies/index.tsx deleted file mode 100644 index 9c91656c3ae..00000000000 --- a/web/core/components/dashboard/widgets/stickies/index.tsx +++ /dev/null @@ -1,83 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import { Plus } from "lucide-react"; -// plane types -import { TRecentProjectsWidgetResponse } from "@plane/types"; -// plane ui -import { Avatar, AvatarGroup, Card } from "@plane/ui"; -// components -import { Logo } from "@/components/common"; -import { WidgetLoader } from "@/components/dashboard/widgets"; -// constants -import { PROJECT_BACKGROUND_COLORS } from "@/constants/dashboard"; -// helpers -import { getFileURL } from "@/helpers/file.helper"; -// hooks -import { useEventTracker, useDashboard, useProject, useCommandPalette, useUserPermissions } from "@/hooks/store"; -// plane web constants -import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; -import { StickiesLayout } from "./stickies-layout"; - -const WIDGET_KEY = "recent_projects"; - -export const StickiesWidget: React.FC = observer((props) => { - const { dashboardId, workspaceSlug } = props; - // store hooks - const { toggleCreateProjectModal } = useCommandPalette(); - const { setTrackElement } = useEventTracker(); - const { allowPermissions } = useUserPermissions(); - const { fetchWidgetStats, getWidgetStats } = useDashboard(); - // derived values - const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const canCreateProject = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.WORKSPACE - ); - - useEffect(() => { - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (!widgetStats) return ; - - return ( -
-
- - My Stickies{" "} - -
- {canCreateProject && ( - - )} -
-
-
- - -
-
- ); -}); diff --git a/web/core/components/dashboard/widgets/stickies/stickies-layout.tsx b/web/core/components/dashboard/widgets/stickies/stickies-layout.tsx deleted file mode 100644 index fb80030784f..00000000000 --- a/web/core/components/dashboard/widgets/stickies/stickies-layout.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import React, { cloneElement, useState, useEffect } from "react"; - -interface TileContent { - title: string; - description: string; -} - -export const TILES: TileContent[] = [ - { - title: "Product Roadmap", - description: - "Q1 2024 product strategy and milestones. This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments.This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments.", - }, - { - title: "Design System", - description: "Component library and design tokens documentation with guidelines for implementation.", - }, - { - title: "User Research", - description: - "Customer interviews and feedback analysis for new features. Includes synthesis of user testing sessions and recommendations for product improvements.", - }, - { - title: "Sprint Planning", - description: "Next sprint goals and task allocation with detailed breakdown of upcoming work items.", - }, - { - title: "Team Updates", - description: "Weekly progress updates and current blockers affecting development timeline.", - }, - { - title: "Release Notes", - description: - "Latest features and bug fixes documentation with detailed changelog and migration guides for developers.", - }, - { - title: "Performance Metrics", - description: - "Key performance indicators and system metrics tracking. This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments.", - }, - { - title: "Documentation", - description: "Technical documentation and API references for the development team.", - }, - { - title: "Product Roadmap", - description: - "Q1 2024 product strategy and milestones. This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments.This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments.", - }, - { - title: "Design System", - description: "Component library and design tokens documentation with guidelines for implementation.", - }, - { - title: "User Research", - description: - "Customer interviews and feedback analysis for new features. Includes synthesis of user testing sessions and recommendations for product improvements.", - }, - { - title: "Sprint Planning", - description: "Next sprint goals and task allocation with detailed breakdown of upcoming work items.", - }, - { - title: "Team Updates", - description: "Weekly progress updates and current blockers affecting development timeline.", - }, - { - title: "Release Notes", - description: - "Latest features and bug fixes documentation with detailed changelog and migration guides for developers. Component library and design tokens documentation with guidelines for implementation. Component library and design tokens documentation with guidelines for implementation. Component library and design tokens documentation with guidelines for implementation.", - }, - { - title: "Performance Metrics", - description: - "Key performance indicators and system metrics tracking. This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments. This includes detailed planning for feature releases and team coordination across multiple departments.", - }, - { - title: "Documentation", - description: "Technical documentation and API references for the development team.", - }, -]; - -interface GridProps { - children: React.ReactNode; - columnCount: number; -} - -const Grid: React.FC = ({ children, columnCount }) => { - const cols = [...Array(columnCount).keys()]; - const rows = [...Array(Math.ceil(React.Children.count(children) / columnCount)).keys()]; - - return ( -
- {cols.map((col: number, index: number) => ( -
- {rows.map((row: number) => { - const child = React.Children.toArray(children)[row * columnCount + col]; - return child && cloneElement(child as React.ReactElement); - })} -
- ))} -
- ); -}; - -export const StickiesLayout = () => { - const [columnCount, setColumnCount] = useState(4); - - useEffect(() => { - const updateColumnCount = () => { - if (window.matchMedia("(min-width: 1024px)").matches) { - setColumnCount(4); // lg screens - } else if (window.matchMedia("(min-width: 768px)").matches) { - setColumnCount(3); // md screens - } else if (window.matchMedia("(min-width: 640px)").matches) { - setColumnCount(2); // sm screens - } else { - setColumnCount(1); // mobile - } - }; - - // Initial check - updateColumnCount(); - - // Add event listener for window resize - window.addEventListener("resize", updateColumnCount); - - // Cleanup - return () => window.removeEventListener("resize", updateColumnCount); - }, []); - - return ( -
- - {TILES.map((content: TileContent, index) => ( -
-

- {index} {content.title} -

-

{content.description}

-
- ))} -
-
- ); -}; diff --git a/web/core/components/home/home-dashboard-widgets.tsx b/web/core/components/home/home-dashboard-widgets.tsx new file mode 100644 index 00000000000..69863972b0a --- /dev/null +++ b/web/core/components/home/home-dashboard-widgets.tsx @@ -0,0 +1,66 @@ +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +// types +import { TWidgetKeys, WidgetProps } from "@plane/types"; +// components +import { RecentActivityWidget, EmptyWorkspace } from "@/components/dashboard"; +// hooks +import { useDashboard, useProject } from "@/hooks/store"; +import { useHome } from "@/hooks/store/use-home"; +import { HomePageHeader } from "@/plane-web/components/home/header"; +import { DashboardQuickLinks } from "./links"; +import { ManageWidgetsModal } from "./widgets/manage"; + +const WIDGETS_LIST: { + [key in TWidgetKeys]: { component: React.FC; fullWidth: boolean }; +} = { + recent_activity: { component: RecentActivityWidget, fullWidth: false }, + // recent_collaborators: { component: RecentCollaboratorsWidget, fullWidth: true }, + // my_stickies: { component: StickiesWidget, fullWidth: false }, +}; + +export const DashboardWidgets = observer(() => { + // router + const { workspaceSlug } = useParams(); + const { totalProjectIds } = useProject(); + // store hooks + const { toggleWidgetSettings, showWidgetSettings } = useHome(); + const { homeDashboardId, homeDashboardWidgets } = useDashboard(); + + const doesWidgetExist = (widgetKey: TWidgetKeys) => + Boolean(homeDashboardWidgets?.find((widget) => widget.key === widgetKey)); + + if (!workspaceSlug || !homeDashboardId) return null; + + return ( +
+ + toggleWidgetSettings(false)} + /> + + + {totalProjectIds?.length === 0 ? ( + + ) : ( + Object.entries(WIDGETS_LIST).map(([key, widget]) => { + const WidgetComponent = widget.component; + // if the widget doesn't exist, return null + // if (!doesWidgetExist(key as TWidgetKeys)) return null; + // if the widget is full width, return it in a 2 column grid + console.log({ widget, key }); + if (widget.fullWidth) + return ( +
+ +
+ ); + else + return ; + }) + )} +
+ ); +}); diff --git a/web/core/components/home/index.ts b/web/core/components/home/index.ts new file mode 100644 index 00000000000..129cdb69ea3 --- /dev/null +++ b/web/core/components/home/index.ts @@ -0,0 +1,3 @@ +export * from "./widgets"; +export * from "./home-dashboard-widgets"; +export * from "./project-empty-state"; diff --git a/web/core/components/dashboard/links/action.tsx b/web/core/components/home/links/action.tsx similarity index 95% rename from web/core/components/dashboard/links/action.tsx rename to web/core/components/home/links/action.tsx index e572a414147..7d23f43ffcf 100644 --- a/web/core/components/dashboard/links/action.tsx +++ b/web/core/components/home/links/action.tsx @@ -5,7 +5,7 @@ type TProps = { }; export const AddLink = (props: TProps) => { const { onClick } = props; - console.log("AddLink"); + return ( + )} +
+ ); +}); diff --git a/web/core/components/dashboard/links/root.tsx b/web/core/components/home/links/root.tsx similarity index 60% rename from web/core/components/dashboard/links/root.tsx rename to web/core/components/home/links/root.tsx index 7d274bca3c9..7a30c0dbe40 100644 --- a/web/core/components/dashboard/links/root.tsx +++ b/web/core/components/home/links/root.tsx @@ -1,6 +1,6 @@ import { observer } from "mobx-react"; -import { useWorkspace } from "@/hooks/store"; -import { AddLink } from "./action"; +import useSWR from "swr"; +import { useHome } from "@/hooks/store/use-home"; import { LinkCreateUpdateModal } from "./create-update-link-modal"; import { ProjectLinkList } from "./links"; import { useLinks } from "./use-links"; @@ -12,9 +12,18 @@ export const DashboardQuickLinks = observer((props: TProps) => { const { workspaceSlug } = props; const { linkOperations } = useLinks(workspaceSlug); const { - links: { isLinkModalOpen, toggleLinkModal, linkData, setLinkData }, - } = useWorkspace(); + quickLinks: { isLinkModalOpen, toggleLinkModal, linkData, setLinkData, fetchLinks }, + } = useHome(); + useSWR( + workspaceSlug ? `HOME_LINKS_${workspaceSlug}` : null, + workspaceSlug ? () => fetchLinks(workspaceSlug.toString()) : null, + { + revalidateIfStale: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + } + ); return ( <> { preloadedData={linkData} setLinkData={setLinkData} /> -
+
{/* rendering links */} - - {/* Add new link */} - toggleLinkModal(true)} />
); diff --git a/web/core/components/dashboard/links/use-links.tsx b/web/core/components/home/links/use-links.tsx similarity index 88% rename from web/core/components/dashboard/links/use-links.tsx rename to web/core/components/home/links/use-links.tsx index d776f6bef02..fe107fd06a9 100644 --- a/web/core/components/dashboard/links/use-links.tsx +++ b/web/core/components/home/links/use-links.tsx @@ -1,7 +1,7 @@ import { useMemo } from "react"; import { TProjectLink } from "@plane/types"; import { setToast, TOAST_TYPE } from "@plane/ui"; -import { useWorkspace } from "@/hooks/store"; +import { useHome } from "@/hooks/store/use-home"; export type TLinkOperations = { create: (data: Partial) => Promise; @@ -15,14 +15,24 @@ export type TProjectLinkRoot = { export const useLinks = (workspaceSlug: string) => { // hooks const { - links: { createLink, updateLink, removeLink, isLinkModalOpen, toggleLinkModal, linkData, setLinkData, fetchLinks }, - } = useWorkspace(); + quickLinks: { + createLink, + updateLink, + removeLink, + isLinkModalOpen, + toggleLinkModal, + linkData, + setLinkData, + fetchLinks, + }, + } = useHome(); const linkOperations: TLinkOperations = useMemo( () => ({ create: async (data: Partial) => { try { if (!workspaceSlug) throw new Error("Missing required fields"); + console.log("data", data, workspaceSlug); await createLink(workspaceSlug, data); setToast({ message: "The link has been successfully created", @@ -31,6 +41,7 @@ export const useLinks = (workspaceSlug: string) => { }); toggleLinkModal(false); } catch (error: any) { + console.error("error", error); setToast({ message: error?.data?.error ?? "The link could not be created", type: TOAST_TYPE.ERROR, @@ -67,7 +78,6 @@ export const useLinks = (workspaceSlug: string) => { type: TOAST_TYPE.SUCCESS, title: "Link removed", }); - toggleLinkModal(false); } catch (error) { setToast({ message: "The link could not be removed", diff --git a/web/core/components/home/project-empty-state.tsx b/web/core/components/home/project-empty-state.tsx new file mode 100644 index 00000000000..c8cba817a37 --- /dev/null +++ b/web/core/components/home/project-empty-state.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { observer } from "mobx-react"; +import Image from "next/image"; +// ui +import { Button } from "@plane/ui"; +// hooks +import { useCommandPalette, useEventTracker, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; +// assets +import ProjectEmptyStateImage from "@/public/empty-state/onboarding/dashboard-light.webp"; + +export const DashboardProjectEmptyState = observer(() => { + // store hooks + const { toggleCreateProjectModal } = useCommandPalette(); + const { setTrackElement } = useEventTracker(); + const { allowPermissions } = useUserPermissions(); + + // derived values + const canCreateProject = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); + + return ( +
+

Overview of your projects, activity, and metrics

+

+ Welcome to Plane, we are excited to have you here. Create your first project and track your issues, and this + page will transform into a space that helps you progress. Admins will also see items which help their team + progress. +

+ Project empty state + {canCreateProject && ( +
+ +
+ )} +
+ ); +}); diff --git a/web/core/components/home/widgets/empty-states/index.ts b/web/core/components/home/widgets/empty-states/index.ts new file mode 100644 index 00000000000..1efe34c51ec --- /dev/null +++ b/web/core/components/home/widgets/empty-states/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/web/core/components/dashboard/widgets/empty-states/root.tsx b/web/core/components/home/widgets/empty-states/root.tsx similarity index 100% rename from web/core/components/dashboard/widgets/empty-states/root.tsx rename to web/core/components/home/widgets/empty-states/root.tsx diff --git a/web/core/components/home/widgets/index.ts b/web/core/components/home/widgets/index.ts new file mode 100644 index 00000000000..19d9aba92f5 --- /dev/null +++ b/web/core/components/home/widgets/index.ts @@ -0,0 +1,4 @@ +export * from "./empty-states"; +export * from "./loaders"; +export * from "./recents"; +export * from "./empty-states"; diff --git a/web/core/components/home/widgets/loaders/assigned-issues.tsx b/web/core/components/home/widgets/loaders/assigned-issues.tsx new file mode 100644 index 00000000000..8a78fedf1a4 --- /dev/null +++ b/web/core/components/home/widgets/loaders/assigned-issues.tsx @@ -0,0 +1,24 @@ +"use client"; + +// ui +import { Loader } from "@plane/ui"; + +export const AssignedIssuesWidgetLoader = () => ( + +
+ + +
+
+ + +
+
+ + + + + +
+
+); diff --git a/web/core/components/home/widgets/loaders/index.ts b/web/core/components/home/widgets/loaders/index.ts new file mode 100644 index 00000000000..ee5286f0fbf --- /dev/null +++ b/web/core/components/home/widgets/loaders/index.ts @@ -0,0 +1 @@ +export * from "./loader"; diff --git a/web/core/components/home/widgets/loaders/issues-by-priority.tsx b/web/core/components/home/widgets/loaders/issues-by-priority.tsx new file mode 100644 index 00000000000..c6f075b5806 --- /dev/null +++ b/web/core/components/home/widgets/loaders/issues-by-priority.tsx @@ -0,0 +1,17 @@ +"use client"; + +// ui +import { Loader } from "@plane/ui"; + +export const IssuesByPriorityWidgetLoader = () => ( + + +
+ + + + + +
+
+); diff --git a/web/core/components/home/widgets/loaders/issues-by-state-group.tsx b/web/core/components/home/widgets/loaders/issues-by-state-group.tsx new file mode 100644 index 00000000000..099323cce75 --- /dev/null +++ b/web/core/components/home/widgets/loaders/issues-by-state-group.tsx @@ -0,0 +1,24 @@ +"use client"; + +import range from "lodash/range"; +// ui +import { Loader } from "@plane/ui"; + +export const IssuesByStateGroupWidgetLoader = () => ( + + +
+
+
+ +
+
+
+
+ {range(5).map((index) => ( + + ))} +
+
+ +); diff --git a/web/core/components/home/widgets/loaders/loader.tsx b/web/core/components/home/widgets/loaders/loader.tsx new file mode 100644 index 00000000000..ae4038b38da --- /dev/null +++ b/web/core/components/home/widgets/loaders/loader.tsx @@ -0,0 +1,31 @@ +// components +import { TWidgetKeys } from "@plane/types"; +import { AssignedIssuesWidgetLoader } from "./assigned-issues"; +import { IssuesByPriorityWidgetLoader } from "./issues-by-priority"; +import { IssuesByStateGroupWidgetLoader } from "./issues-by-state-group"; +import { OverviewStatsWidgetLoader } from "./overview-stats"; +import { RecentActivityWidgetLoader } from "./recent-activity"; +import { RecentCollaboratorsWidgetLoader } from "./recent-collaborators"; +import { RecentProjectsWidgetLoader } from "./recent-projects"; +// types + +type Props = { + widgetKey: TWidgetKeys; +}; + +export const WidgetLoader: React.FC = (props) => { + const { widgetKey } = props; + + const loaders = { + overview_stats: , + assigned_issues: , + created_issues: , + issues_by_state_groups: , + issues_by_priority: , + recent_activity: , + recent_projects: , + recent_collaborators: , + }; + + return loaders[widgetKey]; +}; diff --git a/web/core/components/home/widgets/loaders/overview-stats.tsx b/web/core/components/home/widgets/loaders/overview-stats.tsx new file mode 100644 index 00000000000..e780bb39966 --- /dev/null +++ b/web/core/components/home/widgets/loaders/overview-stats.tsx @@ -0,0 +1,16 @@ +"use client"; + +import range from "lodash/range"; +// ui +import { Loader } from "@plane/ui"; + +export const OverviewStatsWidgetLoader = () => ( + + {range(4).map((index) => ( +
+ + +
+ ))} +
+); diff --git a/web/core/components/home/widgets/loaders/recent-activity.tsx b/web/core/components/home/widgets/loaders/recent-activity.tsx new file mode 100644 index 00000000000..2df78a15af1 --- /dev/null +++ b/web/core/components/home/widgets/loaders/recent-activity.tsx @@ -0,0 +1,22 @@ +"use client"; + +import range from "lodash/range"; +// ui +import { Loader } from "@plane/ui"; + +export const RecentActivityWidgetLoader = () => ( + + + {range(7).map((index) => ( +
+
+ +
+
+ + +
+
+ ))} +
+); diff --git a/web/core/components/home/widgets/loaders/recent-collaborators.tsx b/web/core/components/home/widgets/loaders/recent-collaborators.tsx new file mode 100644 index 00000000000..2dceaf1320a --- /dev/null +++ b/web/core/components/home/widgets/loaders/recent-collaborators.tsx @@ -0,0 +1,20 @@ +"use client"; + +import range from "lodash/range"; +// ui +import { Loader } from "@plane/ui"; + +export const RecentCollaboratorsWidgetLoader = () => ( + <> + {range(8).map((index) => ( + +
+
+ +
+ +
+
+ ))} + +); diff --git a/web/core/components/home/widgets/loaders/recent-projects.tsx b/web/core/components/home/widgets/loaders/recent-projects.tsx new file mode 100644 index 00000000000..38bc7e29a01 --- /dev/null +++ b/web/core/components/home/widgets/loaders/recent-projects.tsx @@ -0,0 +1,22 @@ +"use client"; + +import range from "lodash/range"; +// ui +import { Loader } from "@plane/ui"; + +export const RecentProjectsWidgetLoader = () => ( + + + {range(5).map((index) => ( +
+
+ +
+
+ + +
+
+ ))} +
+); diff --git a/web/core/components/home/widgets/manage/index.tsx b/web/core/components/home/widgets/manage/index.tsx new file mode 100644 index 00000000000..9a2c60c440b --- /dev/null +++ b/web/core/components/home/widgets/manage/index.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react"; +// plane types +// plane ui +import { Button, ModalCore } from "@plane/ui"; +import { WidgetList } from "./widget-list"; + +export type TProps = { + workspaceSlug: string; + isModalOpen: boolean; + handleOnClose?: () => void; +}; + +export const ManageWidgetsModal: FC = observer((props) => { + // props + const { workspaceSlug, isModalOpen, handleOnClose } = props; + + return ( + +
+
Manage widgets
+ +
+ + +
+
+
+ ); +}); diff --git a/web/core/components/home/widgets/manage/widget-item-drag-handle.tsx b/web/core/components/home/widgets/manage/widget-item-drag-handle.tsx new file mode 100644 index 00000000000..2995ce08651 --- /dev/null +++ b/web/core/components/home/widgets/manage/widget-item-drag-handle.tsx @@ -0,0 +1,26 @@ +"use client"; +import React, { FC } from "react"; +import { observer } from "mobx-react"; +// ui +import { DragHandle } from "@plane/ui"; +// helper +import { cn } from "@/helpers/common.helper"; + +type Props = { + sort_order: number | null; + isDragging: boolean; +}; + +export const WidgetItemDragHandle: FC = observer((props) => { + const { isDragging } = props; + + return ( +
+ +
+ ); +}); diff --git a/web/core/components/home/widgets/manage/widget-item.tsx b/web/core/components/home/widgets/manage/widget-item.tsx new file mode 100644 index 00000000000..2e2dc380299 --- /dev/null +++ b/web/core/components/home/widgets/manage/widget-item.tsx @@ -0,0 +1,128 @@ +"use client"; + +import React, { FC, useEffect, useRef, useState } from "react"; +import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; +import { DropTargetRecord, DragLocationHistory } from "@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types"; +import { + draggable, + dropTargetForElements, + ElementDragPayload, +} from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview"; +import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview"; +import { attachInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item"; + +import { observer } from "mobx-react"; +// plane helpers +import { createRoot } from "react-dom/client"; +// ui +import { InstructionType } from "@plane/types"; +// components +import { DropIndicator, ToggleSwitch } from "@plane/ui"; +// helpers +import { cn } from "@plane/utils"; +import { WidgetItemDragHandle } from "./widget-item-drag-handle"; +import { getCanDrop, getInstructionFromPayload } from "./widget.helpers"; + +type Props = { + isLastChild: boolean; + widget: any; + handleDrop: (self: DropTargetRecord, source: ElementDragPayload, location: DragLocationHistory) => void; +}; + +export const WidgetItem: FC = observer((props) => { + // props + const { isLastChild, widget, handleDrop } = props; + //state + const [isDragging, setIsDragging] = useState(false); + const [instruction, setInstruction] = useState(undefined); + + //ref + const elementRef = useRef(null); + + // drag and drop + useEffect(() => { + const element = elementRef.current; + + if (!element) return; + const initialData = { id: widget.id, isGroup: false }; + return combine( + draggable({ + element, + dragHandle: elementRef.current, + getInitialData: () => initialData, + onDragStart: () => { + setIsDragging(true); + }, + onDrop: () => { + setIsDragging(false); + }, + onGenerateDragPreview: ({ nativeSetDragImage }) => { + setCustomNativeDragPreview({ + getOffset: pointerOutsideOfPreview({ x: "0px", y: "0px" }), + render: ({ container }) => { + const root = createRoot(container); + root.render(
{widget.title}
); + return () => root.unmount(); + }, + nativeSetDragImage, + }); + }, + }), + dropTargetForElements({ + element, + canDrop: ({ source }) => getCanDrop(source, widget), + onDragStart: () => { + setIsDragging(true); + }, + getData: ({ input, element }) => { + const blockedStates: InstructionType[] = ["make-child"]; + if (!isLastChild) { + blockedStates.push("reorder-below"); + } + + return attachInstruction(initialData, { + input, + element, + currentLevel: 1, + indentPerLevel: 0, + mode: isLastChild ? "last-in-group" : "standard", + block: blockedStates, + }); + }, + onDrag: ({ self, source, location }) => { + const instruction = getInstructionFromPayload(self, source, location); + setInstruction(instruction); + }, + onDragLeave: () => { + setInstruction(undefined); + }, + onDrop: ({ self, source, location }) => { + setInstruction(undefined); + handleDrop(self, source, location); + }, + }) + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [elementRef?.current, isDragging, isLastChild, widget.id]); + + return ( +
+ +
+ +
{widget.title}
+ +
+ {isLastChild && } +
+ ); +}); diff --git a/web/core/components/home/widgets/manage/widget-list.tsx b/web/core/components/home/widgets/manage/widget-list.tsx new file mode 100644 index 00000000000..4d93275c996 --- /dev/null +++ b/web/core/components/home/widgets/manage/widget-list.tsx @@ -0,0 +1,49 @@ +import { + DragLocationHistory, + DropTargetRecord, + ElementDragPayload, +} from "@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types"; +import { useDashboard } from "@/hooks/store"; +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 { reorderWidgets } = useDashboard(); + + const handleDrop = (self: DropTargetRecord, source: ElementDragPayload, location: DragLocationHistory) => { + const dropTargets = location?.current?.dropTargets ?? []; + if (!dropTargets || dropTargets.length <= 0) return; + const dropTarget = + dropTargets.length > 1 ? dropTargets.find((target: DropTargetRecord) => target?.data?.isChild) : dropTargets[0]; + + const dropTargetData = dropTarget?.data as TargetData; + + if (!dropTarget || !dropTargetData) return; + const instruction = getInstructionFromPayload(dropTarget, source, location); + const droppedId = dropTargetData.id; + const sourceData = source.data as TargetData; + + if (!sourceData.id) return; + if (droppedId) { + reorderWidgets(workspaceSlug, sourceData.id, droppedId, instruction); /** sequence */ + } + }; + + return ( +
+ {WIDGETS_LIST.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 new file mode 100644 index 00000000000..c629f1fbccd --- /dev/null +++ b/web/core/components/home/widgets/manage/widget.helpers.ts @@ -0,0 +1,62 @@ +import { extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item"; +import { IFavorite, InstructionType, IPragmaticPayloadLocation, TDropTarget } from "@plane/types"; + +export type TargetData = { + id: string; + parentId: string | null; + isGroup: boolean; + isChild: boolean; +}; + +/** + * 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 location location includes the data of all the dropTargets the source is being dragged on + * @returns Instruction for dropTarget + */ +export const getInstructionFromPayload = ( + dropTarget: TDropTarget, + source: TDropTarget, + location: IPragmaticPayloadLocation +): InstructionType | undefined => { + const dropTargetData = dropTarget?.data as TargetData; + const sourceData = source?.data as TargetData; + const allDropTargets = location?.current?.dropTargets; + + // if all the dropTargets are greater than 1 meaning the source is being dragged on a group and its child at the same time + // and also if the dropTarget in question is also a group then, it should be a child of the current Droptarget + if (allDropTargets?.length > 1 && dropTargetData?.isGroup) return "make-child"; + + if (!dropTargetData || !sourceData) return undefined; + + let instruction = extractInstruction(dropTargetData)?.type; + + // If the instruction is blocked then set an instruction based on if dropTarget it is a child or not + if (instruction === "instruction-blocked") { + 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, + // 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 + * @param source + * @param favorite + * @returns + */ +export const getCanDrop = (source: TDropTarget, favorite: IFavorite | undefined) => { + const sourceData = source?.data; + + if (!sourceData) return false; + + // a favorite cannot be dropped on to itself + if (sourceData.id === favorite?.id) return false; + + return true; +}; diff --git a/web/core/components/home/widgets/recents/filters.tsx b/web/core/components/home/widgets/recents/filters.tsx new file mode 100644 index 00000000000..49c5441d484 --- /dev/null +++ b/web/core/components/home/widgets/recents/filters.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react"; +import { ChevronDown } from "lucide-react"; +import { TRecentActivityWidgetFilters } from "@plane/types"; +import { CustomMenu } from "@plane/ui"; +import { cn } from "@plane/utils"; + +export type TFiltersDropdown = { + className?: string; + activeFilter: TRecentActivityWidgetFilters | undefined; + setActiveFilter: (filter: TRecentActivityWidgetFilters) => void; +}; + +const filters = ["all", "projects", "pages", "issues"]; +export const FiltersDropdown: FC = observer((props) => { + const { className, activeFilter, setActiveFilter } = props; + + const DropdownOptions = () => + filters?.map((filter) => ( + { + setActiveFilter(filter); + }} + > +
{filter}
+
+ )); + + return ( + + {activeFilter && `${activeFilter}`} + + + } + customButtonClassName="flex justify-center" + closeOnSelect + > + + + ); +}); diff --git a/web/core/components/home/widgets/recents/index.tsx b/web/core/components/home/widgets/recents/index.tsx new file mode 100644 index 00000000000..2ec4fa10e3a --- /dev/null +++ b/web/core/components/home/widgets/recents/index.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import { observer } from "mobx-react"; +import Link from "next/link"; +// types +import { TRecentActivityWidgetResponse } from "@plane/types"; +// components +import { WidgetLoader, WidgetProps } from "@/components/dashboard/widgets"; +// hooks +import { useDashboard, useUser } from "@/hooks/store"; +import { FiltersDropdown } from "./filters"; +import { RecentIssue } from "./issue"; + +const WIDGET_KEY = "recent_activity"; + +export const RecentActivityWidget: React.FC = observer((props) => { + const { dashboardId, workspaceSlug } = props; + const ref = useRef(null); + // store hooks + const { data: currentUser } = useUser(); + const { activeFilter, setActiveFilter, fetchWidgetStats, getWidgetStats } = useDashboard(); + // derived values + const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); + const redirectionLink = `/${workspaceSlug}/profile/${currentUser?.id}/activity`; + + useEffect(() => { + fetchWidgetStats(workspaceSlug, dashboardId, { + widget_key: WIDGET_KEY, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (!widgetStats) return ; + + const resolveRecent = (activity: TRecentActivityWidgetResponse) => { + console.log(); + return ; + }; + + return ( +
+
+ + Recent + + + +
+ {widgetStats.length > 0 && ( +
+ {widgetStats.map((activity) => ( +
{resolveRecent(activity)}
+ ))} +
+ )} +
+ ); +}); diff --git a/web/core/components/home/widgets/recents/issue.tsx b/web/core/components/home/widgets/recents/issue.tsx new file mode 100644 index 00000000000..758922c8538 --- /dev/null +++ b/web/core/components/home/widgets/recents/issue.tsx @@ -0,0 +1,68 @@ +import { TRecentActivityWidgetResponse } from "@plane/types"; +import { PriorityIcon } from "@plane/ui"; +import { ListItem } from "@/components/core/list"; +import { calculateTimeAgo } from "@/helpers/date-time.helper"; +import { useIssueDetail } from "@/hooks/store"; +import { IssueIdentifier } from "@/plane-web/components/issues"; + +type BlockProps = { + activity: TRecentActivityWidgetResponse; + ref: React.RefObject; +}; +export const RecentIssue = (props: BlockProps) => { + const { activity, ref } = props; + const { + issue: { getIssueById }, + } = useIssueDetail(); + console.log({ ...activity.issue_detail, ...activity }, { ...getIssueById(activity?.issue) }); + + if (!activity?.issue) return <>; + const issueDetails = getIssueById(activity?.issue); + return ( + + +
+ {activity.issue_detail?.name} +
+
{calculateTimeAgo(activity.updated_at)}
+
+ } + quickActionElement={ +
+ +
+ {/* 0 ? "transparent-without-text" : "border-without-text"} + buttonClassName={activity.assignee_ids?.length > 0 ? "hover:bg-transparent px-0" : ""} + showTooltip={activity?.assignee_ids?.length === 0} + placeholder="Assignees" + optionsClassName="z-10" + tooltipContent="" + renderByDefault={isMobile} + /> */} +
+
+ } + parentRef={ref} + disableLink={false} + className="bg-transparent my-auto !px-2 border-custom-border-200/40 py-3" + itemClassName="my-auto" + /> + ); +}; diff --git a/web/core/components/page-views/workspace-dashboard.tsx b/web/core/components/page-views/workspace-dashboard.tsx index 095ffab941d..cfb1dfd680f 100644 --- a/web/core/components/page-views/workspace-dashboard.tsx +++ b/web/core/components/page-views/workspace-dashboard.tsx @@ -5,7 +5,6 @@ import { useParams } from "next/navigation"; import { ContentWrapper } from "@plane/ui"; import { DashboardWidgets } from "@/components/dashboard"; import { EmptyState } from "@/components/empty-state"; -import { IssuePeekOverview } from "@/components/issues"; import { TourRoot } from "@/components/onboarding"; import { UserGreetingsView } from "@/components/user"; // constants @@ -16,6 +15,7 @@ import { cn } from "@/helpers/common.helper"; // hooks import { useCommandPalette, useUserProfile, useEventTracker, useDashboard, useProject, useUser } from "@/hooks/store"; import useSize from "@/hooks/use-window-size"; +import { useHome } from "@/hooks/store/use-home"; export const WorkspaceDashboardView = observer(() => { // store hooks @@ -29,6 +29,7 @@ export const WorkspaceDashboardView = observer(() => { const { data: currentUserProfile, updateTourCompleted } = useUserProfile(); const { captureEvent } = useEventTracker(); const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard(); + const { toggleWidgetSettings } = useHome(); const { joinedProjectIds, loader } = useProject(); const [windowWidth] = useSize(); @@ -71,7 +72,9 @@ export const WorkspaceDashboardView = observer(() => { "vertical-scrollbar scrollbar-lg": windowWidth >= 768, })} > - {currentUser && } + {currentUser && ( + toggleWidgetSettings(true)} /> + )} diff --git a/web/core/components/user/user-greetings.tsx b/web/core/components/user/user-greetings.tsx index d498fa51542..24c66ddacaa 100644 --- a/web/core/components/user/user-greetings.tsx +++ b/web/core/components/user/user-greetings.tsx @@ -1,15 +1,17 @@ import { FC } from "react"; // hooks +import { Shapes } from "lucide-react"; import { IUser } from "@plane/types"; import { useCurrentTime } from "@/hooks/use-current-time"; // types export interface IUserGreetingsView { user: IUser; + handleWidgetModal: () => void; } export const UserGreetingsView: FC = (props) => { - const { user } = props; + const { user, handleWidgetModal } = props; // current time hook const { currentTime } = useCurrentTime(); @@ -37,16 +39,25 @@ export const UserGreetingsView: FC = (props) => { const greeting = parseInt(hour, 10) < 12 ? "morning" : parseInt(hour, 10) < 18 ? "afternoon" : "evening"; return ( -
-

- Good {greeting}, {user?.first_name} {user?.last_name} -

-
-
{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}
-
- {weekDay}, {date} {timeString} -
-
+
+
+

+ Good {greeting}, {user?.first_name} {user?.last_name} +

+
+
{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}
+
+ {weekDay}, {date} {timeString} +
+
+
{" "} +
); }; diff --git a/web/core/hooks/store/use-home.ts b/web/core/hooks/store/use-home.ts new file mode 100644 index 00000000000..9ada6ddbd63 --- /dev/null +++ b/web/core/hooks/store/use-home.ts @@ -0,0 +1,11 @@ +import { useContext } from "react"; +// mobx store +import { StoreContext } from "@/lib/store-context"; +// types +import { IHomeStore } from "@/store/workspace/home"; + +export const useHome = (): IHomeStore => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useDashboard must be used within StoreProvider"); + return context.workspaceRoot.home; +}; diff --git a/web/core/services/workspace.service.ts b/web/core/services/workspace.service.ts index f5187e4f3ca..09e27bbdd38 100644 --- a/web/core/services/workspace.service.ts +++ b/web/core/services/workspace.service.ts @@ -282,37 +282,32 @@ export class WorkspaceService extends APIService { } // quick links - async fetchWorkspaceLinks(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/links/`) + async fetchWorkspaceLinks(workspaceSlug: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/quick-links/`) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async createWorkspaceLink(workspaceSlug: string, projectId: string, data: Partial): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/links/`, data) + async createWorkspaceLink(workspaceSlug: string, data: Partial): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/quick-links/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async updateWorkspaceLink( - workspaceSlug: string, - projectId: string, - linkId: string, - data: Partial - ): Promise { - return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/links/${linkId}/`, data) + async updateWorkspaceLink(workspaceSlug: string, linkId: string, data: Partial): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/quick-links/${linkId}/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async deleteWorkspaceLink(workspaceSlug: string, projectId: string, linkId: string): Promise { - return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/links/${linkId}/`) + async deleteWorkspaceLink(workspaceSlug: string, linkId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/quick-links/${linkId}/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/web/core/store/dashboard.store.ts b/web/core/store/dashboard.store.ts index 9bdaadf4b09..bc50dc1f6f5 100644 --- a/web/core/store/dashboard.store.ts +++ b/web/core/store/dashboard.store.ts @@ -9,6 +9,7 @@ import { TWidgetStatsResponse, TWidgetKeys, TWidgetStatsRequestParams, + TRecentActivityWidgetFilters, } from "@plane/types"; // services import { DashboardService } from "@/services/dashboard.service"; @@ -19,6 +20,7 @@ export interface IDashboardStore { // error states widgetStatsError: { [workspaceSlug: string]: Record> }; // observables + activeFilter: TRecentActivityWidgetFilters; homeDashboardId: string | null; widgetDetails: { [workspaceSlug: string]: Record }; // { @@ -59,12 +61,14 @@ export interface IDashboardStore { widgetId: string, data: TWidgetFiltersFormData ) => Promise; + setActiveFilter: (filter: TRecentActivityWidgetFilters) => void; } export class DashboardStore implements IDashboardStore { // error states widgetStatsError: { [workspaceSlug: string]: Record> } = {}; // observables + activeFilter = "all"; homeDashboardId: string | null = null; widgetDetails: { [workspaceSlug: string]: Record } = {}; widgetStats: { [workspaceSlug: string]: Record> } = {}; @@ -79,6 +83,7 @@ export class DashboardStore implements IDashboardStore { // error states widgetStatsError: observable, // observables + activeFilter: observable, homeDashboardId: observable.ref, widgetDetails: observable, widgetStats: observable, @@ -90,6 +95,7 @@ export class DashboardStore implements IDashboardStore { // update actions updateDashboardWidget: action, updateDashboardWidgetFilters: action, + setActiveFilter: action, }); // router store @@ -282,4 +288,8 @@ export class DashboardStore implements IDashboardStore { throw error; } }; + + setActiveFilter = (filter: TRecentActivityWidgetFilters) => { + this.activeFilter = filter; + }; } diff --git a/web/core/store/workspace/home.ts b/web/core/store/workspace/home.ts new file mode 100644 index 00000000000..607ec37d01c --- /dev/null +++ b/web/core/store/workspace/home.ts @@ -0,0 +1,72 @@ +import { action, makeObservable, observable, runInAction } from "mobx"; +import { IWorkspaceLinkStore, WorkspaceLinkStore } from "./link.store"; + +export interface IHomeStore { + // observables + showWidgetSettings: boolean; + widgetsMap: Record; + //stores + quickLinks: IWorkspaceLinkStore; + // actions + toggleWidgetSettings: (value?: boolean) => void; + reorderWidgets: (workspaceSlug: string, widgetId: string, destinationId: string, edge: string | undefined) => void; +} + +export class HomeStore implements IHomeStore { + // observables + showWidgetSettings = false; + widgetsMap: Record = {}; + // stores + quickLinks: IWorkspaceLinkStore; + + constructor() { + makeObservable(this, { + // observables + showWidgetSettings: observable, + widgetsMap: observable, + // actions + toggleWidgetSettings: action, + reorderWidgets: action, + }); + // services + + // stores + this.quickLinks = new WorkspaceLinkStore(); + } + + toggleWidgetSettings = (value?: boolean) => { + this.showWidgetSettings = value !== undefined ? value : !this.showWidgetSettings; + }; + + reorderWidgets = async (workspaceSlug: string, widgetId: string, destinationId: string, edge: string | undefined) => { + // try { + // let resultSequence = 10000; + // if (edge) { + // const sortedIds = orderBy(Object.values(this.favoriteMap), "sequence", "desc").map((fav) => fav.id); + // const destinationSequence = this.favoriteMap[destinationId]?.sequence || undefined; + // if (destinationSequence) { + // const destinationIndex = sortedIds.findIndex((id) => id === destinationId); + // if (edge === "reorder-above") { + // const prevSequence = this.favoriteMap[sortedIds[destinationIndex - 1]]?.sequence || undefined; + // if (prevSequence) { + // resultSequence = (destinationSequence + prevSequence) / 2; + // } else { + // resultSequence = destinationSequence + resultSequence; + // } + // } else { + // resultSequence = destinationSequence - resultSequence; + // } + // } + // } + // await this.dashboardService.updateDashboardWidget(workspaceSlug, dashboardId, widgetId, { + // sequence: resultSequence, + // }); + // runInAction(() => { + // set(this.favoriteMap, [favoriteId, "sequence"], resultSequence); + // }); + // } catch (error) { + // console.error("Failed to move favorite folder"); + // throw error; + // } + }; +} diff --git a/web/core/store/workspace/index.ts b/web/core/store/workspace/index.ts index 8de02944fd2..112d4395518 100644 --- a/web/core/store/workspace/index.ts +++ b/web/core/store/workspace/index.ts @@ -8,7 +8,7 @@ import { WorkspaceService } from "@/plane-web/services"; import { CoreRootStore } from "@/store/root.store"; // sub-stores import { ApiTokenStore, IApiTokenStore } from "./api-token.store"; -import { IWorkspaceLinkStore, WorkspaceLinkStore } from "./link.store"; +import { HomeStore, IHomeStore } from "./home"; import { IWebhookStore, WebhookStore } from "./webhook.store"; export interface IWorkspaceRootStore { @@ -31,7 +31,7 @@ export interface IWorkspaceRootStore { // sub-stores webhook: IWebhookStore; apiToken: IApiTokenStore; - links: IWorkspaceLinkStore; + home: IHomeStore; } export class WorkspaceRootStore implements IWorkspaceRootStore { @@ -43,7 +43,7 @@ export class WorkspaceRootStore implements IWorkspaceRootStore { // root store router; user; - links; + home; // sub-stores webhook: IWebhookStore; apiToken: IApiTokenStore; @@ -72,7 +72,7 @@ export class WorkspaceRootStore implements IWorkspaceRootStore { // root store this.router = _rootStore.router; this.user = _rootStore.user; - this.links = new WorkspaceLinkStore(); + this.home = new HomeStore(); // sub-stores this.webhook = new WebhookStore(_rootStore); this.apiToken = new ApiTokenStore(_rootStore); diff --git a/web/core/store/workspace/link.store.ts b/web/core/store/workspace/link.store.ts index 60af48284bc..a6b2cd40ecd 100644 --- a/web/core/store/workspace/link.store.ts +++ b/web/core/store/workspace/link.store.ts @@ -7,7 +7,7 @@ import { WorkspaceService } from "@/plane-web/services"; export interface IWorkspaceLinkStoreActions { addLinks: (projectId: string, links: TLink[]) => void; - fetchLinks: (workspaceSlug: string, projectId: string) => Promise; + fetchLinks: (workspaceSlug: string) => Promise; createLink: (workspaceSlug: string, data: Partial) => Promise; updateLink: (workspaceSlug: string, linkId: string, data: Partial) => Promise; removeLink: (workspaceSlug: string, linkId: string) => Promise; @@ -79,27 +79,28 @@ export class WorkspaceLinkStore implements IWorkspaceLinkStore { }); }; - addLinks = (projectId: string, links: TLink[]) => { + addLinks = (workspaceSlug: string, links: TLink[]) => { runInAction(() => { - this.links[projectId] = links.map((link) => link.id); + this.links[workspaceSlug] = links.map((link) => link.id); links.forEach((link) => set(this.linkMap, link.id, link)); }); }; - fetchLinks = async (workspaceSlug: string, projectId: string) => { - const response = await this.workspaceService.fetchWorkspaceLinks(workspaceSlug, projectId); - this.addLinks(projectId, response); + fetchLinks = async (workspaceSlug: string) => { + const response = await this.workspaceService.fetchWorkspaceLinks(workspaceSlug); + this.addLinks(workspaceSlug, response); return response; }; createLink = async (workspaceSlug: string, data: Partial) => { - // const response = await this.workspaceService.createWorkspaceLink(workspaceSlug, projectId, data); - // const issueLinkCount = this.getLinksByWorkspaceId(projectId)?.length ?? 0; - // runInAction(() => { - // this.links[workspaceSlug] = [response.id, ...this.links[workspaceSlug]]; - // set(this.linkMap, response.id, response); - // }); - // return response; + console.log("hereee"); + const response = await this.workspaceService.createWorkspaceLink(workspaceSlug, data); + + runInAction(() => { + this.links[workspaceSlug] = [response.id, ...(this.links[workspaceSlug] ?? [])]; + set(this.linkMap, response.id, response); + }); + return response; }; updateLink = async (workspaceSlug: string, linkId: string, data: Partial) => { @@ -109,14 +110,13 @@ export class WorkspaceLinkStore implements IWorkspaceLinkStore { }); }); - // const response = await this.workspaceService.updateWorkspaceLink(workspaceSlug, projectId, linkId, data); - - // return response; + const response = await this.workspaceService.updateWorkspaceLink(workspaceSlug, linkId, data); + return response; }; removeLink = async (workspaceSlug: string, linkId: string) => { // const issueLinkCount = this.getLinksByWorkspaceId(projectId)?.length ?? 0; - // await this.workspaceService.deleteWorkspaceLink(workspaceSlug, projectId, linkId); + await this.workspaceService.deleteWorkspaceLink(workspaceSlug, linkId); const linkIndex = this.links[workspaceSlug].findIndex((link) => link === linkId); if (linkIndex >= 0) From 2254d50491c25de61afe1b4238366395b8ee8086 Mon Sep 17 00:00:00 2001 From: gakshita Date: Fri, 3 Jan 2025 14:45:26 +0530 Subject: [PATCH 04/18] fix --- web/core/components/page-views/workspace-dashboard.tsx | 5 +++-- web/core/store/dashboard.store.ts | 10 ---------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/web/core/components/page-views/workspace-dashboard.tsx b/web/core/components/page-views/workspace-dashboard.tsx index cfb1dfd680f..54f9cebd732 100644 --- a/web/core/components/page-views/workspace-dashboard.tsx +++ b/web/core/components/page-views/workspace-dashboard.tsx @@ -14,8 +14,9 @@ import { PRODUCT_TOUR_COMPLETED } from "@/constants/event-tracker"; import { cn } from "@/helpers/common.helper"; // hooks import { useCommandPalette, useUserProfile, useEventTracker, useDashboard, useProject, useUser } from "@/hooks/store"; -import useSize from "@/hooks/use-window-size"; import { useHome } from "@/hooks/store/use-home"; +import useSize from "@/hooks/use-window-size"; +import { IssuePeekOverview } from "../issues"; export const WorkspaceDashboardView = observer(() => { // store hooks @@ -66,7 +67,7 @@ export const WorkspaceDashboardView = observer(() => { <> {joinedProjectIds.length > 0 || loader ? ( <> - {/* */} + = 768, diff --git a/web/core/store/dashboard.store.ts b/web/core/store/dashboard.store.ts index bc50dc1f6f5..9bdaadf4b09 100644 --- a/web/core/store/dashboard.store.ts +++ b/web/core/store/dashboard.store.ts @@ -9,7 +9,6 @@ import { TWidgetStatsResponse, TWidgetKeys, TWidgetStatsRequestParams, - TRecentActivityWidgetFilters, } from "@plane/types"; // services import { DashboardService } from "@/services/dashboard.service"; @@ -20,7 +19,6 @@ export interface IDashboardStore { // error states widgetStatsError: { [workspaceSlug: string]: Record> }; // observables - activeFilter: TRecentActivityWidgetFilters; homeDashboardId: string | null; widgetDetails: { [workspaceSlug: string]: Record }; // { @@ -61,14 +59,12 @@ export interface IDashboardStore { widgetId: string, data: TWidgetFiltersFormData ) => Promise; - setActiveFilter: (filter: TRecentActivityWidgetFilters) => void; } export class DashboardStore implements IDashboardStore { // error states widgetStatsError: { [workspaceSlug: string]: Record> } = {}; // observables - activeFilter = "all"; homeDashboardId: string | null = null; widgetDetails: { [workspaceSlug: string]: Record } = {}; widgetStats: { [workspaceSlug: string]: Record> } = {}; @@ -83,7 +79,6 @@ export class DashboardStore implements IDashboardStore { // error states widgetStatsError: observable, // observables - activeFilter: observable, homeDashboardId: observable.ref, widgetDetails: observable, widgetStats: observable, @@ -95,7 +90,6 @@ export class DashboardStore implements IDashboardStore { // update actions updateDashboardWidget: action, updateDashboardWidgetFilters: action, - setActiveFilter: action, }); // router store @@ -288,8 +282,4 @@ export class DashboardStore implements IDashboardStore { throw error; } }; - - setActiveFilter = (filter: TRecentActivityWidgetFilters) => { - this.activeFilter = filter; - }; } From a0317650fb901ac421a65f361338fd1767bec71c Mon Sep 17 00:00:00 2001 From: gakshita Date: Fri, 3 Jan 2025 14:57:52 +0530 Subject: [PATCH 05/18] fix: seperate route added --- .../(projects)/home/header.tsx | 60 ++++++++++++ .../[workspaceSlug]/(projects)/home/page.tsx | 28 ++++++ web/core/components/home/index.ts | 1 + web/core/components/home/root.tsx | 96 +++++++++++++++++++ web/core/components/home/user-greetings.tsx | 63 ++++++++++++ .../page-views/workspace-dashboard.tsx | 6 +- web/core/components/user/user-greetings.tsx | 33 +++---- 7 files changed, 260 insertions(+), 27 deletions(-) create mode 100644 web/app/[workspaceSlug]/(projects)/home/header.tsx create mode 100644 web/app/[workspaceSlug]/(projects)/home/page.tsx create mode 100644 web/core/components/home/root.tsx create mode 100644 web/core/components/home/user-greetings.tsx diff --git a/web/app/[workspaceSlug]/(projects)/home/header.tsx b/web/app/[workspaceSlug]/(projects)/home/header.tsx new file mode 100644 index 00000000000..a7a19afe6b0 --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/home/header.tsx @@ -0,0 +1,60 @@ +"use client"; + +import Image from "next/image"; +import { useTheme } from "next-themes"; +import { Home } from "lucide-react"; +// images +import githubBlackImage from "/public/logos/github-black.png"; +import githubWhiteImage from "/public/logos/github-white.png"; +// ui +import { Breadcrumbs, Header } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common"; +// constants +import { GITHUB_REDIRECTED } from "@/constants/event-tracker"; +// hooks +import { useEventTracker } from "@/hooks/store"; + +export const WorkspaceDashboardHeader = () => { + // hooks + const { captureEvent } = useEventTracker(); + const { resolvedTheme } = useTheme(); + + return ( + <> +
+ +
+ + } />} + /> + +
+
+ + + captureEvent(GITHUB_REDIRECTED, { + element: "navbar", + }) + } + className="flex flex-shrink-0 items-center gap-1.5 rounded bg-custom-background-80 px-3 py-1.5" + href="https://github.com/makeplane/plane" + target="_blank" + rel="noopener noreferrer" + > + GitHub Logo + Star us on GitHub + + +
+ + ); +}; diff --git a/web/app/[workspaceSlug]/(projects)/home/page.tsx b/web/app/[workspaceSlug]/(projects)/home/page.tsx new file mode 100644 index 00000000000..9f9f535e03d --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/home/page.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { observer } from "mobx-react"; +// components +import { PageHead, AppHeader, ContentWrapper } from "@/components/core"; +// hooks +import { WorkspaceHomeView } from "@/components/home"; +import { useWorkspace } from "@/hooks/store"; +// local components +import { WorkspaceDashboardHeader } from "../header"; + +const WorkspaceDashboardPage = observer(() => { + const { currentWorkspace } = useWorkspace(); + // derived values + const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Home` : undefined; + + return ( + <> + } /> + + + + + + ); +}); + +export default WorkspaceDashboardPage; diff --git a/web/core/components/home/index.ts b/web/core/components/home/index.ts index 129cdb69ea3..d783e089a0e 100644 --- a/web/core/components/home/index.ts +++ b/web/core/components/home/index.ts @@ -1,3 +1,4 @@ export * from "./widgets"; export * from "./home-dashboard-widgets"; export * from "./project-empty-state"; +export * from "./root"; diff --git a/web/core/components/home/root.tsx b/web/core/components/home/root.tsx new file mode 100644 index 00000000000..cd836eced73 --- /dev/null +++ b/web/core/components/home/root.tsx @@ -0,0 +1,96 @@ +import { useEffect } from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +// components +import { ContentWrapper } from "@plane/ui"; +import { EmptyState } from "@/components/empty-state"; +import { TourRoot } from "@/components/onboarding"; +// constants +import { EmptyStateType } from "@/constants/empty-state"; +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 { useHome } from "@/hooks/store/use-home"; +import useSize from "@/hooks/use-window-size"; +import { IssuePeekOverview } from "../issues"; +import { DashboardWidgets } from "./home-dashboard-widgets"; +import { UserGreetingsView } from "./user-greetings"; + +export const WorkspaceHomeView = observer(() => { + // store hooks + const { + // captureEvent, + setTrackElement, + } = useEventTracker(); + const { toggleCreateProjectModal } = useCommandPalette(); + const { workspaceSlug } = useParams(); + const { data: currentUser } = useUser(); + const { data: currentUserProfile, updateTourCompleted } = useUserProfile(); + const { captureEvent } = useEventTracker(); + const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard(); + const { toggleWidgetSettings } = useHome(); + const { joinedProjectIds, loader } = useProject(); + + const [windowWidth] = useSize(); + + const handleTourCompleted = () => { + updateTourCompleted() + .then(() => { + captureEvent(PRODUCT_TOUR_COMPLETED, { + user_id: currentUser?.id, + state: "SUCCESS", + }); + }) + .catch((error) => { + console.error(error); + }); + }; + + // fetch home dashboard widgets on workspace change + useEffect(() => { + if (!workspaceSlug) return; + + fetchHomeDashboardWidgets(workspaceSlug?.toString()); + }, [fetchHomeDashboardWidgets, workspaceSlug]); + + // TODO: refactor loader implementation + return ( + <> + {currentUserProfile && !currentUserProfile.is_tour_completed && ( +
+ +
+ )} + {homeDashboardId && joinedProjectIds && ( + <> + {joinedProjectIds.length > 0 || loader ? ( + <> + + = 768, + })} + > + {currentUser && ( + toggleWidgetSettings(true)} /> + )} + + + + + ) : ( + { + setTrackElement("Dashboard empty state"); + toggleCreateProjectModal(true); + }} + /> + )} + + )} + + ); +}); diff --git a/web/core/components/home/user-greetings.tsx b/web/core/components/home/user-greetings.tsx new file mode 100644 index 00000000000..24c66ddacaa --- /dev/null +++ b/web/core/components/home/user-greetings.tsx @@ -0,0 +1,63 @@ +import { FC } from "react"; +// hooks +import { Shapes } from "lucide-react"; +import { IUser } from "@plane/types"; +import { useCurrentTime } from "@/hooks/use-current-time"; +// types + +export interface IUserGreetingsView { + user: IUser; + handleWidgetModal: () => void; +} + +export const UserGreetingsView: FC = (props) => { + const { user, handleWidgetModal } = props; + // current time hook + const { currentTime } = useCurrentTime(); + + const hour = new Intl.DateTimeFormat("en-US", { + hour12: false, + hour: "numeric", + }).format(currentTime); + + const date = new Intl.DateTimeFormat("en-US", { + month: "short", + day: "numeric", + }).format(currentTime); + + const weekDay = new Intl.DateTimeFormat("en-US", { + weekday: "long", + }).format(currentTime); + + const timeString = new Intl.DateTimeFormat("en-US", { + timeZone: user?.user_timezone, + hour12: false, // Use 24-hour format + hour: "2-digit", + minute: "2-digit", + }).format(currentTime); + + const greeting = parseInt(hour, 10) < 12 ? "morning" : parseInt(hour, 10) < 18 ? "afternoon" : "evening"; + + return ( +
+
+

+ Good {greeting}, {user?.first_name} {user?.last_name} +

+
+
{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}
+
+ {weekDay}, {date} {timeString} +
+
+
{" "} + +
+ ); +}; diff --git a/web/core/components/page-views/workspace-dashboard.tsx b/web/core/components/page-views/workspace-dashboard.tsx index 54f9cebd732..25665169d04 100644 --- a/web/core/components/page-views/workspace-dashboard.tsx +++ b/web/core/components/page-views/workspace-dashboard.tsx @@ -14,7 +14,6 @@ import { PRODUCT_TOUR_COMPLETED } from "@/constants/event-tracker"; import { cn } from "@/helpers/common.helper"; // hooks import { useCommandPalette, useUserProfile, useEventTracker, useDashboard, useProject, useUser } from "@/hooks/store"; -import { useHome } from "@/hooks/store/use-home"; import useSize from "@/hooks/use-window-size"; import { IssuePeekOverview } from "../issues"; @@ -30,7 +29,6 @@ export const WorkspaceDashboardView = observer(() => { const { data: currentUserProfile, updateTourCompleted } = useUserProfile(); const { captureEvent } = useEventTracker(); const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard(); - const { toggleWidgetSettings } = useHome(); const { joinedProjectIds, loader } = useProject(); const [windowWidth] = useSize(); @@ -73,9 +71,7 @@ export const WorkspaceDashboardView = observer(() => { "vertical-scrollbar scrollbar-lg": windowWidth >= 768, })} > - {currentUser && ( - toggleWidgetSettings(true)} /> - )} + {currentUser && }
diff --git a/web/core/components/user/user-greetings.tsx b/web/core/components/user/user-greetings.tsx index 24c66ddacaa..216da7a5292 100644 --- a/web/core/components/user/user-greetings.tsx +++ b/web/core/components/user/user-greetings.tsx @@ -1,17 +1,15 @@ import { FC } from "react"; // hooks -import { Shapes } from "lucide-react"; import { IUser } from "@plane/types"; import { useCurrentTime } from "@/hooks/use-current-time"; // types export interface IUserGreetingsView { user: IUser; - handleWidgetModal: () => void; } export const UserGreetingsView: FC = (props) => { - const { user, handleWidgetModal } = props; + const { user } = props; // current time hook const { currentTime } = useCurrentTime(); @@ -39,25 +37,16 @@ export const UserGreetingsView: FC = (props) => { const greeting = parseInt(hour, 10) < 12 ? "morning" : parseInt(hour, 10) < 18 ? "afternoon" : "evening"; return ( -
-
-

- Good {greeting}, {user?.first_name} {user?.last_name} -

-
-
{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}
-
- {weekDay}, {date} {timeString} -
-
-
{" "} - +
+

+ Good {greeting}, {user?.first_name} {user?.last_name} +

+
+
{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}
+
+ {weekDay}, {date} {timeString} +
+
); }; From 0d564e6a5d30c03ccdfd1bb24836ecd60fdfa1b9 Mon Sep 17 00:00:00 2001 From: gakshita Date: Fri, 3 Jan 2025 15:12:15 +0530 Subject: [PATCH 06/18] fix --- web/core/components/page-views/workspace-dashboard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/core/components/page-views/workspace-dashboard.tsx b/web/core/components/page-views/workspace-dashboard.tsx index 25665169d04..f4df757cd8b 100644 --- a/web/core/components/page-views/workspace-dashboard.tsx +++ b/web/core/components/page-views/workspace-dashboard.tsx @@ -5,6 +5,7 @@ import { useParams } from "next/navigation"; import { ContentWrapper } from "@plane/ui"; import { DashboardWidgets } from "@/components/dashboard"; import { EmptyState } from "@/components/empty-state"; +import { IssuePeekOverview } from "@/components/issues"; import { TourRoot } from "@/components/onboarding"; import { UserGreetingsView } from "@/components/user"; // constants @@ -15,7 +16,6 @@ import { cn } from "@/helpers/common.helper"; // hooks import { useCommandPalette, useUserProfile, useEventTracker, useDashboard, useProject, useUser } from "@/hooks/store"; import useSize from "@/hooks/use-window-size"; -import { IssuePeekOverview } from "../issues"; export const WorkspaceDashboardView = observer(() => { // store hooks From bba84361b165836a8400ad7aeefff69c1c7a5aa9 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Fri, 3 Jan 2025 16:51:27 +0530 Subject: [PATCH 07/18] Only return user ID of project members --- apiserver/plane/app/serializers/workspace.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py index ad45cf1a82a..beb158aaa8a 100644 --- a/apiserver/plane/app/serializers/workspace.py +++ b/apiserver/plane/app/serializers/workspace.py @@ -153,13 +153,6 @@ def get_project_identifier(self, obj): return project.identifier if project else None -class ProjectMemberSerializer(BaseSerializer): - member = UserLiteSerializer(read_only=True) - - class Meta: - model = ProjectMember - fields = ["member"] - class ProjectRecentVisitSerializer(serializers.ModelSerializer): project_members = serializers.SerializerMethodField() @@ -168,10 +161,9 @@ class Meta: fields = ["id", "name", "logo_props", "project_members", "identifier"] def get_project_members(self, obj): - members = ProjectMember.objects.filter(project_id=obj.id).select_related('member') - - serializer = ProjectMemberSerializer(members, many=True) - return serializer.data + members = ProjectMember.objects.filter(project_id=obj.id).values_list("member", flat=True) + + return members class PageRecentVisitSerializer(serializers.ModelSerializer): project_id = serializers.SerializerMethodField() From 99d9702ef5b5667432e953c0aac46f1c10607371 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Fri, 3 Jan 2025 17:02:30 +0530 Subject: [PATCH 08/18] Return issue ID --- apiserver/plane/app/serializers/workspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py index beb158aaa8a..d809be131ad 100644 --- a/apiserver/plane/app/serializers/workspace.py +++ b/apiserver/plane/app/serializers/workspace.py @@ -146,7 +146,7 @@ class IssueRecentVisitSerializer(serializers.ModelSerializer): class Meta: model = Issue - fields = ["name", "state", "priority", "assignees", "type", "sequence_id", "project_id", "project_identifier"] + fields = ["id", "name", "state", "priority", "assignees", "type", "sequence_id", "project_id", "project_identifier", ] def get_project_identifier(self, obj): project = obj.project From ff29170ec7e8d33271bfeaa103196f3849eeccdb Mon Sep 17 00:00:00 2001 From: gakshita Date: Fri, 3 Jan 2025 19:38:14 +0530 Subject: [PATCH 09/18] fix: recents api integrations --- packages/types/src/dashboard.d.ts | 38 +++++++ .../home/home-dashboard-widgets.tsx | 43 ++++---- .../home/widgets/empty-states/root.tsx | 2 +- .../home/{ => widgets}/links/action.tsx | 0 .../links/create-update-link-modal.tsx | 0 .../home/{ => widgets}/links/index.ts | 0 .../home/{ => widgets}/links/link-detail.tsx | 2 +- .../home/{ => widgets}/links/links.tsx | 4 +- .../home/{ => widgets}/links/root.tsx | 2 +- .../home/{ => widgets}/links/use-links.tsx | 0 .../home/widgets/loaders/assigned-issues.tsx | 24 ----- .../widgets/loaders/issues-by-priority.tsx | 17 ---- .../widgets/loaders/issues-by-state-group.tsx | 24 ----- .../home/widgets/loaders/loader.tsx | 26 ++--- .../home/widgets/loaders/overview-stats.tsx | 16 --- .../home/widgets/loaders/quick-links.tsx | 13 +++ .../home/widgets/loaders/recent-activity.tsx | 8 +- .../widgets/loaders/recent-collaborators.tsx | 20 ---- .../home/widgets/loaders/recent-projects.tsx | 22 ----- .../components/home/widgets/manage/index.tsx | 4 +- .../home/widgets/manage/widget-item.tsx | 8 +- .../home/widgets/manage/widget-list.tsx | 4 +- .../home/widgets/recents/filters.tsx | 20 ++-- .../components/home/widgets/recents/index.tsx | 99 +++++++++++-------- .../components/home/widgets/recents/issue.tsx | 83 +++++++++------- .../components/home/widgets/recents/page.tsx | 63 ++++++++++++ .../home/widgets/recents/project.tsx | 71 +++++++++++++ .../hooks/use-workspace-issue-properties.ts | 9 -- .../layouts/auth-layout/workspace-wrapper.tsx | 9 +- web/core/services/workspace.service.ts | 14 +++ web/core/store/workspace/home.ts | 13 +++ 31 files changed, 380 insertions(+), 278 deletions(-) rename web/core/components/home/{ => widgets}/links/action.tsx (100%) rename web/core/components/home/{ => widgets}/links/create-update-link-modal.tsx (100%) rename web/core/components/home/{ => widgets}/links/index.ts (100%) rename web/core/components/home/{ => widgets}/links/link-detail.tsx (96%) rename web/core/components/home/{ => widgets}/links/links.tsx (94%) rename web/core/components/home/{ => widgets}/links/root.tsx (96%) rename web/core/components/home/{ => widgets}/links/use-links.tsx (100%) delete mode 100644 web/core/components/home/widgets/loaders/assigned-issues.tsx delete mode 100644 web/core/components/home/widgets/loaders/issues-by-priority.tsx delete mode 100644 web/core/components/home/widgets/loaders/issues-by-state-group.tsx delete mode 100644 web/core/components/home/widgets/loaders/overview-stats.tsx create mode 100644 web/core/components/home/widgets/loaders/quick-links.tsx delete mode 100644 web/core/components/home/widgets/loaders/recent-collaborators.tsx delete mode 100644 web/core/components/home/widgets/loaders/recent-projects.tsx create mode 100644 web/core/components/home/widgets/recents/page.tsx create mode 100644 web/core/components/home/widgets/recents/project.tsx diff --git a/packages/types/src/dashboard.d.ts b/packages/types/src/dashboard.d.ts index b42b5506fbd..d96523280f9 100644 --- a/packages/types/src/dashboard.d.ts +++ b/packages/types/src/dashboard.d.ts @@ -186,3 +186,41 @@ export type THomeDashboardResponse = { dashboard: TDashboard; widgets: TWidget[]; }; + +// home +type TPageEntityData = { + id: string; + name: string; + logo_props: TLogoProps; + project_id: string; + owned_by: string; + project_identifier: string; +}; + +type TProjectEntityData = { + id: string; + name: string; + logo_props: TLogoProps; + project_members: ProjectMember[]; + identifier: string; +}; + +type TIssueEntityData = { + id: string; + name: string; + state: string; + priority: TIssuePriorities; + assignees: string[]; + type: string | null; + sequence_id: number; + project_id: string; + project_identifier: string; +}; + +type TActivityEntityData = { + id: string; + entity_name: "page" | "project" | "issue"; + entity_identifier: string; + visited_at: string; + entity_data: TPageEntityData | TProjectEntityData | TIssueEntityData; +}; diff --git a/web/core/components/home/home-dashboard-widgets.tsx b/web/core/components/home/home-dashboard-widgets.tsx index 69863972b0a..1e910082cab 100644 --- a/web/core/components/home/home-dashboard-widgets.tsx +++ b/web/core/components/home/home-dashboard-widgets.tsx @@ -3,20 +3,20 @@ import { useParams } from "next/navigation"; // types import { TWidgetKeys, WidgetProps } from "@plane/types"; // components -import { RecentActivityWidget, EmptyWorkspace } from "@/components/dashboard"; // hooks import { useDashboard, useProject } from "@/hooks/store"; import { useHome } from "@/hooks/store/use-home"; import { HomePageHeader } from "@/plane-web/components/home/header"; -import { DashboardQuickLinks } from "./links"; +import { RecentActivityWidget } from "./widgets"; +import { DashboardQuickLinks } from "./widgets/links"; import { ManageWidgetsModal } from "./widgets/manage"; +import { StickiesWidget } from "@/plane-web/components/stickies"; const WIDGETS_LIST: { - [key in TWidgetKeys]: { component: React.FC; fullWidth: boolean }; + [key: string]: { component: React.FC; fullWidth: boolean }; } = { recent_activity: { component: RecentActivityWidget, fullWidth: false }, - // recent_collaborators: { component: RecentCollaboratorsWidget, fullWidth: true }, - // my_stickies: { component: StickiesWidget, fullWidth: false }, + stickies: { component: StickiesWidget, fullWidth: false }, }; export const DashboardWidgets = observer(() => { @@ -42,25 +42,20 @@ export const DashboardWidgets = observer(() => { /> - {totalProjectIds?.length === 0 ? ( - - ) : ( - Object.entries(WIDGETS_LIST).map(([key, widget]) => { - const WidgetComponent = widget.component; - // if the widget doesn't exist, return null - // if (!doesWidgetExist(key as TWidgetKeys)) return null; - // if the widget is full width, return it in a 2 column grid - console.log({ widget, key }); - if (widget.fullWidth) - return ( -
- -
- ); - else - return ; - }) - )} + {Object.entries(WIDGETS_LIST).map(([key, widget]) => { + const WidgetComponent = widget.component; + // if the widget doesn't exist, return null + // if (!doesWidgetExist(key as TWidgetKeys)) return null; + // if the widget is full width, return it in a 2 column grid + if (widget.fullWidth) + return ( +
+ +
+ ); + else + return ; + })}
); }); diff --git a/web/core/components/home/widgets/empty-states/root.tsx b/web/core/components/home/widgets/empty-states/root.tsx index fe6c8e5f3b2..06606f367d0 100644 --- a/web/core/components/home/widgets/empty-states/root.tsx +++ b/web/core/components/home/widgets/empty-states/root.tsx @@ -42,7 +42,7 @@ export const EmptyWorkspace = () => { icon: , cta: { text: "Invite now", - link: "settings/members", + link: `/${workspaceSlug}/settings/members`, }, }, { diff --git a/web/core/components/home/links/action.tsx b/web/core/components/home/widgets/links/action.tsx similarity index 100% rename from web/core/components/home/links/action.tsx rename to web/core/components/home/widgets/links/action.tsx diff --git a/web/core/components/home/links/create-update-link-modal.tsx b/web/core/components/home/widgets/links/create-update-link-modal.tsx similarity index 100% rename from web/core/components/home/links/create-update-link-modal.tsx rename to web/core/components/home/widgets/links/create-update-link-modal.tsx diff --git a/web/core/components/home/links/index.ts b/web/core/components/home/widgets/links/index.ts similarity index 100% rename from web/core/components/home/links/index.ts rename to web/core/components/home/widgets/links/index.ts diff --git a/web/core/components/home/links/link-detail.tsx b/web/core/components/home/widgets/links/link-detail.tsx similarity index 96% rename from web/core/components/home/links/link-detail.tsx rename to web/core/components/home/widgets/links/link-detail.tsx index cbea44c8b58..26aa4f9c8b4 100644 --- a/web/core/components/home/links/link-detail.tsx +++ b/web/core/components/home/widgets/links/link-detail.tsx @@ -80,7 +80,7 @@ export const ProjectLinkDetail: FC = observer((props) => {
{linkDetail.title || linkDetail.url}
-
{calculateTimeAgo(linkDetail.created_at)}
+
{calculateTimeAgo(linkDetail.created_at)}
; export type TProjectLinkList = { linkOperations: TLinkOperationsModal; - workspaceSlug: string; }; @@ -50,7 +50,7 @@ export const ProjectLinkList: FC = observer((props) => { return () => window.removeEventListener("resize", updateColumnCount); }, []); - if (!links) return <>; + if (links === undefined) return ; return (
diff --git a/web/core/components/home/links/root.tsx b/web/core/components/home/widgets/links/root.tsx similarity index 96% rename from web/core/components/home/links/root.tsx rename to web/core/components/home/widgets/links/root.tsx index 7a30c0dbe40..1b2b389709d 100644 --- a/web/core/components/home/links/root.tsx +++ b/web/core/components/home/widgets/links/root.tsx @@ -33,7 +33,7 @@ export const DashboardQuickLinks = observer((props: TProps) => { preloadedData={linkData} setLinkData={setLinkData} /> -
+
{/* rendering links */}
diff --git a/web/core/components/home/links/use-links.tsx b/web/core/components/home/widgets/links/use-links.tsx similarity index 100% rename from web/core/components/home/links/use-links.tsx rename to web/core/components/home/widgets/links/use-links.tsx diff --git a/web/core/components/home/widgets/loaders/assigned-issues.tsx b/web/core/components/home/widgets/loaders/assigned-issues.tsx deleted file mode 100644 index 8a78fedf1a4..00000000000 --- a/web/core/components/home/widgets/loaders/assigned-issues.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; - -// ui -import { Loader } from "@plane/ui"; - -export const AssignedIssuesWidgetLoader = () => ( - -
- - -
-
- - -
-
- - - - - -
-
-); diff --git a/web/core/components/home/widgets/loaders/issues-by-priority.tsx b/web/core/components/home/widgets/loaders/issues-by-priority.tsx deleted file mode 100644 index c6f075b5806..00000000000 --- a/web/core/components/home/widgets/loaders/issues-by-priority.tsx +++ /dev/null @@ -1,17 +0,0 @@ -"use client"; - -// ui -import { Loader } from "@plane/ui"; - -export const IssuesByPriorityWidgetLoader = () => ( - - -
- - - - - -
-
-); diff --git a/web/core/components/home/widgets/loaders/issues-by-state-group.tsx b/web/core/components/home/widgets/loaders/issues-by-state-group.tsx deleted file mode 100644 index 099323cce75..00000000000 --- a/web/core/components/home/widgets/loaders/issues-by-state-group.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; - -import range from "lodash/range"; -// ui -import { Loader } from "@plane/ui"; - -export const IssuesByStateGroupWidgetLoader = () => ( - - -
-
-
- -
-
-
-
- {range(5).map((index) => ( - - ))} -
-
- -); diff --git a/web/core/components/home/widgets/loaders/loader.tsx b/web/core/components/home/widgets/loaders/loader.tsx index ae4038b38da..0edb95346b0 100644 --- a/web/core/components/home/widgets/loaders/loader.tsx +++ b/web/core/components/home/widgets/loaders/loader.tsx @@ -1,30 +1,24 @@ // components -import { TWidgetKeys } from "@plane/types"; -import { AssignedIssuesWidgetLoader } from "./assigned-issues"; -import { IssuesByPriorityWidgetLoader } from "./issues-by-priority"; -import { IssuesByStateGroupWidgetLoader } from "./issues-by-state-group"; -import { OverviewStatsWidgetLoader } from "./overview-stats"; import { RecentActivityWidgetLoader } from "./recent-activity"; -import { RecentCollaboratorsWidgetLoader } from "./recent-collaborators"; -import { RecentProjectsWidgetLoader } from "./recent-projects"; +import { QuickLinksWidgetLoader } from "./quick-links"; + // types type Props = { - widgetKey: TWidgetKeys; + widgetKey: EWidgetKeys; }; +export enum EWidgetKeys { + RECENT_ACTIVITY = "recent_activity", + QUICK_LINKS = "quick_links", +} + export const WidgetLoader: React.FC = (props) => { const { widgetKey } = props; const loaders = { - overview_stats: , - assigned_issues: , - created_issues: , - issues_by_state_groups: , - issues_by_priority: , - recent_activity: , - recent_projects: , - recent_collaborators: , + [EWidgetKeys.RECENT_ACTIVITY]: , + [EWidgetKeys.QUICK_LINKS]: , }; return loaders[widgetKey]; diff --git a/web/core/components/home/widgets/loaders/overview-stats.tsx b/web/core/components/home/widgets/loaders/overview-stats.tsx deleted file mode 100644 index e780bb39966..00000000000 --- a/web/core/components/home/widgets/loaders/overview-stats.tsx +++ /dev/null @@ -1,16 +0,0 @@ -"use client"; - -import range from "lodash/range"; -// ui -import { Loader } from "@plane/ui"; - -export const OverviewStatsWidgetLoader = () => ( - - {range(4).map((index) => ( -
- - -
- ))} -
-); diff --git a/web/core/components/home/widgets/loaders/quick-links.tsx b/web/core/components/home/widgets/loaders/quick-links.tsx new file mode 100644 index 00000000000..e4b8deafcb8 --- /dev/null +++ b/web/core/components/home/widgets/loaders/quick-links.tsx @@ -0,0 +1,13 @@ +"use client"; + +import range from "lodash/range"; +// ui +import { Loader } from "@plane/ui"; + +export const QuickLinksWidgetLoader = () => ( + + {range(4).map((index) => ( + + ))} + +); diff --git a/web/core/components/home/widgets/loaders/recent-activity.tsx b/web/core/components/home/widgets/loaders/recent-activity.tsx index 2df78a15af1..2f78db64a0a 100644 --- a/web/core/components/home/widgets/loaders/recent-activity.tsx +++ b/web/core/components/home/widgets/loaders/recent-activity.tsx @@ -5,16 +5,14 @@ import range from "lodash/range"; import { Loader } from "@plane/ui"; export const RecentActivityWidgetLoader = () => ( - - + {range(7).map((index) => (
- +
-
+
-
))} diff --git a/web/core/components/home/widgets/loaders/recent-collaborators.tsx b/web/core/components/home/widgets/loaders/recent-collaborators.tsx deleted file mode 100644 index 2dceaf1320a..00000000000 --- a/web/core/components/home/widgets/loaders/recent-collaborators.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client"; - -import range from "lodash/range"; -// ui -import { Loader } from "@plane/ui"; - -export const RecentCollaboratorsWidgetLoader = () => ( - <> - {range(8).map((index) => ( - -
-
- -
- -
-
- ))} - -); diff --git a/web/core/components/home/widgets/loaders/recent-projects.tsx b/web/core/components/home/widgets/loaders/recent-projects.tsx deleted file mode 100644 index 38bc7e29a01..00000000000 --- a/web/core/components/home/widgets/loaders/recent-projects.tsx +++ /dev/null @@ -1,22 +0,0 @@ -"use client"; - -import range from "lodash/range"; -// ui -import { Loader } from "@plane/ui"; - -export const RecentProjectsWidgetLoader = () => ( - - - {range(5).map((index) => ( -
-
- -
-
- - -
-
- ))} -
-); diff --git a/web/core/components/home/widgets/manage/index.tsx b/web/core/components/home/widgets/manage/index.tsx index 9a2c60c440b..670212c6f59 100644 --- a/web/core/components/home/widgets/manage/index.tsx +++ b/web/core/components/home/widgets/manage/index.tsx @@ -4,7 +4,7 @@ import { FC } from "react"; import { observer } from "mobx-react"; // plane types // plane ui -import { Button, ModalCore } from "@plane/ui"; +import { Button, EModalWidth, ModalCore } from "@plane/ui"; import { WidgetList } from "./widget-list"; export type TProps = { @@ -18,7 +18,7 @@ export const ManageWidgetsModal: FC = observer((props) => { const { workspaceSlug, isModalOpen, handleOnClose } = props; return ( - +
Manage widgets
diff --git a/web/core/components/home/widgets/manage/widget-item.tsx b/web/core/components/home/widgets/manage/widget-item.tsx index 2e2dc380299..d9c4523b85a 100644 --- a/web/core/components/home/widgets/manage/widget-item.tsx +++ b/web/core/components/home/widgets/manage/widget-item.tsx @@ -112,14 +112,16 @@ export const WidgetItem: FC = observer((props) => {
- -
{widget.title}
+
+ +
{widget.title}
+
{isLastChild && } diff --git a/web/core/components/home/widgets/manage/widget-list.tsx b/web/core/components/home/widgets/manage/widget-list.tsx index 4d93275c996..485f6891b21 100644 --- a/web/core/components/home/widgets/manage/widget-list.tsx +++ b/web/core/components/home/widgets/manage/widget-list.tsx @@ -3,9 +3,9 @@ import { DropTargetRecord, ElementDragPayload, } from "@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types"; -import { useDashboard } from "@/hooks/store"; import { WidgetItem } from "./widget-item"; import { getInstructionFromPayload, TargetData } from "./widget.helpers"; +import { useHome } from "@/hooks/store/use-home"; const WIDGETS_LIST = [ { id: 1, title: "quick links" }, @@ -13,7 +13,7 @@ const WIDGETS_LIST = [ { id: 3, title: "stickies" }, ]; export const WidgetList = ({ workspaceSlug }: { workspaceSlug: string }) => { - const { reorderWidgets } = useDashboard(); + const { reorderWidgets } = useHome(); const handleDrop = (self: DropTargetRecord, source: ElementDragPayload, location: DragLocationHistory) => { const dropTargets = location?.current?.dropTargets ?? []; diff --git a/web/core/components/home/widgets/recents/filters.tsx b/web/core/components/home/widgets/recents/filters.tsx index 49c5441d484..61edc808856 100644 --- a/web/core/components/home/widgets/recents/filters.tsx +++ b/web/core/components/home/widgets/recents/filters.tsx @@ -3,30 +3,30 @@ import { FC } from "react"; import { observer } from "mobx-react"; import { ChevronDown } from "lucide-react"; -import { TRecentActivityWidgetFilters } from "@plane/types"; import { CustomMenu } from "@plane/ui"; import { cn } from "@plane/utils"; export type TFiltersDropdown = { className?: string; - activeFilter: TRecentActivityWidgetFilters | undefined; - setActiveFilter: (filter: TRecentActivityWidgetFilters) => void; + activeFilter: string; + setActiveFilter: (filter: string) => void; + filters: { name: string; icon?: React.ReactNode }[]; }; -const filters = ["all", "projects", "pages", "issues"]; export const FiltersDropdown: FC = observer((props) => { - const { className, activeFilter, setActiveFilter } = props; + const { className, activeFilter, setActiveFilter, filters } = props; const DropdownOptions = () => filters?.map((filter) => ( { - setActiveFilter(filter); + setActiveFilter(filter.name); }} > -
{filter}
+ {filter.icon &&
{filter.icon}
} +
{`${filter.name}s`}
)); @@ -37,7 +37,7 @@ export const FiltersDropdown: FC = observer((props) => { placement="bottom-start" customButton={ } diff --git a/web/core/components/home/widgets/recents/index.tsx b/web/core/components/home/widgets/recents/index.tsx index 2ec4fa10e3a..9ba0862b312 100644 --- a/web/core/components/home/widgets/recents/index.tsx +++ b/web/core/components/home/widgets/recents/index.tsx @@ -1,59 +1,78 @@ "use client"; -import { useEffect, useRef } from "react"; +import { useRef, useState } from "react"; import { observer } from "mobx-react"; -import Link from "next/link"; +import { WidgetProps } from "@/components/dashboard/widgets"; // types -import { TRecentActivityWidgetResponse } from "@plane/types"; +import { TActivityEntityData } from "@plane/types"; // components -import { WidgetLoader, WidgetProps } from "@/components/dashboard/widgets"; -// hooks -import { useDashboard, useUser } from "@/hooks/store"; 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"; -const WIDGET_KEY = "recent_activity"; +const WIDGET_KEY = EWidgetKeys.RECENT_ACTIVITY; +const workspaceService = new WorkspaceService(); +const filters = [ + { name: "all item" }, + { name: "issue", icon: }, + { name: "page", icon: }, + { name: "project", icon: }, +]; export const RecentActivityWidget: React.FC = observer((props) => { - const { dashboardId, workspaceSlug } = props; + const { workspaceSlug } = props; const ref = useRef(null); - // store hooks - const { data: currentUser } = useUser(); - const { activeFilter, setActiveFilter, fetchWidgetStats, getWidgetStats } = useDashboard(); - // derived values - const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const redirectionLink = `/${workspaceSlug}/profile/${currentUser?.id}/activity`; - - useEffect(() => { - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (!widgetStats) return ; - - const resolveRecent = (activity: TRecentActivityWidgetResponse) => { - console.log(); - return ; + const [filter, setFilter] = useState(filters[0].name); + + const { data: recents, isLoading } = useSWR( + workspaceSlug ? `WORKSPACE_RECENT_ACTIVITY_${workspaceSlug}_${filter}` : null, + workspaceSlug + ? () => + workspaceService.fetchWorkspaceRecents( + workspaceSlug.toString(), + filter === filters[0].name ? undefined : filter + ) + : null, + { + revalidateIfStale: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + } + ); + + const resolveRecent = (activity: TActivityEntityData) => { + switch (activity.entity_name) { + case "page": + return ; + case "project": + return ; + case "issue": + return ; + default: + return <>; + } }; + if (!isLoading && recents?.length === 0) return ; + return ( -
-
- - Recent - +
+
+
Recents
- +
- {widgetStats.length > 0 && ( -
- {widgetStats.map((activity) => ( -
{resolveRecent(activity)}
- ))} -
- )} + {isLoading && } + {!isLoading && + recents?.length > 0 && + recents.map((activity: TActivityEntityData) =>
{resolveRecent(activity)}
)}
); }); diff --git a/web/core/components/home/widgets/recents/issue.tsx b/web/core/components/home/widgets/recents/issue.tsx index 758922c8538..14095d6903c 100644 --- a/web/core/components/home/widgets/recents/issue.tsx +++ b/web/core/components/home/widgets/recents/issue.tsx @@ -1,23 +1,25 @@ -import { TRecentActivityWidgetResponse } from "@plane/types"; -import { PriorityIcon } from "@plane/ui"; +import { TActivityEntityData, TIssueEntityData } from "@plane/types"; +import { PriorityIcon, StateGroupIcon } from "@plane/ui"; import { ListItem } from "@/components/core/list"; import { calculateTimeAgo } from "@/helpers/date-time.helper"; -import { useIssueDetail } from "@/hooks/store"; import { IssueIdentifier } from "@/plane-web/components/issues"; +import { MemberDropdown } from "@/components/dropdowns"; +import { useIssueDetail, useProjectState } from "@/hooks/store"; type BlockProps = { - activity: TRecentActivityWidgetResponse; + activity: TActivityEntityData; ref: React.RefObject; + workspaceSlug: string; }; export const RecentIssue = (props: BlockProps) => { - const { activity, ref } = props; - const { - issue: { getIssueById }, - } = useIssueDetail(); - console.log({ ...activity.issue_detail, ...activity }, { ...getIssueById(activity?.issue) }); + const { activity, ref, workspaceSlug } = props; + // hooks + const { getStateById } = useProjectState(); + const { setPeekIssue } = useIssueDetail(); + // derived values + const issueDetails: TIssueEntityData = activity.entity_data as TIssueEntityData; + const state = getStateById(issueDetails?.state); - if (!activity?.issue) return <>; - const issueDetails = getIssueById(activity?.issue); return ( { prependTitleElement={
-
- {activity.issue_detail?.name} -
-
{calculateTimeAgo(activity.updated_at)}
+
{issueDetails?.name}
+
{calculateTimeAgo(activity.visited_at)}
} quickActionElement={ -
- -
- {/* 0 ? "transparent-without-text" : "border-without-text"} - buttonClassName={activity.assignee_ids?.length > 0 ? "hover:bg-transparent px-0" : ""} - showTooltip={activity?.assignee_ids?.length === 0} - placeholder="Assignees" - optionsClassName="z-10" - tooltipContent="" - renderByDefault={isMobile} - /> */} -
+
+ + + {issueDetails?.assignees?.length > 0 && ( +
+ {}} + disabled + multiple + buttonVariant={issueDetails?.assignees?.length > 0 ? "transparent-without-text" : "border-without-text"} + buttonClassName={issueDetails?.assignees?.length > 0 ? "hover:bg-transparent px-0" : ""} + showTooltip={issueDetails?.assignees?.length === 0} + placeholder="Assignees" + optionsClassName="z-10" + tooltipContent="" + /> +
+ )}
} parentRef={ref} disableLink={false} - className="bg-transparent my-auto !px-2 border-custom-border-200/40 py-3" + className="bg-transparent my-auto !px-2 border-none py-3" itemClassName="my-auto" + onItemClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + setPeekIssue({ workspaceSlug, projectId: issueDetails?.project_id, issueId: activity.entity_data.id }); + }} /> ); }; diff --git a/web/core/components/home/widgets/recents/page.tsx b/web/core/components/home/widgets/recents/page.tsx new file mode 100644 index 00000000000..f7361f0310f --- /dev/null +++ b/web/core/components/home/widgets/recents/page.tsx @@ -0,0 +1,63 @@ +import { TActivityEntityData, TPageEntityData } from "@plane/types"; +import { Avatar, Logo } from "@plane/ui"; +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; + ref: React.RefObject; + workspaceSlug: string; +}; +export const RecentPage = (props: BlockProps) => { + const { activity, ref, workspaceSlug } = props; + // router + const router = useRouter(); + // hooks + const { getUserDetails } = useMember(); + // derived values + const pageDetails: TPageEntityData = activity.entity_data as TPageEntityData; + const ownerDetails = getUserDetails(pageDetails?.owned_by); + return ( + +
+ <> + {pageDetails?.logo_props?.in_use ? ( + + ) : ( + + )} + +
+
+ {pageDetails?.project_identifier} +
+
{pageDetails?.name}
+
{calculateTimeAgo(activity.visited_at)}
+
+ } + quickActionElement={ +
+ +
+ } + parentRef={ref} + disableLink={false} + className="bg-transparent my-auto !px-2 border-none py-3" + itemClassName="my-auto" + onItemClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + router.push(`/${workspaceSlug}/projects/${pageDetails?.project_id}/pages/${pageDetails.id}`); + }} + /> + ); +}; diff --git a/web/core/components/home/widgets/recents/project.tsx b/web/core/components/home/widgets/recents/project.tsx new file mode 100644 index 00000000000..18037d2701d --- /dev/null +++ b/web/core/components/home/widgets/recents/project.tsx @@ -0,0 +1,71 @@ +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"; + +type BlockProps = { + activity: TActivityEntityData; + ref: React.RefObject; + workspaceSlug: string; +}; +export const RecentProject = (props: BlockProps) => { + const { activity, ref, workspaceSlug } = props; + // router + const router = useRouter(); + // derived values + const projectDetails: TProjectEntityData = activity.entity_data as TProjectEntityData; + + return ( + +
+ +
+
+ {projectDetails?.identifier} +
+
{projectDetails?.name}
+
{calculateTimeAgo(activity.visited_at)}
+
+ } + quickActionElement={ +
+ {projectDetails?.project_members?.length > 0 && ( +
+ {}} + disabled + multiple + buttonVariant={ + projectDetails?.project_members?.length > 0 ? "transparent-without-text" : "border-without-text" + } + buttonClassName={projectDetails?.project_members?.length > 0 ? "hover:bg-transparent px-0" : ""} + showTooltip={projectDetails?.project_members?.length === 0} + placeholder="Assignees" + optionsClassName="z-10" + tooltipContent="" + /> +
+ )} +
+ } + parentRef={ref} + disableLink={false} + className="bg-transparent my-auto !px-2 border-none py-3" + itemClassName="my-auto" + onItemClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + router.push(`/${workspaceSlug}/projects/${projectDetails?.id}/issues`); + }} + /> + ); +}; diff --git a/web/core/hooks/use-workspace-issue-properties.ts b/web/core/hooks/use-workspace-issue-properties.ts index 4cad3a2df38..a3da0295995 100644 --- a/web/core/hooks/use-workspace-issue-properties.ts +++ b/web/core/hooks/use-workspace-issue-properties.ts @@ -4,8 +4,6 @@ import { useCycle, useProjectEstimates, useLabel, useModule, useProjectState } f export const useWorkspaceIssueProperties = (workspaceSlug: string | string[] | undefined) => { const { fetchWorkspaceLabels } = useLabel(); - const { fetchWorkspaceStates } = useProjectState(); - const { getWorkspaceEstimates } = useProjectEstimates(); const { fetchWorkspaceModules } = useModule(); @@ -33,13 +31,6 @@ export const useWorkspaceIssueProperties = (workspaceSlug: string | string[] | u { revalidateIfStale: false, revalidateOnFocus: false } ); - // fetch workspace states - useSWR( - workspaceSlug ? `WORKSPACE_STATES_${workspaceSlug}` : null, - workspaceSlug ? () => fetchWorkspaceStates(workspaceSlug.toString()) : null, - { revalidateIfStale: false, revalidateOnFocus: false } - ); - // fetch workspace estimates useSWR( workspaceSlug ? `WORKSPACE_ESTIMATES_${workspaceSlug}` : null, diff --git a/web/core/layouts/auth-layout/workspace-wrapper.tsx b/web/core/layouts/auth-layout/workspace-wrapper.tsx index ab39dd76147..87588008ea2 100644 --- a/web/core/layouts/auth-layout/workspace-wrapper.tsx +++ b/web/core/layouts/auth-layout/workspace-wrapper.tsx @@ -14,7 +14,7 @@ import { Button, setToast, TOAST_TYPE, Tooltip } from "@plane/ui"; // components import { LogoSpinner } from "@/components/common"; // hooks -import { useMember, useProject, useUser, useUserPermissions, useWorkspace } from "@/hooks/store"; +import { useMember, useProject, useProjectState, useUser, useUserPermissions, useWorkspace } from "@/hooks/store"; import { useFavorite } from "@/hooks/store/use-favorite"; import { usePlatformOS } from "@/hooks/use-platform-os"; // local @@ -48,6 +48,7 @@ export const WorkspaceAuthWrapper: FC = observer((props) const { isMobile } = usePlatformOS(); const { loader, workspaceInfoBySlug, fetchUserWorkspaceInfo, fetchUserProjectPermissions, allowPermissions } = useUserPermissions(); + const { fetchWorkspaceStates } = useProjectState(); // derived values const canPerformWorkspaceMemberActions = allowPermissions( [EUserPermissions.ADMIN, EUserPermissions.MEMBER], @@ -93,6 +94,12 @@ export const WorkspaceAuthWrapper: FC = observer((props) : null, { revalidateIfStale: false, revalidateOnFocus: false } ); + // fetch workspace states + useSWR( + workspaceSlug ? `WORKSPACE_STATES_${workspaceSlug}` : null, + workspaceSlug ? () => fetchWorkspaceStates(workspaceSlug.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } + ); // initialize the local database const { isLoading: isDBInitializing } = useSWRImmutable( diff --git a/web/core/services/workspace.service.ts b/web/core/services/workspace.service.ts index 09e27bbdd38..6c3885c4888 100644 --- a/web/core/services/workspace.service.ts +++ b/web/core/services/workspace.service.ts @@ -313,6 +313,7 @@ export class WorkspaceService extends APIService { throw error?.response?.data; }); } + async searchEntity(workspaceSlug: string, params: TSearchEntityRequestPayload): Promise { return this.get(`/api/workspaces/${workspaceSlug}/entity-search/`, { params: { @@ -325,4 +326,17 @@ export class WorkspaceService extends APIService { throw error?.response?.data; }); } + + // recents + async fetchWorkspaceRecents(workspaceSlug: string, entity_name?: string) { + return this.get(`/api/workspaces/${workspaceSlug}/recent-visits/`, { + params: { + entity_name, + }, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } } diff --git a/web/core/store/workspace/home.ts b/web/core/store/workspace/home.ts index 607ec37d01c..355bdd86a20 100644 --- a/web/core/store/workspace/home.ts +++ b/web/core/store/workspace/home.ts @@ -1,5 +1,6 @@ import { action, makeObservable, observable, runInAction } from "mobx"; import { IWorkspaceLinkStore, WorkspaceLinkStore } from "./link.store"; +import { WorkspaceService } from "@/plane-web/services"; export interface IHomeStore { // observables @@ -18,6 +19,8 @@ export class HomeStore implements IHomeStore { widgetsMap: Record = {}; // stores quickLinks: IWorkspaceLinkStore; + // services + workspaceService: WorkspaceService; constructor() { makeObservable(this, { @@ -29,6 +32,7 @@ export class HomeStore implements IHomeStore { reorderWidgets: action, }); // services + this.workspaceService = new WorkspaceService(); // stores this.quickLinks = new WorkspaceLinkStore(); @@ -69,4 +73,13 @@ export class HomeStore implements IHomeStore { // throw error; // } }; + + // fetchRecentActivity = async (workspaceSlug: string) => { + // try { + // const response = await this.workspaceService.fetchWorkspaceRecents(workspaceSlug); + // } catch (error) { + // console.error("Failed to fetch recent activity"); + // throw error; + // } + // }; } From fb9c3645bdbaa14315aeb2b2cf3db2632e35c6fa Mon Sep 17 00:00:00 2001 From: gakshita Date: Fri, 3 Jan 2025 19:55:13 +0530 Subject: [PATCH 10/18] fix: types --- packages/types/src/dashboard.d.ts | 44 ------------------ packages/types/src/home.d.ts | 46 +++++++++++++++++++ packages/types/src/index.d.ts | 1 + .../home/home-dashboard-widgets.tsx | 24 +++------- .../components/home/widgets/links/root.tsx | 6 +-- .../home/widgets/manage/widget-item.tsx | 2 +- .../home/widgets/recents/filters.tsx | 7 +-- .../components/home/widgets/recents/index.tsx | 11 +++-- 8 files changed, 67 insertions(+), 74 deletions(-) create mode 100644 packages/types/src/home.d.ts diff --git a/packages/types/src/dashboard.d.ts b/packages/types/src/dashboard.d.ts index d96523280f9..19d09bd92e8 100644 --- a/packages/types/src/dashboard.d.ts +++ b/packages/types/src/dashboard.d.ts @@ -4,11 +4,6 @@ import { TIssue } from "./issues/issue"; import { TStateGroups } from "./state"; import { TIssueRelationTypes } from "@/plane-web/types"; -export type WidgetProps = { - dashboardId: string; - workspaceSlug: string; -}; - export type TWidgetKeys = | "overview_stats" | "assigned_issues" @@ -22,7 +17,6 @@ export type TWidgetKeys = export type TIssuesListTypes = "pending" | "upcoming" | "overdue" | "completed"; // widget filters -export type TRecentActivityWidgetFilters = "all" | "projects" | "pages" | "issues"; export type TAssignedIssuesWidgetFilters = { custom_dates?: string[]; @@ -186,41 +180,3 @@ export type THomeDashboardResponse = { dashboard: TDashboard; widgets: TWidget[]; }; - -// home -type TPageEntityData = { - id: string; - name: string; - logo_props: TLogoProps; - project_id: string; - owned_by: string; - project_identifier: string; -}; - -type TProjectEntityData = { - id: string; - name: string; - logo_props: TLogoProps; - project_members: ProjectMember[]; - identifier: string; -}; - -type TIssueEntityData = { - id: string; - name: string; - state: string; - priority: TIssuePriorities; - assignees: string[]; - type: string | null; - sequence_id: number; - project_id: string; - project_identifier: string; -}; - -type TActivityEntityData = { - id: string; - entity_name: "page" | "project" | "issue"; - entity_identifier: string; - visited_at: string; - entity_data: TPageEntityData | TProjectEntityData | TIssueEntityData; -}; diff --git a/packages/types/src/home.d.ts b/packages/types/src/home.d.ts new file mode 100644 index 00000000000..1723dafd2bb --- /dev/null +++ b/packages/types/src/home.d.ts @@ -0,0 +1,46 @@ +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 THomeWidgetProps = { + workspaceSlug: string; +}; + +export type TPageEntityData = { + id: string; + name: string; + logo_props: TLogoProps; + project_id: string; + owned_by: string; + project_identifier: string; +}; + +export type TProjectEntityData = { + id: string; + name: string; + logo_props: TLogoProps; + project_members: string[]; + identifier: string; +}; + +export type TIssueEntityData = { + id: string; + name: string; + state: string; + priority: TIssuePriorities; + assignees: string[]; + type: string | null; + sequence_id: number; + project_id: string; + project_identifier: string; +}; + +export type TActivityEntityData = { + id: string; + entity_name: "page" | "project" | "issue"; + entity_identifier: string; + visited_at: string; + entity_data: TPageEntityData | TProjectEntityData | TIssueEntityData; +}; diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index 169478f6570..e26856263b9 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -40,3 +40,4 @@ export * from "./timezone"; export * from "./activity"; export * from "./epics"; export * from "./charts"; +export * from "./home"; diff --git a/web/core/components/home/home-dashboard-widgets.tsx b/web/core/components/home/home-dashboard-widgets.tsx index 1e910082cab..57940d0fca2 100644 --- a/web/core/components/home/home-dashboard-widgets.tsx +++ b/web/core/components/home/home-dashboard-widgets.tsx @@ -1,11 +1,10 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // types -import { TWidgetKeys, WidgetProps } from "@plane/types"; -// components +import { THomeWidgetKeys, THomeWidgetProps, TWidgetKeys } from "@plane/types"; // hooks -import { useDashboard, useProject } from "@/hooks/store"; import { useHome } from "@/hooks/store/use-home"; +// components import { HomePageHeader } from "@/plane-web/components/home/header"; import { RecentActivityWidget } from "./widgets"; import { DashboardQuickLinks } from "./widgets/links"; @@ -13,8 +12,9 @@ import { ManageWidgetsModal } from "./widgets/manage"; import { StickiesWidget } from "@/plane-web/components/stickies"; const WIDGETS_LIST: { - [key: string]: { component: React.FC; fullWidth: boolean }; + [key in THomeWidgetKeys]: { component: React.FC; fullWidth: boolean }; } = { + quick_links: { component: DashboardQuickLinks, fullWidth: false }, recent_activity: { component: RecentActivityWidget, fullWidth: false }, stickies: { component: StickiesWidget, fullWidth: false }, }; @@ -22,15 +22,10 @@ const WIDGETS_LIST: { export const DashboardWidgets = observer(() => { // router const { workspaceSlug } = useParams(); - const { totalProjectIds } = useProject(); // store hooks const { toggleWidgetSettings, showWidgetSettings } = useHome(); - const { homeDashboardId, homeDashboardWidgets } = useDashboard(); - - const doesWidgetExist = (widgetKey: TWidgetKeys) => - Boolean(homeDashboardWidgets?.find((widget) => widget.key === widgetKey)); - if (!workspaceSlug || !homeDashboardId) return null; + if (!workspaceSlug) return null; return (
@@ -40,21 +35,16 @@ export const DashboardWidgets = observer(() => { isModalOpen={showWidgetSettings} handleOnClose={() => toggleWidgetSettings(false)} /> - {Object.entries(WIDGETS_LIST).map(([key, widget]) => { const WidgetComponent = widget.component; - // if the widget doesn't exist, return null - // if (!doesWidgetExist(key as TWidgetKeys)) return null; - // if the widget is full width, return it in a 2 column grid if (widget.fullWidth) return (
- +
); - else - return ; + else return ; })}
); diff --git a/web/core/components/home/widgets/links/root.tsx b/web/core/components/home/widgets/links/root.tsx index 1b2b389709d..e10c642e40f 100644 --- a/web/core/components/home/widgets/links/root.tsx +++ b/web/core/components/home/widgets/links/root.tsx @@ -4,11 +4,9 @@ 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"; -type TProps = { - workspaceSlug: string; -}; -export const DashboardQuickLinks = observer((props: TProps) => { +export const DashboardQuickLinks = observer((props: THomeWidgetProps) => { const { workspaceSlug } = props; const { linkOperations } = useLinks(workspaceSlug); const { diff --git a/web/core/components/home/widgets/manage/widget-item.tsx b/web/core/components/home/widgets/manage/widget-item.tsx index d9c4523b85a..fb101a093f8 100644 --- a/web/core/components/home/widgets/manage/widget-item.tsx +++ b/web/core/components/home/widgets/manage/widget-item.tsx @@ -122,7 +122,7 @@ export const WidgetItem: FC = observer((props) => {
{widget.title}
- + {/* */}
{isLastChild && }
diff --git a/web/core/components/home/widgets/recents/filters.tsx b/web/core/components/home/widgets/recents/filters.tsx index 61edc808856..84391775b16 100644 --- a/web/core/components/home/widgets/recents/filters.tsx +++ b/web/core/components/home/widgets/recents/filters.tsx @@ -5,12 +5,13 @@ import { observer } from "mobx-react"; import { ChevronDown } from "lucide-react"; import { CustomMenu } from "@plane/ui"; import { cn } from "@plane/utils"; +import { TRecentActivityFilterKeys } from "@plane/types"; export type TFiltersDropdown = { className?: string; - activeFilter: string; - setActiveFilter: (filter: string) => void; - filters: { name: string; icon?: React.ReactNode }[]; + activeFilter: TRecentActivityFilterKeys; + setActiveFilter: (filter: TRecentActivityFilterKeys) => void; + filters: { name: TRecentActivityFilterKeys; icon?: React.ReactNode }[]; }; export const FiltersDropdown: FC = observer((props) => { diff --git a/web/core/components/home/widgets/recents/index.tsx b/web/core/components/home/widgets/recents/index.tsx index 9ba0862b312..9773f7bc7fc 100644 --- a/web/core/components/home/widgets/recents/index.tsx +++ b/web/core/components/home/widgets/recents/index.tsx @@ -2,9 +2,8 @@ import { useRef, useState } from "react"; import { observer } from "mobx-react"; -import { WidgetProps } from "@/components/dashboard/widgets"; // types -import { TActivityEntityData } from "@plane/types"; +import { TActivityEntityData, THomeWidgetProps, TRecentActivityFilterKeys } from "@plane/types"; // components import { FiltersDropdown } from "./filters"; import { RecentIssue } from "./issue"; @@ -19,17 +18,19 @@ import { EmptyWorkspace } from "../empty-states"; const WIDGET_KEY = EWidgetKeys.RECENT_ACTIVITY; const workspaceService = new WorkspaceService(); -const filters = [ +const filters: { name: TRecentActivityFilterKeys; icon?: React.ReactNode }[] = [ { name: "all item" }, { name: "issue", icon: }, { name: "page", icon: }, { name: "project", icon: }, ]; -export const RecentActivityWidget: React.FC = observer((props) => { +export const RecentActivityWidget: React.FC = observer((props) => { const { workspaceSlug } = props; + // state + const [filter, setFilter] = useState(filters[0].name); + // ref const ref = useRef(null); - const [filter, setFilter] = useState(filters[0].name); const { data: recents, isLoading } = useSWR( workspaceSlug ? `WORKSPACE_RECENT_ACTIVITY_${workspaceSlug}_${filter}` : null, From 5a1c521e62ce06828b16ab260058d80d81b7fbe4 Mon Sep 17 00:00:00 2001 From: gakshita Date: Fri, 3 Jan 2025 19:59:13 +0530 Subject: [PATCH 11/18] fix: types --- packages/types/src/dashboard.d.ts | 1 - packages/types/src/home.d.ts | 23 +++++++++++++++++++++ packages/types/src/index.d.ts | 2 -- packages/types/src/workspace/dashboard.d.ts | 4 ---- packages/types/src/workspace/index.ts | 2 -- packages/types/src/workspace/link.d.ts | 22 -------------------- 6 files changed, 23 insertions(+), 31 deletions(-) delete mode 100644 packages/types/src/workspace/dashboard.d.ts delete mode 100644 packages/types/src/workspace/index.ts delete mode 100644 packages/types/src/workspace/link.d.ts diff --git a/packages/types/src/dashboard.d.ts b/packages/types/src/dashboard.d.ts index 19d09bd92e8..96efea00706 100644 --- a/packages/types/src/dashboard.d.ts +++ b/packages/types/src/dashboard.d.ts @@ -17,7 +17,6 @@ export type TWidgetKeys = export type TIssuesListTypes = "pending" | "upcoming" | "overdue" | "completed"; // widget filters - export type TAssignedIssuesWidgetFilters = { custom_dates?: string[]; duration?: EDurationFilters; diff --git a/packages/types/src/home.d.ts b/packages/types/src/home.d.ts index 1723dafd2bb..4dc320bda20 100644 --- a/packages/types/src/home.d.ts +++ b/packages/types/src/home.d.ts @@ -44,3 +44,26 @@ export type TActivityEntityData = { visited_at: string; entity_data: TPageEntityData | TProjectEntityData | TIssueEntityData; }; + +export type TLinkEditableFields = { + title: string; + url: string; +}; + +export type TLink = TLinkEditableFields & { + created_by_id: string; + id: string; + metadata: any; + workspace_slug: string; + + //need + created_at: Date; +}; + +export type TLinkMap = { + [workspace_slug: string]: TLink; +}; + +export type TLinkIdMap = { + [workspace_slug: string]: string[]; +}; diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index e26856263b9..7a9cd8b3327 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -34,8 +34,6 @@ export * from "./favorite"; export * from "./file"; export * from "./workspace-draft-issues/base"; export * from "./command-palette"; -export * from "./workspace/link"; -export * from "./workspace/dashboard"; export * from "./timezone"; export * from "./activity"; export * from "./epics"; diff --git a/packages/types/src/workspace/dashboard.d.ts b/packages/types/src/workspace/dashboard.d.ts deleted file mode 100644 index 114891a8ff9..00000000000 --- a/packages/types/src/workspace/dashboard.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type WidgetProps = { - dashboardId: string; - workspaceSlug: string; -}; diff --git a/packages/types/src/workspace/index.ts b/packages/types/src/workspace/index.ts deleted file mode 100644 index 318c703f90e..00000000000 --- a/packages/types/src/workspace/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./link"; -export * from "./dashboard"; diff --git a/packages/types/src/workspace/link.d.ts b/packages/types/src/workspace/link.d.ts deleted file mode 100644 index 00bad1a58c8..00000000000 --- a/packages/types/src/workspace/link.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -export type TLinkEditableFields = { - title: string; - url: string; -}; - -export type TLink = TLinkEditableFields & { - created_by_id: string; - id: string; - metadata: any; - workspace_slug: string; - - //need - created_at: Date; -}; - -export type TLinkMap = { - [workspace_slug: string]: TLink; -}; - -export type TLinkIdMap = { - [workspace_slug: string]: string[]; -}; From 314f714277b04fb50727d4985e9c32e767ea7b43 Mon Sep 17 00:00:00 2001 From: gakshita Date: Mon, 6 Jan 2025 12:41:54 +0530 Subject: [PATCH 12/18] fix: added tooltips --- .../components/home/widgets/recents/issue.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/web/core/components/home/widgets/recents/issue.tsx b/web/core/components/home/widgets/recents/issue.tsx index 14095d6903c..c5f76b09303 100644 --- a/web/core/components/home/widgets/recents/issue.tsx +++ b/web/core/components/home/widgets/recents/issue.tsx @@ -1,10 +1,10 @@ import { TActivityEntityData, TIssueEntityData } from "@plane/types"; -import { PriorityIcon, StateGroupIcon } from "@plane/ui"; +import { PriorityIcon, StateGroupIcon, Tooltip } from "@plane/ui"; import { ListItem } from "@/components/core/list"; -import { calculateTimeAgo } from "@/helpers/date-time.helper"; -import { IssueIdentifier } from "@/plane-web/components/issues"; import { MemberDropdown } from "@/components/dropdowns"; +import { calculateTimeAgo } from "@/helpers/date-time.helper"; import { useIssueDetail, useProjectState } from "@/hooks/store"; +import { IssueIdentifier } from "@/plane-web/components/issues"; type BlockProps = { activity: TActivityEntityData; @@ -40,8 +40,16 @@ export const RecentIssue = (props: BlockProps) => { } quickActionElement={
- - + +
+ +
+
+ +
+ +
+
{issueDetails?.assignees?.length > 0 && (
Date: Mon, 6 Jan 2025 13:27:34 +0530 Subject: [PATCH 13/18] chore: added apis --- packages/types/src/home.d.ts | 7 ++ .../home/home-dashboard-widgets.tsx | 4 +- web/core/components/home/user-greetings.tsx | 6 +- web/core/services/workspace.service.ts | 24 +++- web/core/store/workspace/home.ts | 115 +++++++++++------- 5 files changed, 107 insertions(+), 49 deletions(-) diff --git a/packages/types/src/home.d.ts b/packages/types/src/home.d.ts index 4dc320bda20..52d057a10c2 100644 --- a/packages/types/src/home.d.ts +++ b/packages/types/src/home.d.ts @@ -67,3 +67,10 @@ export type TLinkMap = { export type TLinkIdMap = { [workspace_slug: string]: string[]; }; + +export type TWidgetEntityData = { + key: string; + 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 57940d0fca2..c7daac656f1 100644 --- a/web/core/components/home/home-dashboard-widgets.tsx +++ b/web/core/components/home/home-dashboard-widgets.tsx @@ -1,15 +1,15 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // types -import { THomeWidgetKeys, THomeWidgetProps, TWidgetKeys } from "@plane/types"; +import { THomeWidgetKeys, THomeWidgetProps } from "@plane/types"; // hooks import { useHome } from "@/hooks/store/use-home"; // components import { HomePageHeader } from "@/plane-web/components/home/header"; +import { StickiesWidget } from "@/plane-web/components/stickies"; import { RecentActivityWidget } from "./widgets"; import { DashboardQuickLinks } from "./widgets/links"; import { ManageWidgetsModal } from "./widgets/manage"; -import { StickiesWidget } from "@/plane-web/components/stickies"; const WIDGETS_LIST: { [key in THomeWidgetKeys]: { component: React.FC; fullWidth: boolean }; diff --git a/web/core/components/home/user-greetings.tsx b/web/core/components/home/user-greetings.tsx index 24c66ddacaa..c30f9cdd293 100644 --- a/web/core/components/home/user-greetings.tsx +++ b/web/core/components/home/user-greetings.tsx @@ -50,14 +50,14 @@ export const UserGreetingsView: FC = (props) => { {weekDay}, {date} {timeString}
-
{" "} - + */}
); }; diff --git a/web/core/services/workspace.service.ts b/web/core/services/workspace.service.ts index 6c3885c4888..a4d2e7fdcc7 100644 --- a/web/core/services/workspace.service.ts +++ b/web/core/services/workspace.service.ts @@ -15,6 +15,7 @@ import { TLink, TSearchResponse, TSearchEntityRequestPayload, + TWidgetEntityData, } from "@plane/types"; import { APIService } from "@/services/api.service"; // helpers @@ -306,7 +307,7 @@ export class WorkspaceService extends APIService { }); } - async deleteWorkspaceLink(workspaceSlug: string, linkId: string): Promise { + async deleteWorkspaceLink(workspaceSlug: string, linkId: string): Promise { return this.delete(`/api/workspaces/${workspaceSlug}/quick-links/${linkId}/`) .then((response) => response?.data) .catch((error) => { @@ -339,4 +340,25 @@ export class WorkspaceService extends APIService { throw error?.response; }); } + + // widgets + async fetchWorkspaceWidgets(workspaceSlug: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/home-preferences/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async updateWorkspaceWidget( + workspaceSlug: string, + widgetKey: string, + data: Partial + ): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/home-preferences/${widgetKey}/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } } diff --git a/web/core/store/workspace/home.ts b/web/core/store/workspace/home.ts index 355bdd86a20..48166bf6545 100644 --- a/web/core/store/workspace/home.ts +++ b/web/core/store/workspace/home.ts @@ -1,22 +1,28 @@ +import orderBy from "lodash/orderBy"; +import set from "lodash/set"; import { action, makeObservable, observable, runInAction } from "mobx"; -import { IWorkspaceLinkStore, WorkspaceLinkStore } from "./link.store"; +import { TWidgetEntityData } from "@plane/types"; import { WorkspaceService } from "@/plane-web/services"; +import { IWorkspaceLinkStore, WorkspaceLinkStore } from "./link.store"; export interface IHomeStore { // observables showWidgetSettings: boolean; - widgetsMap: Record; + widgetsMap: Record; //stores quickLinks: IWorkspaceLinkStore; // actions toggleWidgetSettings: (value?: boolean) => void; - reorderWidgets: (workspaceSlug: string, widgetId: string, destinationId: string, edge: string | undefined) => void; + fetchWidgets: (workspaceSlug: string) => Promise; + reorderWidget: (workspaceSlug: string, widgetKey: string, destinationId: string, edge: string | undefined) => void; + toggleWidget: (workspaceSlug: string, widgetKey: string, is_enabled: boolean) => void; } export class HomeStore implements IHomeStore { // observables showWidgetSettings = false; - widgetsMap: Record = {}; + widgetsMap: Record = {}; + widgets: string[] = []; // stores quickLinks: IWorkspaceLinkStore; // services @@ -27,9 +33,12 @@ export class HomeStore implements IHomeStore { // observables showWidgetSettings: observable, widgetsMap: observable, + widgets: observable, // actions toggleWidgetSettings: action, - reorderWidgets: action, + fetchWidgets: action, + reorderWidget: action, + toggleWidget: action, }); // services this.workspaceService = new WorkspaceService(); @@ -42,44 +51,64 @@ export class HomeStore implements IHomeStore { this.showWidgetSettings = value !== undefined ? value : !this.showWidgetSettings; }; - reorderWidgets = async (workspaceSlug: string, widgetId: string, destinationId: string, edge: string | undefined) => { - // try { - // let resultSequence = 10000; - // if (edge) { - // const sortedIds = orderBy(Object.values(this.favoriteMap), "sequence", "desc").map((fav) => fav.id); - // const destinationSequence = this.favoriteMap[destinationId]?.sequence || undefined; - // if (destinationSequence) { - // const destinationIndex = sortedIds.findIndex((id) => id === destinationId); - // if (edge === "reorder-above") { - // const prevSequence = this.favoriteMap[sortedIds[destinationIndex - 1]]?.sequence || undefined; - // if (prevSequence) { - // resultSequence = (destinationSequence + prevSequence) / 2; - // } else { - // resultSequence = destinationSequence + resultSequence; - // } - // } else { - // resultSequence = destinationSequence - resultSequence; - // } - // } - // } - // await this.dashboardService.updateDashboardWidget(workspaceSlug, dashboardId, widgetId, { - // sequence: resultSequence, - // }); - // runInAction(() => { - // set(this.favoriteMap, [favoriteId, "sequence"], resultSequence); - // }); - // } catch (error) { - // console.error("Failed to move favorite folder"); - // throw error; - // } + fetchWidgets = async (workspaceSlug: string) => { + try { + const widgets = await this.workspaceService.fetchWorkspaceWidgets(workspaceSlug); + runInAction(() => { + this.widgets = widgets.map((widget) => widget.key); + widgets.forEach((widget) => { + this.widgetsMap[widget.key] = widget; + }); + }); + } catch (error) { + console.error("Failed to fetch widgets"); + throw error; + } }; - // fetchRecentActivity = async (workspaceSlug: string) => { - // try { - // const response = await this.workspaceService.fetchWorkspaceRecents(workspaceSlug); - // } catch (error) { - // console.error("Failed to fetch recent activity"); - // throw error; - // } - // }; + toggleWidget = async (workspaceSlug: string, widgetKey: string, is_enabled: boolean) => { + try { + await this.workspaceService.updateWorkspaceWidget(workspaceSlug, widgetKey, { + is_enabled, + }); + runInAction(() => { + this.widgetsMap[widgetKey].is_enabled = is_enabled; + }); + } catch (error) { + console.error("Failed to toggle widget"); + throw error; + } + }; + + reorderWidget = async (workspaceSlug: string, widgetKey: string, destinationId: string, edge: string | undefined) => { + try { + let resultSequence = 10000; + if (edge) { + const sortedIds = orderBy(Object.values(this.widgetsMap), "sort_order", "desc").map((widget) => widget.key); + const destinationSequence = this.widgetsMap[destinationId]?.sort_order || undefined; + if (destinationSequence) { + const destinationIndex = sortedIds.findIndex((id) => id === destinationId); + if (edge === "reorder-above") { + const prevSequence = this.widgetsMap[sortedIds[destinationIndex - 1]]?.sort_order || undefined; + if (prevSequence) { + resultSequence = (destinationSequence + prevSequence) / 2; + } else { + resultSequence = destinationSequence + resultSequence; + } + } else { + resultSequence = destinationSequence - resultSequence; + } + } + } + await this.workspaceService.updateWorkspaceWidget(workspaceSlug, widgetKey, { + sort_order: resultSequence, + }); + runInAction(() => { + set(this.widgetsMap, [widgetKey, "sort_order"], resultSequence); + }); + } catch (error) { + console.error("Failed to move widget"); + throw error; + } + }; } From 94a9b000082d2302d4802feb2af45801eeae884f Mon Sep 17 00:00:00 2001 From: gakshita Date: Mon, 6 Jan 2025 13:53:56 +0530 Subject: [PATCH 14/18] fix: widgets fix --- .../home/widgets/manage/widget-item.tsx | 22 ++++++++++++------- .../home/widgets/manage/widget-list.tsx | 22 ++++++++++--------- .../home/widgets/manage/widget.helpers.ts | 16 +++++++------- web/core/store/workspace/home.ts | 1 + 4 files changed, 35 insertions(+), 26 deletions(-) diff --git a/web/core/components/home/widgets/manage/widget-item.tsx b/web/core/components/home/widgets/manage/widget-item.tsx index fb101a093f8..03fad6df0a4 100644 --- a/web/core/components/home/widgets/manage/widget-item.tsx +++ b/web/core/components/home/widgets/manage/widget-item.tsx @@ -14,9 +14,10 @@ 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 @@ -26,13 +27,15 @@ import { getCanDrop, getInstructionFromPayload } from "./widget.helpers"; type Props = { isLastChild: boolean; - widget: any; + widget: TWidgetEntityData; 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 { isLastChild, widget, handleDrop, handleToggle } = props; + const { workspace_slug } = useParams(); //state const [isDragging, setIsDragging] = useState(false); const [instruction, setInstruction] = useState(undefined); @@ -45,7 +48,7 @@ export const WidgetItem: FC = observer((props) => { 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 +65,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 +107,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 +123,12 @@ export const WidgetItem: FC = observer((props) => { >
-
{widget.title}
+
{widget.name}
- {/* */} + handleToggle(workspace_slug.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 485f6891b21..6422e74bd7a 100644 --- a/web/core/components/home/widgets/manage/widget-list.tsx +++ b/web/core/components/home/widgets/manage/widget-list.tsx @@ -3,17 +3,18 @@ import { DropTargetRecord, ElementDragPayload, } from "@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types"; +import { useHome } from "@/hooks/store/use-home"; import { WidgetItem } from "./widget-item"; import { getInstructionFromPayload, TargetData } from "./widget.helpers"; -import { useHome } from "@/hooks/store/use-home"; -const WIDGETS_LIST = [ - { id: 1, title: "quick links" }, - { id: 2, title: "recents" }, - { id: 3, title: "stickies" }, +// TODO: Replace with api data +const widgets = [ + { key: "1", name: "quick links", is_enabled: true, sort_order: 1 }, + { key: "2", name: "recents", is_enabled: true, sort_order: 2 }, + { key: "3", name: "stickies", is_enabled: true, sort_order: 3 }, ]; export const WidgetList = ({ workspaceSlug }: { workspaceSlug: string }) => { - const { reorderWidgets } = useHome(); + const { reorderWidget, toggleWidget } = useHome(); const handleDrop = (self: DropTargetRecord, source: ElementDragPayload, location: DragLocationHistory) => { const dropTargets = location?.current?.dropTargets ?? []; @@ -30,18 +31,19 @@ export const WidgetList = ({ workspaceSlug }: { workspaceSlug: string }) => { if (!sourceData.id) return; if (droppedId) { - reorderWidgets(workspaceSlug, sourceData.id, droppedId, instruction); /** sequence */ + reorderWidget(workspaceSlug, sourceData.id, droppedId, instruction); /** sequence */ } }; return (
- {WIDGETS_LIST.map((widget, index) => ( + {widgets.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/store/workspace/home.ts b/web/core/store/workspace/home.ts index 48166bf6545..34c2e0c5a28 100644 --- a/web/core/store/workspace/home.ts +++ b/web/core/store/workspace/home.ts @@ -9,6 +9,7 @@ export interface IHomeStore { // observables showWidgetSettings: boolean; widgetsMap: Record; + widgets: string[]; //stores quickLinks: IWorkspaceLinkStore; // actions From 92607d2459494a7b1bdf234e5e8fbbb501c94337 Mon Sep 17 00:00:00 2001 From: gakshita Date: Mon, 6 Jan 2025 14:22:19 +0530 Subject: [PATCH 15/18] fix: lint --- web/core/components/home/widgets/links/links.tsx | 2 +- web/core/components/home/widgets/links/root.tsx | 2 +- .../components/home/widgets/loaders/loader.tsx | 2 +- .../components/home/widgets/recents/filters.tsx | 2 +- web/core/components/home/widgets/recents/index.tsx | 14 +++++++------- web/core/components/home/widgets/recents/page.tsx | 6 +++--- .../components/home/widgets/recents/project.tsx | 4 ++-- web/core/components/issues/issue-modal/base.tsx | 2 +- 8 files changed, 17 insertions(+), 17 deletions(-) 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/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/components/issues/issue-modal/base.tsx b/web/core/components/issues/issue-modal/base.tsx index a54bf81f6be..090cb41d399 100644 --- a/web/core/components/issues/issue-modal/base.tsx +++ b/web/core/components/issues/issue-modal/base.tsx @@ -3,9 +3,9 @@ import React, { useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; import { useParams, usePathname } from "next/navigation"; +import { EIssuesStoreType } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // types -import { EIssuesStoreType } from "@plane/constants"; import type { TBaseIssue, TIssue } from "@plane/types"; // ui import { EModalPosition, EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@plane/ui"; From e9cd53826ae124ab211b1166138c3b5d55b98875 Mon Sep 17 00:00:00 2001 From: gakshita Date: Mon, 6 Jan 2025 19:46:28 +0530 Subject: [PATCH 16/18] fix: integrated dashboard apis --- packages/types/src/home.d.ts | 4 +- .../home/home-dashboard-widgets.tsx | 18 +++++---- web/core/components/home/root.tsx | 27 +++++++------- web/core/components/home/user-greetings.tsx | 4 +- .../home/widgets/manage/widget-item.tsx | 16 +++++--- .../home/widgets/manage/widget-list.tsx | 37 ++++++++++++------- web/core/store/workspace/home.ts | 21 ++++++++--- 7 files changed, 78 insertions(+), 49 deletions(-) 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/manage/widget-item.tsx b/web/core/components/home/widgets/manage/widget-item.tsx index 03fad6df0a4..453a96588c0 100644 --- a/web/core/components/home/widgets/manage/widget-item.tsx +++ b/web/core/components/home/widgets/manage/widget-item.tsx @@ -22,26 +22,30 @@ import { InstructionType, TWidgetEntityData } from "@plane/types"; 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: TWidgetEntityData; 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, handleToggle } = props; - const { workspace_slug } = useParams(); + 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(() => { @@ -123,11 +127,11 @@ export const WidgetItem: FC = observer((props) => { >
-
{widget.name}
+
{widget.key.replaceAll("_", " ")}
handleToggle(workspace_slug.toString(), widget.key, !widget.is_enabled)} + onChange={() => 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 6422e74bd7a..526d51ba913 100644 --- a/web/core/components/home/widgets/manage/widget-list.tsx +++ b/web/core/components/home/widgets/manage/widget-list.tsx @@ -3,18 +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"; -// TODO: Replace with api data -const widgets = [ - { key: "1", name: "quick links", is_enabled: true, sort_order: 1 }, - { key: "2", name: "recents", is_enabled: true, sort_order: 2 }, - { key: "3", name: "stickies", is_enabled: true, sort_order: 3 }, -]; -export const WidgetList = ({ workspaceSlug }: { workspaceSlug: string }) => { - const { reorderWidget, toggleWidget } = 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 ?? []; @@ -31,21 +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.map((widget, index) => ( + {orderedWidgets.map((widget, index) => ( ))}
); -}; +}); diff --git a/web/core/store/workspace/home.ts b/web/core/store/workspace/home.ts index 34c2e0c5a28..137e53fb004 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,7 +9,9 @@ export interface IHomeStore { // observables showWidgetSettings: boolean; widgetsMap: Record; - widgets: string[]; + widgets: THomeWidgetKeys[]; + // computed + orderedWidgets: THomeWidgetKeys[]; //stores quickLinks: IWorkspaceLinkStore; // actions @@ -23,7 +25,7 @@ export class HomeStore implements IHomeStore { // observables showWidgetSettings = false; widgetsMap: Record = {}; - widgets: string[] = []; + widgets: THomeWidgetKeys[] = []; // stores quickLinks: IWorkspaceLinkStore; // services @@ -35,6 +37,8 @@ export class HomeStore implements IHomeStore { showWidgetSettings: observable, widgetsMap: observable, widgets: observable, + // computed + orderedWidgets: computed, // actions toggleWidgetSettings: action, fetchWidgets: action, @@ -48,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; }; @@ -56,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; }); @@ -82,11 +90,14 @@ export class HomeStore implements IHomeStore { }; reorderWidget = async (workspaceSlug: string, widgetKey: string, destinationId: string, edge: string | undefined) => { + console.log("edge", edge); try { let resultSequence = 10000; + console.log("edge", edge); if (edge) { const sortedIds = orderBy(Object.values(this.widgetsMap), "sort_order", "desc").map((widget) => widget.key); const destinationSequence = this.widgetsMap[destinationId]?.sort_order || undefined; + console.log("destinationSequence", destinationSequence); if (destinationSequence) { const destinationIndex = sortedIds.findIndex((id) => id === destinationId); if (edge === "reorder-above") { From 062d2fe1c8de11f985b565b1e6f2c05e912bd942 Mon Sep 17 00:00:00 2001 From: gakshita Date: Mon, 6 Jan 2025 21:21:30 +0530 Subject: [PATCH 17/18] fix: added ee dummy component for header --- web/ee/components/home/header.tsx | 1 + 1 file changed, 1 insertion(+) create mode 100644 web/ee/components/home/header.tsx 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 = () => <>; From 4bea812a3cec00be197eae9ca2148310cec1e95f Mon Sep 17 00:00:00 2001 From: gakshita Date: Tue, 7 Jan 2025 12:48:36 +0530 Subject: [PATCH 18/18] fix: removed logs --- web/core/store/workspace/home.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/web/core/store/workspace/home.ts b/web/core/store/workspace/home.ts index 137e53fb004..4867e5f5bc2 100644 --- a/web/core/store/workspace/home.ts +++ b/web/core/store/workspace/home.ts @@ -90,14 +90,11 @@ export class HomeStore implements IHomeStore { }; reorderWidget = async (workspaceSlug: string, widgetKey: string, destinationId: string, edge: string | undefined) => { - console.log("edge", edge); try { let resultSequence = 10000; - console.log("edge", edge); if (edge) { const sortedIds = orderBy(Object.values(this.widgetsMap), "sort_order", "desc").map((widget) => widget.key); const destinationSequence = this.widgetsMap[destinationId]?.sort_order || undefined; - console.log("destinationSequence", destinationSequence); if (destinationSequence) { const destinationIndex = sortedIds.findIndex((id) => id === destinationId); if (edge === "reorder-above") {