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 <gurusainath007@gmail.com>
This commit is contained in:
Prateek Shourya 2024-01-25 13:41:02 +05:30 committed by GitHub
parent 2956c43ed5
commit 03cbad5110
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 316 additions and 209 deletions

View File

@ -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<TInboxContentRoot> = observer((props) => {
const { workspaceSlug, projectId, inboxId, inboxIssueId } = props;
// hooks
const {
issues: { loader, getInboxIssuesByInboxId },
} = useInboxIssues();
const inboxIssuesList = inboxId ? getInboxIssuesByInboxId(inboxId) : undefined;
return (
<>
{loader === "init-loader" ? (
<Loader className="flex h-full gap-5 p-5">
<div className="basis-2/3 space-y-2">
<Loader.Item height="30px" width="40%" />
<Loader.Item height="15px" width="60%" />
<Loader.Item height="15px" width="60%" />
<Loader.Item height="15px" width="40%" />
</div>
<div className="basis-1/3 space-y-3">
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
</div>
</Loader>
) : (
<>
{!inboxIssueId ? (
<div className="grid h-full place-items-center p-4 text-custom-text-200">
<div className="grid h-full place-items-center">
<div className="my-5 flex flex-col items-center gap-4">
<Inbox size={60} strokeWidth={1.5} />
{inboxIssuesList && inboxIssuesList.length > 0 ? (
<span className="text-custom-text-200">
{inboxIssuesList?.length} issues found. Select an issue from the sidebar to view its details.
</span>
) : (
<span className="text-custom-text-200">No issues found</span>
)}
</div>
</div>
</div>
) : (
<div className="w-full h-full overflow-hidden relative flex flex-col">
<div className="flex-shrink-0 min-h-[50px] border-b border-custom-border-300">
<InboxIssueActionsHeader
workspaceSlug={workspaceSlug}
projectId={projectId}
inboxId={inboxId}
inboxIssueId={inboxIssueId}
/>
</div>
<div className="w-full h-full">
<InboxIssueDetailRoot
workspaceSlug={workspaceSlug}
projectId={projectId}
inboxId={inboxId}
issueId={inboxIssueId}
/>
</div>
</div>
)}
</>
)}
</>
);
});

View File

