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 { 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<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 today = new Date();
|
||||
@ -207,20 +245,14 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-custom-border-200 bg-custom-background-90 p-1.5 hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "ArrowUp" });
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
onClick={() => handleInboxIssueNavigation("prev")}
|
||||
>
|
||||
<ChevronUp size={14} strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-custom-border-200 bg-custom-background-90 p-1.5 hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "ArrowDown" });
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
onClick={() => handleInboxIssueNavigation("next")}
|
||||
>
|
||||
<ChevronDown size={14} strokeWidth={2} />
|
||||
</button>
|
||||
|
@ -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";
|
||||
|
@ -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<TInboxIssueFilterSelection> = 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<TInboxIssueFilterSelection> = 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"
|
||||
|
@ -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<TInboxSidebarRoot> = (props) => {
|
||||
export const InboxSidebarRoot: FC<TInboxSidebarRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, inboxId } = props;
|
||||
// store hooks
|
||||
const {
|
||||
issues: { loader },
|
||||
} = useInboxIssues();
|
||||
|
||||
return (
|
||||
<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 w-6 h-6 flex justify-center items-center rounded bg-custom-background-80">
|
||||
<Inbox className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="font-medium">Inbox</div>
|
||||
</div>
|
||||
<div className="z-[999999]">
|
||||
<div className="z-20">
|
||||
<InboxIssueFilterSelection workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
|
||||
</div>
|
||||
</div>
|
||||
@ -30,9 +39,18 @@ export const InboxSidebarRoot: FC<TInboxSidebarRoot> = (props) => {
|
||||
<InboxIssueAppliedFilter workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
|
||||
</div>
|
||||
|
||||
<div className="w-full h-full overflow-hidden">
|
||||
<InboxIssueList workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
|
||||
</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">
|
||||
<InboxIssueList workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -78,7 +78,9 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={!is_editable} />
|
||||
<div className="pb-12">
|
||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={!is_editable} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -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<TInboxIssueDetailRoot> = (props) => {
|
||||
// issue details
|
||||
const issue = getIssueById(issueId);
|
||||
|
||||
if (!issue) return <></>;
|
||||
return (
|
||||
<>
|
||||
{issue ? (
|
||||
<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">
|
||||
<InboxIssueMainContent
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
inboxId={inboxId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
is_editable={is_editable}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full w-1/3 space-y-5 overflow-hidden border-l border-custom-border-300 py-5">
|
||||
<InboxIssueDetailsSidebar
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
is_editable={is_editable}
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
<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">
|
||||
<InboxIssueMainContent
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
inboxId={inboxId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
is_editable={is_editable}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full w-1/3 space-y-5 overflow-hidden border-l border-custom-border-300 py-5">
|
||||
<InboxIssueDetailsSidebar
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
is_editable={is_editable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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<Props> = 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<Props> = 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 (
|
||||
<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 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">
|
||||
{projectDetails?.identifier}-{issue?.sequence_id}
|
||||
</h4>
|
||||
@ -53,86 +52,103 @@ export const InboxIssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
|
||||
<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="py-1">
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* State */}
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<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" />
|
||||
<p>State</p>
|
||||
</div>
|
||||
<div className="h-5 sm:w-1/2">
|
||||
<StateDropdown
|
||||
value={issue?.state_id ?? undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
disabled={!is_editable}
|
||||
buttonVariant="background-with-text"
|
||||
/>
|
||||
<span>State</span>
|
||||
</div>
|
||||
<StateDropdown
|
||||
value={issue?.state_id ?? undefined}
|
||||
onChange={(val) => 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"
|
||||
/>
|
||||
</div>
|
||||
{/* Assignee */}
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<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" />
|
||||
<p>Assignees</p>
|
||||
</div>
|
||||
<div className="h-5 sm:w-1/2">
|
||||
<ProjectMemberDropdown
|
||||
value={issue?.assignee_ids ?? undefined}
|
||||
onChange={(val) => 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" : ""}
|
||||
/>
|
||||
<span>Assignees</span>
|
||||
</div>
|
||||
<ProjectMemberDropdown
|
||||
value={issue?.assignee_ids ?? undefined}
|
||||
onChange={(val) => 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"
|
||||
/>
|
||||
</div>
|
||||
{/* Priority */}
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<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" />
|
||||
<p>Priority</p>
|
||||
</div>
|
||||
<div className="h-5 sm:w-1/2">
|
||||
<PriorityDropdown
|
||||
value={issue?.priority || undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })}
|
||||
disabled={!is_editable}
|
||||
buttonVariant="background-with-text"
|
||||
/>
|
||||
<span>Priority</span>
|
||||
</div>
|
||||
<PriorityDropdown
|
||||
value={issue?.priority || undefined}
|
||||
onChange={(val) => 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`divide-y-2 divide-custom-border-200 ${!is_editable ? "opacity-60" : ""}`}>
|
||||
<div className="py-1">
|
||||
<div className={`divide-y-2 divide-custom-border-200 mt-3 ${!is_editable ? "opacity-60" : ""}`}>
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Due Date */}
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||
<CalendarDays className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Due date</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<CustomDatePicker
|
||||
placeholder="Due date"
|
||||
value={issue.target_date || undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { target_date: val })}
|
||||
className="border-none bg-custom-background-80"
|
||||
minDate={minDate ?? undefined}
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<CalendarCheck2 className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Due date</span>
|
||||
</div>
|
||||
<DateDropdown
|
||||
placeholder="Add due date"
|
||||
value={issue.target_date}
|
||||
onChange={(val) =>
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
{/* Labels */}
|
||||
<div className={`flex flex-wrap items-start py-2 ${!is_editable ? "opacity-60" : ""}`}>
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
||||
<div className="flex items-center gap-2 min-h-8">
|
||||
<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" />
|
||||
<p>Label</p>
|
||||
<span>Labels</span>
|
||||
</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
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
|
@ -2,17 +2,14 @@ import { ReactElement } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { observer } from "mobx-react";
|
||||
import { Inbox } from "lucide-react";
|
||||
// hooks
|
||||
import { useProject, useInboxIssues } from "hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
// components
|
||||
import { ProjectInboxHeader } from "components/headers";
|
||||
import { InboxSidebarRoot, InboxIssueActionsHeader } from "components/inbox";
|
||||
import { InboxIssueDetailRoot } from "components/issues/issue-detail/inbox";
|
||||
import { InboxSidebarRoot, InboxContentRoot } from "components/inbox";
|
||||
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
|
||||
@ -20,13 +17,10 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
|
||||
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" ? (
|
||||
<div className="relative flex w-full h-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative flex h-full overflow-hidden">
|
||||
<div className="flex-shrink-0 w-[340px] h-full border-r border-custom-border-100">
|
||||
{workspaceSlug && projectId && inboxId && (
|
||||
<InboxSidebarRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
inboxId={inboxId.toString()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
{workspaceSlug && projectId && inboxId && inboxIssueId ? (
|
||||
<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()}
|
||||
projectId={projectId.toString()}
|
||||
inboxId={inboxId.toString()}
|
||||
inboxIssueId={inboxIssueId?.toString() || undefined}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full h-full">
|
||||
<InboxIssueDetailRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
inboxId={inboxId.toString()}
|
||||
issueId={inboxIssueId.toString()}
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
<div className="relative flex h-full overflow-hidden">
|
||||
<div className="flex-shrink-0 w-[340px] h-full border-r border-custom-border-300">
|
||||
<InboxSidebarRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
inboxId={inboxId.toString()}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<InboxContentRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
inboxId={inboxId.toString()}
|
||||
inboxIssueId={inboxIssueId?.toString() || undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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<TInboxIssueExtendedDetail[]>;
|
||||
fetchInboxIssues: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
loaderType?: TLoader
|
||||
) => Promise<TInboxIssueExtendedDetail[]>;
|
||||
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);
|
||||
|
Loading…
Reference in New Issue
Block a user