From 03cbad5110282c4b692b41ef8a01a8eabf508929 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 25 Jan 2024 13:41:02 +0530 Subject: [PATCH] fix: inbox issue bug fixes and improvements. (#3460) * style: fix create comment card overflow issue. * chore: improved loader and filter selection logic. * chore: implement inbox issue navigation functionality. * chore: loaders in inbox issue sidebar and the content root * chore: inbox issue detail sidebar revamp. --------- Co-authored-by: gurusainath --- web/components/inbox/content/root.tsx | 86 ++++++++++ web/components/inbox/inbox-issue-actions.tsx | 52 ++++-- web/components/inbox/index.ts | 2 + .../inbox/sidebar/filter/filter-selection.tsx | 10 ++ web/components/inbox/sidebar/root.tsx | 32 +++- .../issue-detail/inbox/main-content.tsx | 4 +- .../issues/issue-detail/inbox/root.tsx | 64 +++---- .../issues/issue-detail/inbox/sidebar.tsx | 158 ++++++++++-------- .../projects/[projectId]/inbox/[inboxId].tsx | 90 +++------- web/store/inbox/inbox_filter.store.ts | 3 +- web/store/inbox/inbox_issue.store.ts | 24 ++- 11 files changed, 316 insertions(+), 209 deletions(-) create mode 100644 web/components/inbox/content/root.tsx diff --git a/web/components/inbox/content/root.tsx b/web/components/inbox/content/root.tsx new file mode 100644 index 000000000..26f58131e --- /dev/null +++ b/web/components/inbox/content/root.tsx @@ -0,0 +1,86 @@ +import { FC } from "react"; +import { observer } from "mobx-react"; +import { Inbox } from "lucide-react"; +// hooks +import { useInboxIssues } from "hooks/store"; +// components +import { InboxIssueActionsHeader } from "components/inbox"; +import { InboxIssueDetailRoot } from "components/issues/issue-detail/inbox"; +// ui +import { Loader } from "@plane/ui"; + +type TInboxContentRoot = { + workspaceSlug: string; + projectId: string; + inboxId: string; + inboxIssueId: string | undefined; +}; + +export const InboxContentRoot: FC = observer((props) => { + const { workspaceSlug, projectId, inboxId, inboxIssueId } = props; + // hooks + const { + issues: { loader, getInboxIssuesByInboxId }, + } = useInboxIssues(); + + const inboxIssuesList = inboxId ? getInboxIssuesByInboxId(inboxId) : undefined; + + return ( + <> + {loader === "init-loader" ? ( + +
+ + + + +
+
+ + + + +
+
+ ) : ( + <> + {!inboxIssueId ? ( +
+
+
+ + {inboxIssuesList && inboxIssuesList.length > 0 ? ( + + {inboxIssuesList?.length} issues found. Select an issue from the sidebar to view its details. + + ) : ( + No issues found + )} +
+
+
+ ) : ( +
+
+ +
+
+ +
+
+ )} + + )} + + ); +}); diff --git a/web/components/inbox/inbox-issue-actions.tsx b/web/components/inbox/inbox-issue-actions.tsx index 25b444de0..0ca28b950 100644 --- a/web/components/inbox/inbox-issue-actions.tsx +++ b/web/components/inbox/inbox-issue-actions.tsx @@ -1,4 +1,4 @@ -import { FC, useEffect, useMemo, useState } from "react"; +import { FC, useCallback, useEffect, useMemo, useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import DatePicker from "react-datepicker"; @@ -16,7 +16,7 @@ import { // ui import { Button } from "@plane/ui"; // icons -import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Inbox, Trash2, XCircle } from "lucide-react"; +import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Trash2, XCircle } from "lucide-react"; // types import type { TInboxStatus, TInboxDetailedStatus } from "@plane/types"; import { EUserProjectRoles } from "constants/project"; @@ -135,6 +135,44 @@ export const InboxIssueActionsHeader: FC = observer((p ] ); + const handleInboxIssueNavigation = useCallback( + (direction: "next" | "prev") => { + if (!inboxIssues || !inboxIssueId) return; + const nextIssueIndex = + direction === "next" + ? (currentIssueIndex + 1) % inboxIssues.length + : (currentIssueIndex - 1 + inboxIssues.length) % inboxIssues.length; + const nextIssueId = inboxIssues[nextIssueIndex]; + if (!nextIssueId) return; + router.push({ + pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`, + query: { + inboxIssueId: nextIssueId, + }, + }); + }, + [workspaceSlug, projectId, inboxId, inboxIssues, inboxIssueId, currentIssueIndex, router] + ); + + const onKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === "ArrowUp") { + handleInboxIssueNavigation("prev"); + } else if (e.key === "ArrowDown") { + handleInboxIssueNavigation("next"); + } + }, + [handleInboxIssueNavigation] + ); + + useEffect(() => { + document.addEventListener("keydown", onKeyDown); + + return () => { + document.removeEventListener("keydown", onKeyDown); + }; + }, [onKeyDown]); + const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const today = new Date(); @@ -207,20 +245,14 @@ export const InboxIssueActionsHeader: FC = observer((p diff --git a/web/components/inbox/index.ts b/web/components/inbox/index.ts index ae267f54c..bc8be5506 100644 --- a/web/components/inbox/index.ts +++ b/web/components/inbox/index.ts @@ -3,6 +3,8 @@ export * from "./modals"; export * from "./inbox-issue-actions"; export * from "./inbox-issue-status"; +export * from "./content/root"; + export * from "./sidebar/root"; export * from "./sidebar/filter/filter-selection"; diff --git a/web/components/inbox/sidebar/filter/filter-selection.tsx b/web/components/inbox/sidebar/filter/filter-selection.tsx index 62b9f4855..a34cf1871 100644 --- a/web/components/inbox/sidebar/filter/filter-selection.tsx +++ b/web/components/inbox/sidebar/filter/filter-selection.tsx @@ -1,5 +1,6 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; // mobx store import { useInboxIssues } from "hooks/store"; // ui @@ -16,6 +17,9 @@ type TInboxIssueFilterSelection = { workspaceSlug: string; projectId: string; in export const InboxIssueFilterSelection: FC = observer((props) => { const { workspaceSlug, projectId, inboxId } = props; + // router + const router = useRouter(); + const { inboxIssueId } = router.query; // hooks const { filters: { inboxFilters, updateInboxFilters }, @@ -49,6 +53,12 @@ export const InboxIssueFilterSelection: FC = observe updateInboxFilters(workspaceSlug.toString(), projectId.toString(), inboxId.toString(), { [option.key]: [...currentValue, option.value], }); + + if (inboxIssueId) { + router.push({ + pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`, + }); + } }} direction="right" height="rg" diff --git a/web/components/inbox/sidebar/root.tsx b/web/components/inbox/sidebar/root.tsx index cca6b4582..50bb8a31c 100644 --- a/web/components/inbox/sidebar/root.tsx +++ b/web/components/inbox/sidebar/root.tsx @@ -1,5 +1,10 @@ import { FC } from "react"; import { Inbox } from "lucide-react"; +import { observer } from "mobx-react"; +// hooks +import { useInboxIssues } from "hooks/store"; +// ui +import { Loader } from "@plane/ui"; // components import { InboxIssueList, InboxIssueFilterSelection, InboxIssueAppliedFilter } from "../"; @@ -9,19 +14,23 @@ type TInboxSidebarRoot = { inboxId: string; }; -export const InboxSidebarRoot: FC = (props) => { +export const InboxSidebarRoot: FC = observer((props) => { const { workspaceSlug, projectId, inboxId } = props; + // store hooks + const { + issues: { loader }, + } = useInboxIssues(); return (
-
+
Inbox
-
+
@@ -30,9 +39,18 @@ export const InboxSidebarRoot: FC = (props) => {
-
- -
+ {loader && ["init-loader", "mutation"].includes(loader) ? ( + + + + + + + ) : ( +
+ +
+ )}
); -}; +}); diff --git a/web/components/issues/issue-detail/inbox/main-content.tsx b/web/components/issues/issue-detail/inbox/main-content.tsx index 960ef20d7..2e612ad34 100644 --- a/web/components/issues/issue-detail/inbox/main-content.tsx +++ b/web/components/issues/issue-detail/inbox/main-content.tsx @@ -78,7 +78,9 @@ export const InboxIssueMainContent: React.FC = observer((props) => { )}
- +
+ +
); }); diff --git a/web/components/issues/issue-detail/inbox/root.tsx b/web/components/issues/issue-detail/inbox/root.tsx index f689e4210..b8f12a944 100644 --- a/web/components/issues/issue-detail/inbox/root.tsx +++ b/web/components/issues/issue-detail/inbox/root.tsx @@ -9,8 +9,6 @@ import useToast from "hooks/use-toast"; // types import { TIssue } from "@plane/types"; import { TIssueOperations } from "../root"; -// ui -import { Loader } from "@plane/ui"; // constants import { EUserProjectRoles } from "constants/project"; @@ -105,46 +103,28 @@ export const InboxIssueDetailRoot: FC = (props) => { // issue details const issue = getIssueById(issueId); + if (!issue) return <>; return ( - <> - {issue ? ( -
-
- -
-
- -
-
- ) : ( - -
- - - - -
-
- - - - -
-
- )} - +
+
+ +
+
+ +
+
); }; diff --git a/web/components/issues/issue-detail/inbox/sidebar.tsx b/web/components/issues/issue-detail/inbox/sidebar.tsx index 0b14e46cb..e0b2aca28 100644 --- a/web/components/issues/issue-detail/inbox/sidebar.tsx +++ b/web/components/issues/issue-detail/inbox/sidebar.tsx @@ -1,17 +1,15 @@ import React from "react"; -// import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { CalendarDays, Signal, Tag } from "lucide-react"; +import { CalendarCheck2, Signal, Tag } from "lucide-react"; // hooks -import { useIssueDetail, useProject } from "hooks/store"; +import { useIssueDetail, useProject, useProjectState } from "hooks/store"; // components import { IssueLabel, TIssueOperations } from "components/issues"; -import { PriorityDropdown, ProjectMemberDropdown, StateDropdown } from "components/dropdowns"; -// ui -import { CustomDatePicker } from "components/ui"; +import { DateDropdown, PriorityDropdown, ProjectMemberDropdown, StateDropdown } from "components/dropdowns"; // icons import { DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui"; -// types +// helper +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; type Props = { workspaceSlug: string; @@ -23,12 +21,9 @@ type Props = { export const InboxIssueDetailsSidebar: React.FC = observer((props) => { const { workspaceSlug, projectId, issueId, issueOperations, is_editable } = props; - // router - // FIXME: Check if we need this. Previously it was used to render Project Identifier conditionally. - // const router = useRouter(); - // const { inboxIssueId } = router.query; // store hooks const { getProjectById } = useProject(); + const { projectStates } = useProjectState(); const { issue: { getIssueById }, } = useIssueDetail(); @@ -41,11 +36,15 @@ export const InboxIssueDetailsSidebar: React.FC = observer((props) => { const minDate = issue.start_date ? new Date(issue.start_date) : null; minDate?.setDate(minDate.getDate()); + const currentIssueState = projectStates?.find((s) => s.id === issue.state_id); + return (
- + {currentIssueState && ( + + )}