@ -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 { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import DatePicker from "react-datepicker"; import DatePicker from "react-datepicker";
@ -16,7 +16,7 @@ import {
// ui // ui
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// icons // 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 // types
import type { TInboxStatus, TInboxDetailedStatus } from "@plane/types"; import type { TInboxStatus, TInboxDetailedStatus } from "@plane/types";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
@ -135,6 +135,44 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = 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 isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const today = new Date(); const today = new Date();
@ -207,20 +245,14 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
<button <button
type="button" type="button"
className="rounded border border-custom-border-200 bg-custom-background-90 p-1.5 hover:bg-custom-background-80" className="rounded border border-custom-border-200 bg-custom-background-90 p-1.5 hover:bg-custom-background-80"
onClick={() => { onClick={() => handleInboxIssueNavigation("prev")}
const e = new KeyboardEvent("keydown", { key: "ArrowUp" });
document.dispatchEvent(e);
}}
> >
<ChevronUp size={14} strokeWidth={2} /> <ChevronUp size={14} strokeWidth={2} />
</button> </button>
<button <button
type="button" type="button"
className="rounded border border-custom-border-200 bg-custom-background-90 p-1.5 hover:bg-custom-background-80" className="rounded border border-custom-border-200 bg-custom-background-90 p-1.5 hover:bg-custom-background-80"
onClick={() => { onClick={() => handleInboxIssueNavigation("next")}
const e = new KeyboardEvent("keydown", { key: "ArrowDown" });
document.dispatchEvent(e);
}}
> >
<ChevronDown size={14} strokeWidth={2} /> <ChevronDown size={14} strokeWidth={2} />
</button> </button>

View File

@ -3,6 +3,8 @@ export * from "./modals";
export * from "./inbox-issue-actions"; export * from "./inbox-issue-actions";
export * from "./inbox-issue-status"; export * from "./inbox-issue-status";
export * from "./content/root";
export * from "./sidebar/root"; export * from "./sidebar/root";
export * from "./sidebar/filter/filter-selection"; export * from "./sidebar/filter/filter-selection";

View File

@ -1,5 +1,6 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
// mobx store // mobx store
import { useInboxIssues } from "hooks/store"; import { useInboxIssues } from "hooks/store";
// ui // ui
@ -16,6 +17,9 @@ type TInboxIssueFilterSelection = { workspaceSlug: string; projectId: string; in
export const InboxIssueFilterSelection: FC<TInboxIssueFilterSelection> = observer((props) => { export const InboxIssueFilterSelection: FC<TInboxIssueFilterSelection> = observer((props) => {
const { workspaceSlug, projectId, inboxId } = props; const { workspaceSlug, projectId, inboxId } = props;
// router
const router = useRouter();
const { inboxIssueId } = router.query;
// hooks // hooks
const { const {
filters: { inboxFilters, updateInboxFilters }, filters: { inboxFilters, updateInboxFilters },
@ -49,6 +53,12 @@ export const InboxIssueFilterSelection: FC<TInboxIssueFilterSelection> = observe
updateInboxFilters(workspaceSlug.toString(), projectId.toString(), inboxId.toString(), { updateInboxFilters(workspaceSlug.toString(), projectId.toString(), inboxId.toString(), {
[option.key]: [...currentValue, option.value], [option.key]: [...currentValue, option.value],
}); });
if (inboxIssueId) {
router.push({
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
});
}
}} }}
direction="right" direction="right"
height="rg" height="rg"

View File

@ -1,5 +1,10 @@
import { FC } from "react"; import { FC } from "react";
import { Inbox } from "lucide-react"; import { Inbox } from "lucide-react";
import { observer } from "mobx-react";
// hooks
import { useInboxIssues } from "hooks/store";
// ui
import { Loader } from "@plane/ui";
// components // components
import { InboxIssueList, InboxIssueFilterSelection, InboxIssueAppliedFilter } from "../"; import { InboxIssueList, InboxIssueFilterSelection, InboxIssueAppliedFilter } from "../";
@ -9,19 +14,23 @@ type TInboxSidebarRoot = {
inboxId: string; inboxId: string;
}; };
export const InboxSidebarRoot: FC<TInboxSidebarRoot> = (props) => { export const InboxSidebarRoot: FC<TInboxSidebarRoot> = observer((props) => {
const { workspaceSlug, projectId, inboxId } = props; const { workspaceSlug, projectId, inboxId } = props;
// store hooks
const {
issues: { loader },
} = useInboxIssues();
return ( return (
<div className="relative flex flex-col w-full h-full"> <div className="relative flex flex-col w-full h-full">
<div className="flex-shrink-0 w-full h-[50px] relative flex justify-between items-center gap-2 p-2 px-3 border-b border-custom-border-100"> <div className="flex-shrink-0 w-full h-[50px] relative flex justify-between items-center gap-2 p-2 px-3 border-b border-custom-border-300">
<div className="relative flex items-center gap-1"> <div className="relative flex items-center gap-1">
<div className="relative w-6 h-6 flex justify-center items-center rounded bg-custom-background-80"> <div className="relative w-6 h-6 flex justify-center items-center rounded bg-custom-background-80">
<Inbox className="w-4 h-4" /> <Inbox className="w-4 h-4" />
</div> </div>
<div className="font-medium">Inbox</div> <div className="font-medium">Inbox</div>
</div> </div>
<div className="z-[999999]"> <div className="z-20">
<InboxIssueFilterSelection workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} /> <InboxIssueFilterSelection workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
</div> </div>
</div> </div>
@ -30,9 +39,18 @@ export const InboxSidebarRoot: FC<TInboxSidebarRoot> = (props) => {
<InboxIssueAppliedFilter workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} /> <InboxIssueAppliedFilter workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
</div> </div>
{loader && ["init-loader", "mutation"].includes(loader) ? (
<Loader className="flex flex-col h-full gap-5 p-5">
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
</Loader>
) : (
<div className="w-full h-full overflow-hidden"> <div className="w-full h-full overflow-hidden">
<InboxIssueList workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} /> <InboxIssueList workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
</div> </div>
)}
</div> </div>
); );
}; });

View File

@ -78,7 +78,9 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
)} )}
</div> </div>
<div className="pb-12">
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={!is_editable} /> <IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={!is_editable} />
</div>
</> </>
); );
}); });

