forked from github/plane
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:
parent
2956c43ed5
commit
03cbad5110
86
web/components/inbox/content/root.tsx
Normal file
86
web/components/inbox/content/root.tsx
Normal 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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -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>
|
||||||
|
@ -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";
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user