{projectDetails?.identifier}-{issue?.sequence_id}

@@ -53,86 +52,103 @@ export const InboxIssueDetailsSidebar: React.FC = observer((props) => {
+
Properties
-
+
{/* State */} -
-
+
+
-

State

-
-
- issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })} - projectId={projectId?.toString() ?? ""} - disabled={!is_editable} - buttonVariant="background-with-text" - /> + State
+ issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })} + projectId={projectId?.toString() ?? ""} + disabled={!is_editable} + buttonVariant="transparent-with-text" + className="w-3/5 flex-grow group" + buttonContainerClassName="w-full text-left" + buttonClassName="text-sm" + dropdownArrow + dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline" + />
{/* Assignee */} -
-
+
+
-

Assignees

-
-
- issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })} - disabled={!is_editable} - projectId={projectId?.toString() ?? ""} - placeholder="Assignees" - multiple - buttonVariant={issue?.assignee_ids?.length > 0 ? "transparent-without-text" : "background-with-text"} - buttonClassName={issue?.assignee_ids?.length > 0 ? "hover:bg-transparent px-0" : ""} - /> + Assignees
+ issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })} + disabled={!is_editable} + projectId={projectId?.toString() ?? ""} + placeholder="Add assignees" + multiple + buttonVariant={issue?.assignee_ids?.length > 0 ? "transparent-without-text" : "transparent-with-text"} + className="w-3/5 flex-grow group" + buttonContainerClassName="w-full text-left" + buttonClassName={`text-sm justify-between ${ + issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400" + }`} + hideIcon={issue.assignee_ids?.length === 0} + dropdownArrow + dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline" + />
{/* Priority */} -
-
+
+
-