View File

@ -9,8 +9,6 @@ import useToast from "hooks/use-toast";
// types // types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
import { TIssueOperations } from "../root"; import { TIssueOperations } from "../root";
// ui
import { Loader } from "@plane/ui";
// constants // constants
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
@ -105,9 +103,8 @@ export const InboxIssueDetailRoot: FC<TInboxIssueDetailRoot> = (props) => {
// issue details // issue details
const issue = getIssueById(issueId); const issue = getIssueById(issueId);
if (!issue) return <></>;
return ( return (
<>
{issue ? (
<div className="flex h-full overflow-hidden"> <div className="flex h-full overflow-hidden">
<div className="h-full w-2/3 space-y-5 divide-y-2 divide-custom-border-300 overflow-y-auto p-5"> <div className="h-full w-2/3 space-y-5 divide-y-2 divide-custom-border-300 overflow-y-auto p-5">
<InboxIssueMainContent <InboxIssueMainContent
@ -129,22 +126,5 @@ export const InboxIssueDetailRoot: FC<TInboxIssueDetailRoot> = (props) => {
/> />
</div> </div>
</div> </div>
) : (
<Loader className="flex h-full gap-5 p-5">
<div className="basis-2/3 space-y-2">
<Loader.Item height="30px" width="40%" />
<Loader.Item height="15px" width="60%" />
<Loader.Item height="15px" width="60%" />
<Loader.Item height="15px" width="40%" />
</div>
<div className="basis-1/3 space-y-3">
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
</div>
</Loader>
)}
</>
); );
}; };

View File

