diff --git a/packages/types/src/issues/issue.d.ts b/packages/types/src/issues/issue.d.ts index a9d8970f90a..01c0b2f3a58 100644 --- a/packages/types/src/issues/issue.d.ts +++ b/packages/types/src/issues/issue.d.ts @@ -1,4 +1,4 @@ -import { EIssueServiceType } from "@plane/constants"; +import { EIssueServiceType, EIssuesStoreType } from "@plane/constants"; import { TIssuePriorities } from "../issues"; import { TIssueAttachment } from "./issue_attachment"; import { TIssueLink } from "./issue_link"; @@ -181,3 +181,10 @@ export type TPublicIssuesResponse = { extra_stats: null; results: TPublicIssueResponseResults; }; + +export interface IWorkItemPeekOverview { + embedIssue?: boolean; + embedRemoveCurrentNotification?: () => void; + is_draft?: boolean; + storeType?: EIssuesStoreType; +} \ No newline at end of file diff --git a/web/app/(all)/[workspaceSlug]/(projects)/notifications/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/notifications/page.tsx index 8afe768d80c..415ea8fbf71 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/notifications/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/notifications/page.tsx @@ -1,22 +1,14 @@ "use client"; -import { useCallback, useEffect } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -import useSWR from "swr"; // plane imports -import { ENotificationLoader, ENotificationQueryParamType } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // components -import { LogoSpinner } from "@/components/common"; import { PageHead } from "@/components/core"; -import { SimpleEmptyState } from "@/components/empty-state"; -import { InboxContentRoot } from "@/components/inbox"; -import { IssuePeekOverview } from "@/components/issues"; +import { NotificationsRoot } from "@/components/workspace-notifications"; // hooks -import { useIssueDetail, useUserPermissions, useWorkspace, useWorkspaceNotifications } from "@/hooks/store"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; -import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties"; +import { useWorkspace } from "@/hooks/store"; const WorkspaceDashboardPage = observer(() => { const { workspaceSlug } = useParams(); @@ -24,98 +16,15 @@ const WorkspaceDashboardPage = observer(() => { const { t } = useTranslation(); // hooks const { currentWorkspace } = useWorkspace(); - const { - currentSelectedNotificationId, - setCurrentSelectedNotificationId, - notificationLiteByNotificationId, - notificationIdsByWorkspaceId, - getNotifications, - } = useWorkspaceNotifications(); - const { fetchUserProjectInfo } = useUserPermissions(); - const { setPeekIssue } = useIssueDetail(); // derived values const pageTitle = currentWorkspace?.name ? t("notification.page_label", { workspace: currentWorkspace?.name }) : undefined; - const { workspace_slug, project_id, issue_id, is_inbox_issue } = - notificationLiteByNotificationId(currentSelectedNotificationId); - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/intake/issue-detail" }); - - // fetching workspace work item properties - useWorkspaceIssueProperties(workspaceSlug); - - // fetch workspace notifications - const notificationMutation = - currentWorkspace && notificationIdsByWorkspaceId(currentWorkspace.id) - ? ENotificationLoader.MUTATION_LOADER - : ENotificationLoader.INIT_LOADER; - const notificationLoader = - currentWorkspace && notificationIdsByWorkspaceId(currentWorkspace.id) - ? ENotificationQueryParamType.CURRENT - : ENotificationQueryParamType.INIT; - useSWR( - currentWorkspace?.slug ? `WORKSPACE_NOTIFICATION` : null, - currentWorkspace?.slug - ? () => getNotifications(currentWorkspace?.slug, notificationMutation, notificationLoader) - : null - ); - - // fetching user project member info - const { isLoading: projectMemberInfoLoader } = useSWR( - workspace_slug && project_id && is_inbox_issue - ? `PROJECT_MEMBER_PERMISSION_INFO_${workspace_slug}_${project_id}` - : null, - workspace_slug && project_id && is_inbox_issue ? () => fetchUserProjectInfo(workspace_slug, project_id) : null - ); - - const embedRemoveCurrentNotification = useCallback( - () => setCurrentSelectedNotificationId(undefined), - [setCurrentSelectedNotificationId] - ); - - // clearing up the selected notifications when unmounting the page - useEffect( - () => () => { - setCurrentSelectedNotificationId(undefined); - setPeekIssue(undefined); - }, - [setCurrentSelectedNotificationId, setPeekIssue] - ); return ( <> -
- {!currentSelectedNotificationId ? ( -
- -
- ) : ( - <> - {is_inbox_issue === true && workspace_slug && project_id && issue_id ? ( - <> - {projectMemberInfoLoader ? ( -
- -
- ) : ( - {}} - isMobileSidebar={false} - workspaceSlug={workspace_slug} - projectId={project_id} - inboxIssueId={issue_id} - isNotificationEmbed - embedRemoveCurrentNotification={embedRemoveCurrentNotification} - /> - )} - - ) : ( - - )} - - )} -
+ ); }); diff --git a/web/ce/components/workspace-notifications/index.ts b/web/ce/components/workspace-notifications/index.ts index 18c4afa968e..c12683ce617 100644 --- a/web/ce/components/workspace-notifications/index.ts +++ b/web/ce/components/workspace-notifications/index.ts @@ -1 +1,2 @@ export * from "./notification-card/root"; +export * from "./list-root"; diff --git a/web/ce/components/workspace-notifications/list-root.tsx b/web/ce/components/workspace-notifications/list-root.tsx new file mode 100644 index 00000000000..55fd68c3ff0 --- /dev/null +++ b/web/ce/components/workspace-notifications/list-root.tsx @@ -0,0 +1,8 @@ +import { NotificationCardListRoot } from "./notification-card/root"; + +export type TNotificationListRoot = { + workspaceSlug: string; + workspaceId: string; +}; + +export const NotificationListRoot = (props: TNotificationListRoot) => ; diff --git a/web/ce/hooks/use-notification-preview.tsx b/web/ce/hooks/use-notification-preview.tsx new file mode 100644 index 00000000000..b0c18d5542d --- /dev/null +++ b/web/ce/hooks/use-notification-preview.tsx @@ -0,0 +1,25 @@ +import { EIssueServiceType } from "@plane/constants"; +import { IWorkItemPeekOverview } from "@plane/types"; +import { IssuePeekOverview } from "@/components/issues"; +import { useIssueDetail } from "@/hooks/store"; +import { TPeekIssue } from "@/store/issue/issue-details/root.store"; + +export type TNotificationPreview = { + isWorkItem: boolean; + PeekOverviewComponent: React.ComponentType; + setPeekWorkItem: (peekIssue: TPeekIssue | undefined) => void; +}; + +/** + * This function returns if the current active notification is related to work item or an epic. + * @returns isWorkItem: boolean, peekOverviewComponent: IWorkItemPeekOverview, setPeekWorkItem + */ +export const useNotificationPreview = (): TNotificationPreview => { + const { peekIssue, setPeekIssue } = useIssueDetail(EIssueServiceType.ISSUES); + + return { + isWorkItem: Boolean(peekIssue), + PeekOverviewComponent: IssuePeekOverview, + setPeekWorkItem: setPeekIssue, + }; +}; diff --git a/web/core/components/issues/issue-detail/subscription.tsx b/web/core/components/issues/issue-detail/subscription.tsx index e19e787f398..8239d08ac0a 100644 --- a/web/core/components/issues/issue-detail/subscription.tsx +++ b/web/core/components/issues/issue-detail/subscription.tsx @@ -5,7 +5,7 @@ import isNil from "lodash/isNil"; import { observer } from "mobx-react"; import { Bell, BellOff } from "lucide-react"; // plane-i18n -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; +import { EUserPermissions, EUserPermissionsLevel, EIssueServiceType } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // UI import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; @@ -16,17 +16,18 @@ export type TIssueSubscription = { workspaceSlug: string; projectId: string; issueId: string; + serviceType?: EIssueServiceType; }; export const IssueSubscription: FC = observer((props) => { - const { workspaceSlug, projectId, issueId } = props; + const { workspaceSlug, projectId, issueId, serviceType = EIssueServiceType.ISSUES } = props; const { t } = useTranslation(); // hooks const { subscription: { getSubscriptionByIssueId }, createSubscription, removeSubscription, - } = useIssueDetail(); + } = useIssueDetail(serviceType); // state const [loading, setLoading] = useState(false); // hooks @@ -53,12 +54,12 @@ export const IssueSubscription: FC = observer((props) => { : t("issue.subscription.actions.subscribed"), }); setLoading(false); - } catch (error) { + } catch { setLoading(false); setToast({ type: TOAST_TYPE.ERROR, title: t("toast.error"), - message: t("commons.error.message"), + message: t("common.error.message"), }); } }; @@ -78,7 +79,7 @@ export const IssueSubscription: FC = observer((props) => { variant="outline-primary" className="hover:!bg-custom-primary-100/20" onClick={handleSubscription} - disabled={!isEditable} + disabled={!isEditable || loading} > {loading ? ( diff --git a/web/core/components/issues/peek-overview/root.tsx b/web/core/components/issues/peek-overview/root.tsx index 5ed6403bcb9..9d6653440ef 100644 --- a/web/core/components/issues/peek-overview/root.tsx +++ b/web/core/components/issues/peek-overview/root.tsx @@ -14,7 +14,7 @@ import { EUserPermissionsLevel, } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { TIssue } from "@plane/types"; +import { TIssue, IWorkItemPeekOverview } from "@plane/types"; // plane ui import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; // components @@ -24,14 +24,8 @@ import { IssueView, TIssueOperations } from "@/components/issues"; import { useEventTracker, useIssueDetail, useIssues, useUserPermissions } from "@/hooks/store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; -interface IIssuePeekOverview { - embedIssue?: boolean; - embedRemoveCurrentNotification?: () => void; - is_draft?: boolean; - storeType?: EIssuesStoreType; -} -export const IssuePeekOverview: FC = observer((props) => { +export const IssuePeekOverview: FC = observer((props) => { const { embedIssue = false, embedRemoveCurrentNotification, diff --git a/web/core/components/workspace-notifications/root.tsx b/web/core/components/workspace-notifications/root.tsx index 37623a6137a..6b41a9cea33 100644 --- a/web/core/components/workspace-notifications/root.tsx +++ b/web/core/components/workspace-notifications/root.tsx @@ -1,116 +1,116 @@ "use client"; -import { FC, useCallback } from "react"; +import { useCallback, useEffect } from "react"; import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; +import useSWR from "swr"; // plane imports -import { NOTIFICATION_TABS, TNotificationTab } from "@plane/constants"; +import { ENotificationLoader, ENotificationQueryParamType } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // components -import { Header, Row, ERowVariant, EHeaderVariant, ContentWrapper } from "@plane/ui"; -import { CountChip } from "@/components/common"; -import { - NotificationsLoader, - NotificationEmptyState, - NotificationSidebarHeader, - AppliedFilters, -} from "@/components/workspace-notifications"; -// helpers -import { cn } from "@/helpers/common.helper"; -import { getNumberCount } from "@/helpers/string.helper"; +import { cn } from "@plane/utils"; +import { LogoSpinner } from "@/components/common"; +import { SimpleEmptyState } from "@/components/empty-state"; +import { InboxContentRoot } from "@/components/inbox"; // hooks -import { useWorkspace, useWorkspaceNotifications } from "@/hooks/store"; -import { NotificationCardListRoot } from "@/plane-web/components/workspace-notifications"; -export const NotificationsSidebarRoot: FC = observer(() => { - const { workspaceSlug } = useParams(); +import { useUserPermissions, useWorkspace, useWorkspaceNotifications } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties"; +import { useNotificationPreview } from "@/plane-web/hooks/use-notification-preview"; + +type NotificationsRootProps = { + workspaceSlug?: string; +}; + +export const NotificationsRoot = observer(({ workspaceSlug }: NotificationsRootProps) => { + // plane hooks + const { t } = useTranslation(); // hooks - const { getWorkspaceBySlug } = useWorkspace(); + const { currentWorkspace } = useWorkspace(); const { currentSelectedNotificationId, - unreadNotificationsCount, - loader, + setCurrentSelectedNotificationId, + notificationLiteByNotificationId, notificationIdsByWorkspaceId, - currentNotificationTab, - setCurrentNotificationTab, + getNotifications, } = useWorkspaceNotifications(); - - const { t } = useTranslation(); + const { fetchUserProjectInfo } = useUserPermissions(); + const { isWorkItem, PeekOverviewComponent, setPeekWorkItem } = useNotificationPreview(); // derived values - const workspace = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString()) : undefined; - const notificationIds = workspace ? notificationIdsByWorkspaceId(workspace.id) : undefined; + const { workspace_slug, project_id, issue_id, is_inbox_issue } = + notificationLiteByNotificationId(currentSelectedNotificationId); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/intake/issue-detail" }); - const handleTabClick = useCallback( - (tabValue: TNotificationTab) => { - if (currentNotificationTab !== tabValue) { - setCurrentNotificationTab(tabValue); - } - }, - [currentNotificationTab, setCurrentNotificationTab] - ); + // fetching workspace work item properties + useWorkspaceIssueProperties(workspaceSlug); - if (!workspaceSlug || !workspace) return <>; + // fetch workspace notifications + const notificationMutation = + currentWorkspace && notificationIdsByWorkspaceId(currentWorkspace.id) + ? ENotificationLoader.MUTATION_LOADER + : ENotificationLoader.INIT_LOADER; + const notificationLoader = + currentWorkspace && notificationIdsByWorkspaceId(currentWorkspace.id) + ? ENotificationQueryParamType.CURRENT + : ENotificationQueryParamType.INIT; + useSWR( + currentWorkspace?.slug ? `WORKSPACE_NOTIFICATION_${currentWorkspace?.slug}` : null, + currentWorkspace?.slug + ? () => getNotifications(currentWorkspace?.slug, notificationMutation, notificationLoader) + : null + ); - return ( -
-
- - - + // fetching user project member info + const { isLoading: projectMemberInfoLoader } = useSWR( + workspace_slug && project_id && is_inbox_issue + ? `PROJECT_MEMBER_PERMISSION_INFO_${workspace_slug}_${project_id}` + : null, + workspace_slug && project_id && is_inbox_issue ? () => fetchUserProjectInfo(workspace_slug, project_id) : null + ); -
- {NOTIFICATION_TABS.map((tab) => ( -
handleTabClick(tab.value)} - > -
-
{t(tab.i18n_label)}
- {tab.count(unreadNotificationsCount) > 0 && ( - - )} -
- {currentNotificationTab === tab.value && ( -
- )} -
- ))} -
+ const embedRemoveCurrentNotification = useCallback( + () => setCurrentSelectedNotificationId(undefined), + [setCurrentSelectedNotificationId] + ); - {/* applied filters */} - + // clearing up the selected notifications when unmounting the page + useEffect( + () => () => { + setPeekWorkItem(undefined); + }, + [setCurrentSelectedNotificationId, setPeekWorkItem] + ); - {/* rendering notifications */} - {loader === "init-loader" ? ( -
- -
- ) : ( - <> - {notificationIds && notificationIds.length > 0 ? ( - - - - ) : ( -
- -
- )} - - )} -
+ return ( +
+ {!currentSelectedNotificationId ? ( +
+ +
+ ) : ( + <> + {is_inbox_issue === true && workspace_slug && project_id && issue_id ? ( + <> + {projectMemberInfoLoader ? ( +
+ +
+ ) : ( + {}} + isMobileSidebar={false} + workspaceSlug={workspace_slug} + projectId={project_id} + inboxIssueId={issue_id} + isNotificationEmbed + embedRemoveCurrentNotification={embedRemoveCurrentNotification} + /> + )} + + ) : ( + + )} + + )}
); }); diff --git a/web/core/components/workspace-notifications/sidebar/header/root.tsx b/web/core/components/workspace-notifications/sidebar/header/root.tsx index c641bde199c..cdda51fbfa6 100644 --- a/web/core/components/workspace-notifications/sidebar/header/root.tsx +++ b/web/core/components/workspace-notifications/sidebar/header/root.tsx @@ -20,7 +20,7 @@ export const NotificationSidebarHeader: FC = observe if (!workspaceSlug) return <>; return ( -
+
diff --git a/web/core/components/workspace-notifications/sidebar/index.ts b/web/core/components/workspace-notifications/sidebar/index.ts index 9e46ed0e638..4713a9b3c23 100644 --- a/web/core/components/workspace-notifications/sidebar/index.ts +++ b/web/core/components/workspace-notifications/sidebar/index.ts @@ -6,3 +6,5 @@ export * from "./header"; export * from "./filters"; export * from "./notification-card"; + +export * from "./root"; diff --git a/web/core/components/workspace-notifications/sidebar/root.tsx b/web/core/components/workspace-notifications/sidebar/root.tsx new file mode 100644 index 00000000000..48b94bb1a0d --- /dev/null +++ b/web/core/components/workspace-notifications/sidebar/root.tsx @@ -0,0 +1,118 @@ +"use client"; +import { FC, useCallback } from "react"; + +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +// plane imports +import { NOTIFICATION_TABS, TNotificationTab } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +// components +import { Header, Row, ERowVariant, EHeaderVariant, ContentWrapper } from "@plane/ui"; +import { CountChip } from "@/components/common"; +import { + NotificationsLoader, + NotificationEmptyState, + NotificationSidebarHeader, + AppliedFilters, +} from "@/components/workspace-notifications"; +// helpers +import { cn } from "@/helpers/common.helper"; +import { getNumberCount } from "@/helpers/string.helper"; +// hooks +import { useWorkspace, useWorkspaceNotifications } from "@/hooks/store"; + +import { NotificationListRoot } from "@/plane-web/components/workspace-notifications/list-root"; + +export const NotificationsSidebarRoot: FC = observer(() => { + const { workspaceSlug } = useParams(); + // hooks + const { getWorkspaceBySlug } = useWorkspace(); + const { + currentSelectedNotificationId, + unreadNotificationsCount, + loader, + notificationIdsByWorkspaceId, + currentNotificationTab, + setCurrentNotificationTab, + } = useWorkspaceNotifications(); + + const { t } = useTranslation(); + // derived values + const workspace = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString()) : undefined; + const notificationIds = workspace ? notificationIdsByWorkspaceId(workspace.id) : undefined; + + const handleTabClick = useCallback( + (tabValue: TNotificationTab) => { + if (currentNotificationTab !== tabValue) { + setCurrentNotificationTab(tabValue); + } + }, + [currentNotificationTab, setCurrentNotificationTab] + ); + + if (!workspaceSlug || !workspace) return <>; + + return ( +
+
+ + + + +
+ {NOTIFICATION_TABS.map((tab) => ( +
handleTabClick(tab.value)} + > +
+
{t(tab.i18n_label)}
+ {tab.count(unreadNotificationsCount) > 0 && ( + + )} +
+ {currentNotificationTab === tab.value && ( +
+ )} +
+ ))} +
+ + {/* applied filters */} + + + {/* rendering notifications */} + {loader === "init-loader" ? ( +
+ +
+ ) : ( + <> + {notificationIds && notificationIds.length > 0 ? ( + + + + ) : ( +
+ +
+ )} + + )} +
+
+ ); +}); diff --git a/web/core/store/issue/issue-details/root.store.ts b/web/core/store/issue/issue-details/root.store.ts index 5a3b394b04d..1d795dd9409 100644 --- a/web/core/store/issue/issue-details/root.store.ts +++ b/web/core/store/issue/issue-details/root.store.ts @@ -204,7 +204,7 @@ export class IssueDetail implements IIssueDetail { this.commentReaction = new IssueCommentReactionStore(this); this.subIssues = new IssueSubIssuesStore(this, serviceType); this.link = new IssueLinkStore(this, serviceType); - this.subscription = new IssueSubscriptionStore(this); + this.subscription = new IssueSubscriptionStore(this, serviceType); this.relation = new IssueRelationStore(this); } diff --git a/web/core/store/issue/issue-details/subscription.store.ts b/web/core/store/issue/issue-details/subscription.store.ts index 69b685c2316..da3b2e6bfaa 100644 --- a/web/core/store/issue/issue-details/subscription.store.ts +++ b/web/core/store/issue/issue-details/subscription.store.ts @@ -1,10 +1,10 @@ import set from "lodash/set"; import { action, makeObservable, observable, runInAction } from "mobx"; // services +import { EIssueServiceType } from "@plane/constants"; import { IssueService } from "@/services/issue/issue.service"; // types import { IIssueDetail } from "./root.store"; - export interface IIssueSubscriptionStoreActions { addSubscription: (issueId: string, isSubscribed: boolean | undefined | null) => void; fetchSubscriptions: (workspaceSlug: string, projectId: string, issueId: string) => Promise; @@ -27,7 +27,7 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore { // services issueService; - constructor(rootStore: IIssueDetail) { + constructor(rootStore: IIssueDetail, serviceType: EIssueServiceType) { makeObservable(this, { // observables subscriptionMap: observable, @@ -40,7 +40,7 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore { // root store this.rootIssueDetail = rootStore; // services - this.issueService = new IssueService(); + this.issueService = new IssueService(serviceType); } // helper methods