Priority

-
-
- issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })} - disabled={!is_editable} - buttonVariant="background-with-text" - /> + Priority
+ issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })} + disabled={!is_editable} + buttonVariant="border-with-text" + className="w-3/5 flex-grow rounded px-2 hover:bg-custom-background-80" + buttonContainerClassName="w-full text-left" + buttonClassName="w-min h-auto whitespace-nowrap" + />
-
-
+
+
{/* Due Date */} -
-
- -

Due date

-
-
- issueOperations.update(workspaceSlug, projectId, issueId, { target_date: val })} - className="border-none bg-custom-background-80" - minDate={minDate ?? undefined} - disabled={!is_editable} - /> +
+
+ + Due date
+ + issueOperations.update(workspaceSlug, projectId, issueId, { + target_date: val ? renderFormattedPayloadDate(val) : null, + }) + } + minDate={minDate ?? undefined} + disabled={!is_editable} + buttonVariant="transparent-with-text" + className="w-3/5 flex-grow group" + buttonContainerClassName="w-full text-left" + buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`} + hideIcon + clearIconClassName="h-3 w-3 hidden group-hover:inline" + />
{/* Labels */} -
-
+
+
-

Label

+ Labels
-
+
{ const router = useRouter(); const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query; // store hooks - const { - issues: { getInboxIssuesByInboxId }, - } = useInboxIssues(); const { currentProjectDetails } = useProject(); const { filters: { fetchInboxFilters }, - issues: { loader, fetchInboxIssues }, + issues: { fetchInboxIssues }, } = useInboxIssues(); useSWR( @@ -41,67 +35,25 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => { } ); - // inbox issues list - const inboxIssuesList = inboxId ? getInboxIssuesByInboxId(inboxId?.toString()) : undefined; - - if (!workspaceSlug || !projectId || !inboxId) return <>; + if (!workspaceSlug || !projectId || !inboxId || !currentProjectDetails?.inbox_view) return <>; return ( - <> - {loader === "fetch" ? ( -
- -
- ) : ( -
-
- {workspaceSlug && projectId && inboxId && ( - - )} -
-
- {workspaceSlug && projectId && inboxId && inboxIssueId ? ( -
-
- -
-
- -
-
- ) : ( -
-
-
- - {inboxIssuesList && inboxIssuesList.length > 0 ? ( - - {inboxIssuesList?.length} issues found. Select an issue from the sidebar to view its details. - - ) : ( - No issues found - )} -
-
-
- )} -
-
- )} - +
+
+ +
+
+ +
+
); }); diff --git a/web/store/inbox/inbox_filter.store.ts b/web/store/inbox/inbox_filter.store.ts index 5626c2f9a..c4566acbe 100644 --- a/web/store/inbox/inbox_filter.store.ts +++ b/web/store/inbox/inbox_filter.store.ts @@ -115,11 +115,12 @@ export class InboxFilter implements IInboxFilter { }; _filters = { ..._filters, ...filters }; + this.rootStore.inbox.inboxIssue.fetchInboxIssues(workspaceSlug, projectId, inboxId, "mutation"); + const response = await this.rootStore.inbox.inbox.updateInbox(workspaceSlug, projectId, inboxId, { view_props: { filters: _filters }, }); - this.rootStore.inbox.inboxIssue.fetchInboxIssues(workspaceSlug, projectId, inboxId); return response; } catch (error) { throw error; diff --git a/web/store/inbox/inbox_issue.store.ts b/web/store/inbox/inbox_issue.store.ts index 8d1b7137f..d160ed037 100644 --- a/web/store/inbox/inbox_issue.store.ts +++ b/web/store/inbox/inbox_issue.store.ts @@ -17,21 +17,24 @@ import type { TInboxDetailedStatus, TIssue, } from "@plane/types"; -// constants -import { INBOX_ISSUE_SOURCE } from "constants/inbox"; -type TInboxIssueLoader = "fetch" | undefined; +type TLoader = "init-loader" | "mutation" | undefined; export interface IInboxIssue { // observables - loader: TInboxIssueLoader; + loader: TLoader; inboxIssues: TInboxIssueDetailIdMap; inboxIssueMap: TInboxIssueDetailMap; // helper methods getInboxIssuesByInboxId: (inboxId: string) => string[] | undefined; getInboxIssueByIssueId: (inboxId: string, issueId: string) => TInboxIssueDetail | undefined; // actions - fetchInboxIssues: (workspaceSlug: string, projectId: string, inboxId: string) => Promise; + fetchInboxIssues: ( + workspaceSlug: string, + projectId: string, + inboxId: string, + loaderType?: TLoader + ) => Promise; fetchInboxIssueById: ( workspaceSlug: string, projectId: string, @@ -63,7 +66,7 @@ export interface IInboxIssue { export class InboxIssue implements IInboxIssue { // observables - loader: TInboxIssueLoader = "fetch"; + loader: TLoader = "init-loader"; inboxIssues: TInboxIssueDetailIdMap = {}; inboxIssueMap: TInboxIssueDetailMap = {}; // root store @@ -104,9 +107,14 @@ export class InboxIssue implements IInboxIssue { }); // actions - fetchInboxIssues = async (workspaceSlug: string, projectId: string, inboxId: string) => { + fetchInboxIssues = async ( + workspaceSlug: string, + projectId: string, + inboxId: string, + loaderType: TLoader = "init-loader" + ) => { try { - this.loader = "fetch"; + this.loader = loaderType; const queryParams = this.rootStore.inbox.inboxFilter.inboxAppliedFilters ?? {}; const response = await this.inboxIssueService.fetchInboxIssues(workspaceSlug, projectId, inboxId, queryParams);