@ -1,17 +1,15 @@
import React from "react"; import React from "react";
// import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { CalendarDays, Signal, Tag } from "lucide-react"; import { CalendarCheck2, Signal, Tag } from "lucide-react";
// hooks // hooks
import { useIssueDetail, useProject } from "hooks/store"; import { useIssueDetail, useProject, useProjectState } from "hooks/store";
// components // components
import { IssueLabel, TIssueOperations } from "components/issues"; import { IssueLabel, TIssueOperations } from "components/issues";
import { PriorityDropdown, ProjectMemberDropdown, StateDropdown } from "components/dropdowns"; import { DateDropdown, PriorityDropdown, ProjectMemberDropdown, StateDropdown } from "components/dropdowns";
// ui
import { CustomDatePicker } from "components/ui";
// icons // icons
import { DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui"; import { DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui";
// types // helper
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
type Props = { type Props = {
workspaceSlug: string; workspaceSlug: string;
@ -23,12 +21,9 @@ type Props = {
export const InboxIssueDetailsSidebar: React.FC<Props> = observer((props) => { export const InboxIssueDetailsSidebar: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, issueId, issueOperations, is_editable } = 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 // store hooks
const { getProjectById } = useProject(); const { getProjectById } = useProject();
const { projectStates } = useProjectState();
const { const {
issue: { getIssueById }, issue: { getIssueById },
} = useIssueDetail(); } = useIssueDetail();
@ -41,11 +36,15 @@ export const InboxIssueDetailsSidebar: React.FC<Props> = observer((props) => {
const minDate = issue.start_date ? new Date(issue.start_date) : null; const minDate = issue.start_date ? new Date(issue.start_date) : null;
minDate?.setDate(minDate.getDate()); minDate?.setDate(minDate.getDate());
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
return ( return (
<div className="flex h-full w-full flex-col divide-y-2 divide-custom-border-200 overflow-hidden"> <div className="flex h-full w-full flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
<div className="flex items-center justify-between px-5 pb-3"> <div className="flex items-center justify-between px-5 pb-3">
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
<StateGroupIcon className="h-4 w-4" stateGroup="backlog" color="#ff7700" /> {currentIssueState && (
<StateGroupIcon className="h-4 w-4" stateGroup={currentIssueState.group} color={currentIssueState.color} />
)}
<h4 className="text-lg font-medium text-custom-text-300"> <h4 className="text-lg font-medium text-custom-text-300">
{projectDetails?.identifier}-{issue?.sequence_id} {projectDetails?.identifier}-{issue?.sequence_id}
</h4> </h4>
@ -53,86 +52,103 @@ export const InboxIssueDetailsSidebar: React.FC<Props> = observer((props) => {
</div> </div>
<div className="h-full w-full overflow-y-auto px-5"> <div className="h-full w-full overflow-y-auto px-5">
<h5 className="text-sm font-medium my-4">Properties</h5>
<div className={`divide-y-2 divide-custom-border-200 ${!is_editable ? "opacity-60" : ""}`}> <div className={`divide-y-2 divide-custom-border-200 ${!is_editable ? "opacity-60" : ""}`}>
<div className="py-1"> <div className="flex flex-col gap-3">
{/* State */} {/* State */}
<div className="flex flex-wrap items-center py-2"> <div className="flex items-center gap-2 h-8">
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2"> <div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<DoubleCircleIcon className="h-4 w-4 flex-shrink-0" /> <DoubleCircleIcon className="h-4 w-4 flex-shrink-0" />
<p>State</p> <span>State</span>
</div> </div>
<div className="h-5 sm:w-1/2">
<StateDropdown <StateDropdown
value={issue?.state_id ?? undefined} value={issue?.state_id ?? undefined}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })} onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })}
projectId={projectId?.toString() ?? ""} projectId={projectId?.toString() ?? ""}
disabled={!is_editable} disabled={!is_editable}
buttonVariant="background-with-text" 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"
/> />
</div> </div>
</div>
{/* Assignee */} {/* Assignee */}
<div className="flex flex-wrap items-center py-2"> <div className="flex items-center gap-2 h-8">
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2"> <div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<UserGroupIcon className="h-4 w-4 flex-shrink-0" /> <UserGroupIcon className="h-4 w-4 flex-shrink-0" />
<p>Assignees</p> <span>Assignees</span>
</div> </div>
<div className="h-5 sm:w-1/2">
<ProjectMemberDropdown <ProjectMemberDropdown
value={issue?.assignee_ids ?? undefined} value={issue?.assignee_ids ?? undefined}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })} onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
disabled={!is_editable} disabled={!is_editable}
projectId={projectId?.toString() ?? ""} projectId={projectId?.toString() ?? ""}
placeholder="Assignees" placeholder="Add assignees"
multiple multiple
buttonVariant={issue?.assignee_ids?.length > 0 ? "transparent-without-text" : "background-with-text"} buttonVariant={issue?.assignee_ids?.length > 0 ? "transparent-without-text" : "transparent-with-text"}
buttonClassName={issue?.assignee_ids?.length > 0 ? "hover:bg-transparent px-0" : ""} 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"
/> />
</div> </div>
</div>
{/* Priority */} {/* Priority */}
<div className="flex flex-wrap items-center py-2"> <div className="flex items-center gap-2 h-8">
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2"> <div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<Signal className="h-4 w-4 flex-shrink-0" /> <Signal className="h-4 w-4 flex-shrink-0" />
<p>Priority</p> <span>Priority</span>
</div> </div>
<div className="h-5 sm:w-1/2">
<PriorityDropdown <PriorityDropdown
value={issue?.priority || undefined} value={issue?.priority || undefined}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })} onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })}
disabled={!is_editable} disabled={!is_editable}
buttonVariant="background-with-text" 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"
/> />
</div> </div>
</div> </div>
</div> </div>
</div> <div className={`divide-y-2 divide-custom-border-200 mt-3 ${!is_editable ? "opacity-60" : ""}`}>
<div className={`divide-y-2 divide-custom-border-200 ${!is_editable ? "opacity-60" : ""}`}> <div className="flex flex-col gap-3">
<div className="py-1">
{/* Due Date */} {/* Due Date */}
<div className="flex flex-wrap items-center py-2"> <div className="flex items-center gap-2 h-8">
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2"> <div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<CalendarDays className="h-4 w-4 flex-shrink-0" /> <CalendarCheck2 className="h-4 w-4 flex-shrink-0" />
<p>Due date</p> <span>Due date</span>
</div> </div>
<div className="sm:basis-1/2"> <DateDropdown
<CustomDatePicker placeholder="Add due date"
placeholder="Due date" value={issue.target_date}
value={issue.target_date || undefined} onChange={(val) =>
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { target_date: val })} issueOperations.update(workspaceSlug, projectId, issueId, {
className="border-none bg-custom-background-80" target_date: val ? renderFormattedPayloadDate(val) : null,
})
}
minDate={minDate ?? undefined} minDate={minDate ?? undefined}
disabled={!is_editable} 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"
/> />
</div> </div>
</div>
{/* Labels */} {/* Labels */}
<div className={`flex flex-wrap items-start py-2 ${!is_editable ? "opacity-60" : ""}`}> <div className="flex items-center gap-2 min-h-8">
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2"> <div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
<Tag className="h-4 w-4 flex-shrink-0" /> <Tag className="h-4 w-4 flex-shrink-0" />
<p>Label</p> <span>Labels</span>
</div> </div>
<div className="space-y-1 sm:w-1/2"> <div className="w-3/5 flex-grow min-h-8 h-full pt-1">
<IssueLabel <IssueLabel
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}

View File

@ -2,17 +2,14 @@ import { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Inbox } from "lucide-react";
// hooks // hooks
import { useProject, useInboxIssues } from "hooks/store"; import { useProject, useInboxIssues } from "hooks/store";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// ui
import { Spinner } from "@plane/ui";
// components // components
import { ProjectInboxHeader } from "components/headers"; import { ProjectInboxHeader } from "components/headers";
import { InboxSidebarRoot, InboxIssueActionsHeader } from "components/inbox"; import { InboxSidebarRoot, InboxContentRoot } from "components/inbox";
import { InboxIssueDetailRoot } from "components/issues/issue-detail/inbox";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
@ -20,13 +17,10 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query; const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query;
// store hooks // store hooks
const {
issues: { getInboxIssuesByInboxId },
} = useInboxIssues();
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { const {
filters: { fetchInboxFilters }, filters: { fetchInboxFilters },
issues: { loader, fetchInboxIssues }, issues: { fetchInboxIssues },
} = useInboxIssues(); } = useInboxIssues();
useSWR( useSWR(
@ -41,67 +35,25 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
} }
); );
// inbox issues list if (!workspaceSlug || !projectId || !inboxId || !currentProjectDetails?.inbox_view) return <></>;
const inboxIssuesList = inboxId ? getInboxIssuesByInboxId(inboxId?.toString()) : undefined;
if (!workspaceSlug || !projectId || !inboxId) return <></>;
return ( return (
<>
{loader === "fetch" ? (
<div className="relative flex w-full h-full items-center justify-center">
<Spinner />
</div>
) : (
<div className="relative flex h-full overflow-hidden"> <div className="relative flex h-full overflow-hidden">
<div className="flex-shrink-0 w-[340px] h-full border-r border-custom-border-100"> <div className="flex-shrink-0 w-[340px] h-full border-r border-custom-border-300">
{workspaceSlug && projectId && inboxId && (
<InboxSidebarRoot <InboxSidebarRoot
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()} projectId={projectId.toString()}
inboxId={inboxId.toString()} inboxId={inboxId.toString()}
/> />
)}
</div> </div>
<div className="w-full"> <div className="w-full">
{workspaceSlug && projectId && inboxId && inboxIssueId ? ( <InboxContentRoot
<div className="w-full h-full overflow-hidden relative flex flex-col">
<div className="flex-shrink-0 min-h-[50px] border-b border-custom-border-100">
<InboxIssueActionsHeader
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()} projectId={projectId.toString()}
inboxId={inboxId.toString()} inboxId={inboxId.toString()}
inboxIssueId={inboxIssueId?.toString() || undefined} inboxIssueId={inboxIssueId?.toString() || undefined}
/> />
</div> </div>
<div className="w-full h-full">
<InboxIssueDetailRoot
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
inboxId={inboxId.toString()}
issueId={inboxIssueId.toString()}
/>
</div> </div>
</div>
) : (
<div className="grid h-full place-items-center p-4 text-custom-text-200">
<div className="grid h-full place-items-center">
<div className="my-5 flex flex-col items-center gap-4">
<Inbox size={60} strokeWidth={1.5} />
{inboxIssuesList && inboxIssuesList.length > 0 ? (
<span className="text-custom-text-200">
{inboxIssuesList?.length} issues found. Select an issue from the sidebar to view its details.
</span>
) : (
<span className="text-custom-text-200">No issues found</span>
)}
</div>
</div>
</div>
)}
</div>
</div>
)}
</>
); );
}); });

View File

@ -115,11 +115,12 @@ export class InboxFilter implements IInboxFilter {
}; };
_filters = { ..._filters, ...filters }; _filters = { ..._filters, ...filters };
this.rootStore.inbox.inboxIssue.fetchInboxIssues(workspaceSlug, projectId, inboxId, "mutation");
const response = await this.rootStore.inbox.inbox.updateInbox(workspaceSlug, projectId, inboxId, { const response = await this.rootStore.inbox.inbox.updateInbox(workspaceSlug, projectId, inboxId, {
view_props: { filters: _filters }, view_props: { filters: _filters },
}); });
this.rootStore.inbox.inboxIssue.fetchInboxIssues(workspaceSlug, projectId, inboxId);
return response; return response;
} catch (error) { } catch (error) {
throw error; throw error;

View File

@ -17,21 +17,24 @@ import type {
TInboxDetailedStatus, TInboxDetailedStatus,
TIssue, TIssue,
} from "@plane/types"; } from "@plane/types";
// constants
import { INBOX_ISSUE_SOURCE } from "constants/inbox";
type TInboxIssueLoader = "fetch" | undefined; type TLoader = "init-loader" | "mutation" | undefined;
export interface IInboxIssue { export interface IInboxIssue {
// observables // observables
loader: TInboxIssueLoader; loader: TLoader;
inboxIssues: TInboxIssueDetailIdMap; inboxIssues: TInboxIssueDetailIdMap;
inboxIssueMap: TInboxIssueDetailMap; inboxIssueMap: TInboxIssueDetailMap;
// helper methods // helper methods
getInboxIssuesByInboxId: (inboxId: string) => string[] | undefined; getInboxIssuesByInboxId: (inboxId: string) => string[] | undefined;
getInboxIssueByIssueId: (inboxId: string, issueId: string) => TInboxIssueDetail | undefined; getInboxIssueByIssueId: (inboxId: string, issueId: string) => TInboxIssueDetail | undefined;
// actions // actions
fetchInboxIssues: (workspaceSlug: string, projectId: string, inboxId: string) => Promise<TInboxIssueExtendedDetail[]>; fetchInboxIssues: (
workspaceSlug: string,
projectId: string,
inboxId: string,
loaderType?: TLoader
) => Promise<TInboxIssueExtendedDetail[]>;
fetchInboxIssueById: ( fetchInboxIssueById: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
@ -63,7 +66,7 @@ export interface IInboxIssue {
export class InboxIssue implements IInboxIssue { export class InboxIssue implements IInboxIssue {
// observables // observables
loader: TInboxIssueLoader = "fetch"; loader: TLoader = "init-loader";
inboxIssues: TInboxIssueDetailIdMap = {}; inboxIssues: TInboxIssueDetailIdMap = {};
inboxIssueMap: TInboxIssueDetailMap = {}; inboxIssueMap: TInboxIssueDetailMap = {};
// root store // root store
@ -104,9 +107,14 @@ export class InboxIssue implements IInboxIssue {
}); });
// actions // actions
fetchInboxIssues = async (workspaceSlug: string, projectId: string, inboxId: string) => { fetchInboxIssues = async (
workspaceSlug: string,
projectId: string,
inboxId: string,
loaderType: TLoader = "init-loader"
) => {
try { try {
this.loader = "fetch"; this.loader = loaderType;
const queryParams = this.rootStore.inbox.inboxFilter.inboxAppliedFilters ?? {}; const queryParams = this.rootStore.inbox.inboxFilter.inboxAppliedFilters ?? {};
const response = await this.inboxIssueService.fetchInboxIssues(workspaceSlug, projectId, inboxId, queryParams); const response = await this.inboxIssueService.fetchInboxIssues(workspaceSlug, projectId, inboxId, queryParams);