forked from github/plane
chore: route to issue after creating it (#1359)
* chore: navigate to newly created inbox issue * refactor: inbox * fix: hide ai modal after issue creation * chore: hide action buttons after acting upon them * chore: add icon to inbox status * chore: update inbox status colors
This commit is contained in:
parent
41b7544cfc
commit
71b2884b57
@ -1,63 +1,81 @@
|
||||
// hooks
|
||||
import useInboxView from "hooks/use-inbox-view";
|
||||
// ui
|
||||
import { MultiLevelDropdown } from "components/ui";
|
||||
// icons
|
||||
import { getPriorityIcon } from "components/icons";
|
||||
// types
|
||||
import { IInboxFilterOptions } from "types";
|
||||
// constants
|
||||
import { PRIORITIES } from "constants/project";
|
||||
import { INBOX_STATUS } from "constants/inbox";
|
||||
|
||||
type Props = {
|
||||
filters: Partial<IInboxFilterOptions>;
|
||||
onSelect: (option: any) => void;
|
||||
direction?: "left" | "right";
|
||||
height?: "sm" | "md" | "rg" | "lg";
|
||||
};
|
||||
export const FiltersDropdown: React.FC = () => {
|
||||
const { filters, setFilters, filtersLength } = useInboxView();
|
||||
|
||||
export const FiltersDropdown: React.FC<Props> = ({ filters, onSelect, direction, height }) => (
|
||||
<MultiLevelDropdown
|
||||
label="Filters"
|
||||
onSelect={onSelect}
|
||||
direction={direction}
|
||||
height={height}
|
||||
options={[
|
||||
{
|
||||
id: "priority",
|
||||
label: "Priority",
|
||||
value: PRIORITIES,
|
||||
children: [
|
||||
...PRIORITIES.map((priority) => ({
|
||||
id: priority ?? "none",
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
{getPriorityIcon(priority)} {priority ?? "None"}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "priority",
|
||||
value: priority,
|
||||
},
|
||||
selected: filters?.priority?.includes(priority ?? "none"),
|
||||
})),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "inbox_status",
|
||||
label: "Status",
|
||||
value: INBOX_STATUS.map((status) => status.value),
|
||||
children: [
|
||||
...INBOX_STATUS.map((status) => ({
|
||||
id: status.key,
|
||||
label: status.label,
|
||||
value: {
|
||||
key: "inbox_status",
|
||||
value: status.value,
|
||||
},
|
||||
selected: filters?.inbox_status?.includes(status.value),
|
||||
})),
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div className="relative">
|
||||
<MultiLevelDropdown
|
||||
label="Filters"
|
||||
onSelect={(option) => {
|
||||
const key = option.key as keyof typeof filters;
|
||||
|
||||
const valueExists = (filters[key] as any[])?.includes(option.value);
|
||||
|
||||
if (valueExists) {
|
||||
setFilters({
|
||||
[option.key]: ((filters[key] ?? []) as any[])?.filter((val) => val !== option.value),
|
||||
});
|
||||
} else {
|
||||
setFilters({
|
||||
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
|
||||
});
|
||||
}
|
||||
}}
|
||||
direction="right"
|
||||
height="rg"
|
||||
options={[
|
||||
{
|
||||
id: "priority",
|
||||
label: "Priority",
|
||||
value: PRIORITIES,
|
||||
children: [
|
||||
...PRIORITIES.map((priority) => ({
|
||||
id: priority ?? "none",
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
{getPriorityIcon(priority)} {priority ?? "None"}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "priority",
|
||||
value: priority,
|
||||
},
|
||||
selected: filters?.priority?.includes(priority ?? "none"),
|
||||
})),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "inbox_status",
|
||||
label: "Status",
|
||||
value: INBOX_STATUS.map((status) => status.value),
|
||||
children: [
|
||||
...INBOX_STATUS.map((status) => ({
|
||||
id: status.key,
|
||||
label: status.label,
|
||||
value: {
|
||||
key: "inbox_status",
|
||||
value: status.value,
|
||||
},
|
||||
selected: filters?.inbox_status?.includes(status.value),
|
||||
})),
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{filtersLength > 0 && (
|
||||
<div className="absolute -top-2 -right-2 h-4 w-4 text-[0.65rem] grid place-items-center rounded-full text-brand-base bg-brand-surface-2 border border-brand-base z-10">
|
||||
<span>{filtersLength}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -2,17 +2,27 @@ import { useEffect, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// react-datepicker
|
||||
import DatePicker from "react-datepicker";
|
||||
// headless ui
|
||||
import { Popover } from "@headlessui/react";
|
||||
// contexts
|
||||
import { useProjectMyMembership } from "contexts/project-member.context";
|
||||
// services
|
||||
import inboxServices from "services/inbox.service";
|
||||
// hooks
|
||||
import useInboxView from "hooks/use-inbox-view";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { FiltersDropdown } from "components/inbox";
|
||||
import {
|
||||
DeclineIssueModal,
|
||||
DeleteIssueModal,
|
||||
FiltersDropdown,
|
||||
SelectDuplicateInboxIssueModal,
|
||||
} from "components/inbox";
|
||||
// ui
|
||||
import { PrimaryButton, SecondaryButton } from "components/ui";
|
||||
// icons
|
||||
@ -26,47 +36,84 @@ import {
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import type { IInboxIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
issueCount: number;
|
||||
currentIssueIndex: number;
|
||||
issue?: IInboxIssue;
|
||||
onAccept: () => Promise<void>;
|
||||
onDecline: () => void;
|
||||
onMarkAsDuplicate: () => void;
|
||||
onSnooze: (date: Date | string) => void;
|
||||
onDelete: () => void;
|
||||
};
|
||||
|
||||
export const InboxActionHeader: React.FC<Props> = (props) => {
|
||||
const {
|
||||
issueCount,
|
||||
currentIssueIndex,
|
||||
onAccept,
|
||||
onDecline,
|
||||
onMarkAsDuplicate,
|
||||
onSnooze,
|
||||
onDelete,
|
||||
issue,
|
||||
} = props;
|
||||
import type { IInboxIssueDetail, TInboxStatus } from "types";
|
||||
// fetch-keys
|
||||
import { INBOX_ISSUE_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
export const InboxActionHeader = () => {
|
||||
const [isAccepting, setIsAccepting] = useState(false);
|
||||
const [date, setDate] = useState(new Date());
|
||||
const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false);
|
||||
const [declineIssueModal, setDeclineIssueModal] = useState(false);
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { inboxIssueId } = router.query;
|
||||
const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query;
|
||||
|
||||
const { memberRole } = useProjectMyMembership();
|
||||
const { filters, setFilters, filtersLength } = useInboxView();
|
||||
const { user } = useUserAuth();
|
||||
const { memberRole } = useProjectMyMembership();
|
||||
const { issues: inboxIssues, mutate: mutateInboxIssues } = useInboxView();
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const markInboxStatus = async (data: TInboxStatus) => {
|
||||
if (!workspaceSlug || !projectId || !inboxId || !inboxIssueId) return;
|
||||
|
||||
mutate<IInboxIssueDetail>(
|
||||
INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string),
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
issue_inbox: [{ ...prevData.issue_inbox[0], ...data }],
|
||||
};
|
||||
},
|
||||
false
|
||||
);
|
||||
mutateInboxIssues(
|
||||
(prevData) =>
|
||||
(prevData ?? []).map((i) =>
|
||||
i.bridge_id === inboxIssueId
|
||||
? { ...i, issue_inbox: [{ ...i.issue_inbox[0], ...data }] }
|
||||
: i
|
||||
),
|
||||
false
|
||||
);
|
||||
|
||||
await inboxServices
|
||||
.markInboxStatus(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
inboxId.toString(),
|
||||
inboxIssues?.find((inboxIssue) => inboxIssue.bridge_id === inboxIssueId)?.bridge_id!,
|
||||
data,
|
||||
user
|
||||
)
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Something went wrong while updating inbox status. Please try again.",
|
||||
})
|
||||
)
|
||||
.finally(() => {
|
||||
mutate(INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string));
|
||||
mutateInboxIssues();
|
||||
});
|
||||
};
|
||||
|
||||
const handleAcceptIssue = () => {
|
||||
setIsAccepting(true);
|
||||
|
||||
onAccept().finally(() => setIsAccepting(false));
|
||||
markInboxStatus({
|
||||
status: 1,
|
||||
}).finally(() => setIsAccepting(false));
|
||||
};
|
||||
|
||||
const issue = inboxIssues?.find((issue) => issue.bridge_id === inboxIssueId);
|
||||
const currentIssueIndex =
|
||||
inboxIssues?.findIndex((issue) => issue.bridge_id === inboxIssueId) ?? 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (!issue?.issue_inbox[0].snoozed_till) return;
|
||||
|
||||
@ -82,163 +129,165 @@ export const InboxActionHeader: React.FC<Props> = (props) => {
|
||||
tomorrow.setDate(today.getDate() + 1);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 border-b border-brand-base divide-x divide-brand-base">
|
||||
<div className="col-span-1 flex justify-between p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<InboxIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<h3 className="font-medium">Inbox</h3>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<FiltersDropdown
|
||||
filters={filters}
|
||||
onSelect={(option) => {
|
||||
const key = option.key as keyof typeof filters;
|
||||
|
||||
const valueExists = (filters[key] as any[])?.includes(option.value);
|
||||
|
||||
if (valueExists) {
|
||||
setFilters({
|
||||
[option.key]: ((filters[key] ?? []) as any[])?.filter(
|
||||
(val) => val !== option.value
|
||||
),
|
||||
});
|
||||
} else {
|
||||
setFilters({
|
||||
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
|
||||
});
|
||||
}
|
||||
}}
|
||||
direction="right"
|
||||
height="rg"
|
||||
/>
|
||||
{filtersLength > 0 && (
|
||||
<div className="absolute -top-2 -right-2 h-4 w-4 text-[0.65rem] grid place-items-center rounded-full text-brand-base bg-brand-surface-2 border border-brand-base z-10">
|
||||
<span>{filtersLength}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{inboxIssueId && (
|
||||
<div className="flex justify-between items-center gap-4 px-4 col-span-3">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-brand-base bg-brand-surface-1 p-1.5 hover:bg-brand-surface-2"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "ArrowUp" });
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
>
|
||||
<ChevronUpIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-brand-base bg-brand-surface-1 p-1.5 hover:bg-brand-surface-2"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "ArrowDown" });
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
>
|
||||
<ChevronDownIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<div className="text-sm">
|
||||
{currentIssueIndex + 1}/{issueCount}
|
||||
</div>
|
||||
<>
|
||||
<SelectDuplicateInboxIssueModal
|
||||
isOpen={selectDuplicateIssue}
|
||||
onClose={() => setSelectDuplicateIssue(false)}
|
||||
value={
|
||||
inboxIssues?.find((inboxIssue) => inboxIssue.bridge_id === inboxIssueId)?.issue_inbox[0]
|
||||
.duplicate_to
|
||||
}
|
||||
onSubmit={(dupIssueId: string) => {
|
||||
markInboxStatus({
|
||||
status: 2,
|
||||
duplicate_to: dupIssueId,
|
||||
}).finally(() => setSelectDuplicateIssue(false));
|
||||
}}
|
||||
/>
|
||||
<DeclineIssueModal
|
||||
isOpen={declineIssueModal}
|
||||
handleClose={() => setDeclineIssueModal(false)}
|
||||
data={inboxIssues?.find((i) => i.bridge_id === inboxIssueId)}
|
||||
onSubmit={async () => {
|
||||
await markInboxStatus({
|
||||
status: -1,
|
||||
}).finally(() => setDeclineIssueModal(false));
|
||||
}}
|
||||
/>
|
||||
<DeleteIssueModal
|
||||
isOpen={deleteIssueModal}
|
||||
handleClose={() => setDeleteIssueModal(false)}
|
||||
data={inboxIssues?.find((i) => i.bridge_id === inboxIssueId)}
|
||||
/>
|
||||
<div className="grid grid-cols-4 border-b border-brand-base divide-x divide-brand-base">
|
||||
<div className="col-span-1 flex justify-between p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<InboxIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<h3 className="font-medium">Inbox</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
{isAllowed && (
|
||||
<div
|
||||
className={`flex-shrink-0 ${
|
||||
issueStatus === 0 || issueStatus === -2 ? "" : "opacity-70"
|
||||
}`}
|
||||
<FiltersDropdown />
|
||||
</div>
|
||||
{inboxIssueId && (
|
||||
<div className="flex justify-between items-center gap-4 px-4 col-span-3">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-brand-base bg-brand-surface-1 p-1.5 hover:bg-brand-surface-2"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "ArrowUp" });
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
>
|
||||
<Popover className="relative">
|
||||
<Popover.Button
|
||||
as="button"
|
||||
type="button"
|
||||
disabled={!(issueStatus === 0 || issueStatus === -2)}
|
||||
<ChevronUpIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-brand-base bg-brand-surface-1 p-1.5 hover:bg-brand-surface-2"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "ArrowDown" });
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
>
|
||||
<ChevronDownIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<div className="text-sm">
|
||||
{currentIssueIndex + 1}/{inboxIssues?.length ?? 0}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
{isAllowed && (issueStatus === 0 || issueStatus === -2) && (
|
||||
<div className="flex-shrink-0">
|
||||
<Popover className="relative">
|
||||
<Popover.Button as="button" type="button">
|
||||
<SecondaryButton className="flex gap-x-1 items-center" size="sm">
|
||||
<ClockIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<span>Snooze</span>
|
||||
</SecondaryButton>
|
||||
</Popover.Button>
|
||||
<Popover.Panel className="w-80 p-2 absolute right-0 z-10 mt-2 rounded-md border border-brand-base bg-brand-surface-2 shadow-lg">
|
||||
{({ close }) => (
|
||||
<div className="w-full h-full flex flex-col gap-y-1">
|
||||
<DatePicker
|
||||
selected={date ? new Date(date) : null}
|
||||
onChange={(val) => {
|
||||
if (!val) return;
|
||||
setDate(val);
|
||||
}}
|
||||
dateFormat="dd-MM-yyyy"
|
||||
minDate={tomorrow}
|
||||
inline
|
||||
/>
|
||||
<PrimaryButton
|
||||
className="ml-auto"
|
||||
onClick={() => {
|
||||
close();
|
||||
markInboxStatus({
|
||||
status: 0,
|
||||
snoozed_till: new Date(date),
|
||||
});
|
||||
}}
|
||||
>
|
||||
Snooze
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
{isAllowed && issueStatus === -2 && (
|
||||
<div className="flex-shrink-0">
|
||||
<SecondaryButton
|
||||
size="sm"
|
||||
className="flex gap-2 items-center"
|
||||
onClick={() => setSelectDuplicateIssue(true)}
|
||||
>
|
||||
<SecondaryButton
|
||||
className={`flex gap-x-1 items-center ${
|
||||
issueStatus === 0 || issueStatus === -2 ? "" : "cursor-not-allowed"
|
||||
}`}
|
||||
size="sm"
|
||||
>
|
||||
<ClockIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<span>Snooze</span>
|
||||
</SecondaryButton>
|
||||
</Popover.Button>
|
||||
<Popover.Panel className="w-80 p-2 absolute right-0 z-10 mt-2 rounded-md border border-brand-base bg-brand-surface-2 shadow-lg">
|
||||
{({ close }) => (
|
||||
<div className="w-full h-full flex flex-col gap-y-1">
|
||||
<DatePicker
|
||||
selected={date ? new Date(date) : null}
|
||||
onChange={(val) => {
|
||||
if (!val) return;
|
||||
setDate(val);
|
||||
}}
|
||||
dateFormat="dd-MM-yyyy"
|
||||
minDate={tomorrow}
|
||||
inline
|
||||
/>
|
||||
<PrimaryButton
|
||||
className="ml-auto"
|
||||
onClick={() => {
|
||||
close();
|
||||
onSnooze(date);
|
||||
}}
|
||||
>
|
||||
Snooze
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
{isAllowed && (
|
||||
<div className={`flex gap-3 flex-wrap ${issueStatus !== -2 ? "opacity-70" : ""}`}>
|
||||
<SecondaryButton
|
||||
size="sm"
|
||||
className="flex gap-2 items-center"
|
||||
onClick={onMarkAsDuplicate}
|
||||
disabled={issueStatus !== -2}
|
||||
>
|
||||
<StackedLayersHorizontalIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<span>Mark as duplicate</span>
|
||||
</SecondaryButton>
|
||||
<SecondaryButton
|
||||
size="sm"
|
||||
className="flex gap-2 items-center"
|
||||
onClick={handleAcceptIssue}
|
||||
disabled={issueStatus !== -2}
|
||||
loading={isAccepting}
|
||||
>
|
||||
<CheckCircleIcon className="h-4 w-4 text-green-500" />
|
||||
<span>{isAccepting ? "Accepting..." : "Accept"}</span>
|
||||
</SecondaryButton>
|
||||
<SecondaryButton
|
||||
size="sm"
|
||||
className="flex gap-2 items-center"
|
||||
onClick={onDecline}
|
||||
disabled={issueStatus !== -2}
|
||||
>
|
||||
<XCircleIcon className="h-4 w-4 text-red-500" />
|
||||
<span>Decline</span>
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
)}
|
||||
{(isAllowed || user?.id === issue?.created_by) && (
|
||||
<div className="flex-shrink-0">
|
||||
<SecondaryButton size="sm" className="flex gap-2 items-center" onClick={onDelete}>
|
||||
<TrashIcon className="h-4 w-4 text-red-500" />
|
||||
<span>Delete</span>
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
)}
|
||||
<StackedLayersHorizontalIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<span>Mark as duplicate</span>
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
)}
|
||||
{isAllowed && (issueStatus === 0 || issueStatus === -2) && (
|
||||
<div className="flex-shrink-0">
|
||||
<SecondaryButton
|
||||
size="sm"
|
||||
className="flex gap-2 items-center"
|
||||
onClick={handleAcceptIssue}
|
||||
loading={isAccepting}
|
||||
>
|
||||
<CheckCircleIcon className="h-4 w-4 text-green-500" />
|
||||
<span>{isAccepting ? "Accepting..." : "Accept"}</span>
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
)}
|
||||
{isAllowed && issueStatus === -2 && (
|
||||
<div className="flex-shrink-0">
|
||||
<SecondaryButton
|
||||
size="sm"
|
||||
className="flex gap-2 items-center"
|
||||
onClick={() => setDeclineIssueModal(true)}
|
||||
>
|
||||
<XCircleIcon className="h-4 w-4 text-red-500" />
|
||||
<span>Decline</span>
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
)}
|
||||
{(isAllowed || user?.id === issue?.created_by) && (
|
||||
<div className="flex-shrink-0">
|
||||
<SecondaryButton
|
||||
size="sm"
|
||||
className="flex gap-2 items-center"
|
||||
onClick={() => setDeleteIssueModal(true)}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4 text-red-500" />
|
||||
<span>Delete</span>
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -4,13 +4,21 @@ import Link from "next/link";
|
||||
// ui
|
||||
import { Tooltip } from "components/ui";
|
||||
// icons
|
||||
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
||||
import { CalendarDaysIcon, ClockIcon } from "@heroicons/react/24/outline";
|
||||
import { getPriorityIcon } from "components/icons";
|
||||
import {
|
||||
CalendarDaysIcon,
|
||||
CheckCircleIcon,
|
||||
ClockIcon,
|
||||
DocumentDuplicateIcon,
|
||||
ExclamationTriangleIcon,
|
||||
XCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// types
|
||||
import type { IInboxIssue } from "types";
|
||||
// constants
|
||||
import { INBOX_STATUS } from "constants/inbox";
|
||||
|
||||
type Props = {
|
||||
issue: IInboxIssue;
|
||||
@ -30,93 +38,88 @@ export const InboxIssueCard: React.FC<Props> = (props) => {
|
||||
href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}?inboxIssueId=${issue.bridge_id}`}
|
||||
>
|
||||
<a>
|
||||
<Tooltip
|
||||
tooltipContent={
|
||||
issueStatus === -2
|
||||
? "Pending issue"
|
||||
: issueStatus === -1
|
||||
? "Declined issue"
|
||||
: issueStatus === 0
|
||||
? "Snoozed issue"
|
||||
: issueStatus === 1
|
||||
? "Accepted issue"
|
||||
: "Marked as duplicate"
|
||||
}
|
||||
position="right"
|
||||
<div
|
||||
id={issue.id}
|
||||
className={`relative min-h-[5rem] cursor-pointer select-none space-y-3 py-2 px-4 border-b border-brand-base hover:bg-brand-accent hover:bg-opacity-10 ${
|
||||
active ? "bg-brand-accent bg-opacity-5" : " "
|
||||
} ${issue.issue_inbox[0].status !== -2 ? "opacity-60" : ""}`}
|
||||
>
|
||||
<div
|
||||
id={issue.id}
|
||||
className={`relative min-h-[5rem] cursor-pointer select-none space-y-3 py-2 px-4 border-b border-brand-base hover:bg-brand-accent hover:bg-opacity-10 ${
|
||||
active ? "bg-brand-accent bg-opacity-5" : " "
|
||||
} ${issue.issue_inbox[0].status !== -2 ? "opacity-60" : ""}`}
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="flex-shrink-0 text-brand-secondary text-xs">
|
||||
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||
</p>
|
||||
<h5 className="truncate text-sm">{issue.name}</h5>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Tooltip
|
||||
tooltipHeading="State"
|
||||
tooltipContent={addSpaceIfCamelCase(issue.state_detail?.name ?? "Triage")}
|
||||
>
|
||||
<div className="flex items-center gap-2 rounded border border-brand-base shadow-sm text-xs px-2 py-[0.19rem] text-brand-secondary">
|
||||
{getStateGroupIcon(
|
||||
issue.state_detail?.group ?? "backlog",
|
||||
"14",
|
||||
"14",
|
||||
issue.state_detail?.color
|
||||
)}
|
||||
{issue.state_detail?.name ?? "Triage"}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip tooltipHeading="Priority" tooltipContent={`${issue.priority ?? "None"}`}>
|
||||
<div
|
||||
className={`grid h-6 w-6 place-items-center rounded border items-center shadow-sm ${
|
||||
issue.priority === "urgent"
|
||||
? "border-red-500/20 bg-red-500/20 text-red-500"
|
||||
: issue.priority === "high"
|
||||
? "border-orange-500/20 bg-orange-500/20 text-orange-500"
|
||||
: issue.priority === "medium"
|
||||
? "border-yellow-500/20 bg-yellow-500/20 text-yellow-500"
|
||||
: issue.priority === "low"
|
||||
? "border-green-500/20 bg-green-500/20 text-green-500"
|
||||
: "border-brand-base"
|
||||
}`}
|
||||
>
|
||||
{getPriorityIcon(
|
||||
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
|
||||
"text-sm"
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
tooltipHeading="Created at"
|
||||
tooltipContent={`${renderShortNumericDateFormat(issue.created_at ?? "")}`}
|
||||
>
|
||||
<div className="flex items-center gap-1 rounded border border-brand-base shadow-sm text-xs px-2 py-[0.19rem] text-brand-secondary">
|
||||
<CalendarDaysIcon className="h-3.5 w-3.5" />
|
||||
<span>{renderShortNumericDateFormat(issue.created_at ?? "")}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{issue.issue_inbox[0].snoozed_till && (
|
||||
<div
|
||||
className={`text-xs flex items-center gap-1 ${
|
||||
new Date(issue.issue_inbox[0].snoozed_till ?? "") < new Date()
|
||||
? "text-red-500"
|
||||
: "text-blue-500"
|
||||
}`}
|
||||
>
|
||||
<ClockIcon className="h-3.5 w-3.5" />
|
||||
<span>
|
||||
Snoozed till {renderShortNumericDateFormat(issue.issue_inbox[0].snoozed_till)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="flex-shrink-0 text-brand-secondary text-xs">
|
||||
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||
</p>
|
||||
<h5 className="truncate text-sm">{issue.name}</h5>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Tooltip tooltipHeading="Priority" tooltipContent={`${issue.priority ?? "None"}`}>
|
||||
<div
|
||||
className={`grid h-6 w-6 place-items-center rounded border items-center shadow-sm ${
|
||||
issue.priority === "urgent"
|
||||
? "border-red-500/20 bg-red-500/20 text-red-500"
|
||||
: issue.priority === "high"
|
||||
? "border-orange-500/20 bg-orange-500/20 text-orange-500"
|
||||
: issue.priority === "medium"
|
||||
? "border-yellow-500/20 bg-yellow-500/20 text-yellow-500"
|
||||
: issue.priority === "low"
|
||||
? "border-green-500/20 bg-green-500/20 text-green-500"
|
||||
: "border-brand-base"
|
||||
}`}
|
||||
>
|
||||
{getPriorityIcon(
|
||||
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
|
||||
"text-sm"
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
tooltipHeading="Created at"
|
||||
tooltipContent={`${renderShortNumericDateFormat(issue.created_at ?? "")}`}
|
||||
>
|
||||
<div className="flex items-center gap-1 rounded border border-brand-base shadow-sm text-xs px-2 py-[0.19rem] text-brand-secondary">
|
||||
<CalendarDaysIcon className="h-3.5 w-3.5" />
|
||||
<span>{renderShortNumericDateFormat(issue.created_at ?? "")}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
className={`text-xs flex items-center justify-end gap-1 w-full ${
|
||||
issueStatus === 0 && new Date(issue.issue_inbox[0].snoozed_till ?? "") < new Date()
|
||||
? "text-red-500"
|
||||
: INBOX_STATUS.find((s) => s.value === issueStatus)?.textColor
|
||||
}`}
|
||||
>
|
||||
{issueStatus === -2 ? (
|
||||
<>
|
||||
<ExclamationTriangleIcon className="h-3.5 w-3.5" />
|
||||
<span>Pending</span>
|
||||
</>
|
||||
) : issueStatus === -1 ? (
|
||||
<>
|
||||
<XCircleIcon className="h-3.5 w-3.5" />
|
||||
<span>Declined</span>
|
||||
</>
|
||||
) : issueStatus === 0 ? (
|
||||
<>
|
||||
<ClockIcon className="h-3.5 w-3.5" />
|
||||
<span>
|
||||
{new Date(issue.issue_inbox[0].snoozed_till ?? "") < new Date()
|
||||
? "Snoozed date passed"
|
||||
: "Snoozed"}
|
||||
</span>
|
||||
</>
|
||||
) : issueStatus === 1 ? (
|
||||
<>
|
||||
<CheckCircleIcon className="h-3.5 w-3.5" />
|
||||
<span>Accepted</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DocumentDuplicateIcon className="h-3.5 w-3.5" />
|
||||
<span>Duplicate</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import Router, { useRouter } from "next/router";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
@ -29,6 +29,7 @@ import {
|
||||
ClockIcon,
|
||||
DocumentDuplicateIcon,
|
||||
ExclamationTriangleIcon,
|
||||
InboxIcon,
|
||||
XCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
@ -37,6 +38,8 @@ import { renderShortNumericDateFormat } from "helpers/date-time.helper";
|
||||
import type { IInboxIssue, IIssue } from "types";
|
||||
// fetch-keys
|
||||
import { INBOX_ISSUES, INBOX_ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { INBOX_STATUS } from "constants/inbox";
|
||||
|
||||
const defaultValues = {
|
||||
name: "",
|
||||
@ -55,7 +58,7 @@ export const InboxMainContent: React.FC = () => {
|
||||
|
||||
const { user } = useUserAuth();
|
||||
const { memberRole } = useProjectMyMembership();
|
||||
const { params } = useInboxView();
|
||||
const { params, issues: inboxIssues } = useInboxView();
|
||||
|
||||
const { reset, control, watch } = useForm<IIssue>({
|
||||
defaultValues,
|
||||
@ -76,17 +79,6 @@ export const InboxMainContent: React.FC = () => {
|
||||
: null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!issueDetails || !inboxIssueId) return;
|
||||
|
||||
reset({
|
||||
...issueDetails,
|
||||
assignees_list:
|
||||
issueDetails.assignees_list ?? (issueDetails.assignee_details ?? []).map((user) => user.id),
|
||||
labels_list: issueDetails.labels_list ?? issueDetails.labels,
|
||||
});
|
||||
}, [issueDetails, reset, inboxIssueId]);
|
||||
|
||||
const submitChanges = useCallback(
|
||||
async (formData: Partial<IInboxIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !inboxIssueId || !inboxId || !issueDetails) return;
|
||||
@ -144,7 +136,86 @@ export const InboxMainContent: React.FC = () => {
|
||||
]
|
||||
);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (!inboxIssues || !inboxIssueId) return;
|
||||
|
||||
const currentIssueIndex = inboxIssues.findIndex((issue) => issue.bridge_id === inboxIssueId);
|
||||
|
||||
switch (e.key) {
|
||||
case "ArrowUp":
|
||||
Router.push({
|
||||
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
|
||||
query: {
|
||||
inboxIssueId:
|
||||
currentIssueIndex === 0
|
||||
? inboxIssues[inboxIssues.length - 1].bridge_id
|
||||
: inboxIssues[currentIssueIndex - 1].bridge_id,
|
||||
},
|
||||
});
|
||||
break;
|
||||
case "ArrowDown":
|
||||
Router.push({
|
||||
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
|
||||
query: {
|
||||
inboxIssueId:
|
||||
currentIssueIndex === inboxIssues.length - 1
|
||||
? inboxIssues[0].bridge_id
|
||||
: inboxIssues[currentIssueIndex + 1].bridge_id,
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[workspaceSlug, projectId, inboxIssueId, inboxId, inboxIssues]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [onKeyDown]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!issueDetails || !inboxIssueId) return;
|
||||
|
||||
reset({
|
||||
...issueDetails,
|
||||
assignees_list:
|
||||
issueDetails.assignees_list ?? (issueDetails.assignee_details ?? []).map((user) => user.id),
|
||||
labels_list: issueDetails.labels_list ?? issueDetails.labels,
|
||||
});
|
||||
}, [issueDetails, reset, inboxIssueId]);
|
||||
|
||||
const issueStatus = issueDetails?.issue_inbox[0].status;
|
||||
const inboxStatusDetails = INBOX_STATUS.find((s) => s.value === issueStatus);
|
||||
|
||||
if (!inboxIssueId)
|
||||
return (
|
||||
<div className="h-full p-4 grid place-items-center text-brand-secondary">
|
||||
<div className="grid h-full place-items-center">
|
||||
<div className="my-5 flex flex-col items-center gap-4">
|
||||
<InboxIcon height={60} width={60} />
|
||||
{inboxIssues && inboxIssues.length > 0 ? (
|
||||
<span className="text-brand-secondary">
|
||||
{inboxIssues?.length} issues found. Select an issue from the sidebar to view its
|
||||
details.
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-brand-secondary">
|
||||
No issues found. Use{" "}
|
||||
<pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre> shortcut to
|
||||
create a new issue
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -153,19 +224,10 @@ export const InboxMainContent: React.FC = () => {
|
||||
<div className="basis-2/3 h-full overflow-auto p-5 space-y-3">
|
||||
<div
|
||||
className={`flex items-center gap-2 p-3 text-sm border rounded-md ${
|
||||
issueStatus === -2
|
||||
? "text-orange-500 border-orange-500 bg-orange-500/10"
|
||||
: issueStatus === -1
|
||||
issueStatus === 0 &&
|
||||
new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date()
|
||||
? "text-red-500 border-red-500 bg-red-500/10"
|
||||
: issueStatus === 0
|
||||
? new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date()
|
||||
? "text-red-500 border-red-500 bg-red-500/10"
|
||||
: "text-blue-500 border-blue-500 bg-blue-500/10"
|
||||
: issueStatus === 1
|
||||
? "text-green-500 border-green-500 bg-green-500/10"
|
||||
: issueStatus === 2
|
||||
? "text-yellow-500 border-yellow-500 bg-yellow-500/10"
|
||||
: ""
|
||||
: `${inboxStatusDetails?.textColor} ${inboxStatusDetails?.bgColor} ${inboxStatusDetails?.borderColor}`
|
||||
}`}
|
||||
>
|
||||
{issueStatus === -2 ? (
|
||||
@ -266,6 +328,4 @@ export const InboxMainContent: React.FC = () => {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
@ -163,6 +163,8 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
const handleCreateUpdateIssue = async (formData: Partial<IIssue>) => {
|
||||
await handleFormSubmit(formData);
|
||||
|
||||
setGptAssistantModal(false);
|
||||
|
||||
reset({
|
||||
...defaultValues,
|
||||
project: projectId,
|
||||
|
@ -188,6 +188,10 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
message: "Issue created successfully.",
|
||||
});
|
||||
|
||||
router.push(
|
||||
`/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}?inboxIssueId=${res.issue_inbox[0].id}`
|
||||
);
|
||||
|
||||
mutate(INBOX_ISSUES(inboxId.toString(), inboxParams));
|
||||
})
|
||||
.catch(() => {
|
||||
|
@ -3,26 +3,41 @@ export const INBOX_STATUS = [
|
||||
key: "pending",
|
||||
label: "Pending",
|
||||
value: -2,
|
||||
textColor: "text-yellow-500",
|
||||
bgColor: "bg-yellow-500/10",
|
||||
borderColor: "border-yellow-500",
|
||||
},
|
||||
{
|
||||
key: "declined",
|
||||
label: "Declined",
|
||||
value: -1,
|
||||
textColor: "text-red-500",
|
||||
bgColor: "bg-red-500/10",
|
||||
borderColor: "border-red-500",
|
||||
},
|
||||
{
|
||||
key: "snoozed",
|
||||
label: "Snoozed",
|
||||
value: 0,
|
||||
textColor: "text-brand-secondary",
|
||||
bgColor: "bg-gray-500/10",
|
||||
borderColor: "border-gray-500",
|
||||
},
|
||||
{
|
||||
key: "accepted",
|
||||
label: "Accepted",
|
||||
value: 1,
|
||||
textColor: "text-green-500",
|
||||
bgColor: "bg-green-500/10",
|
||||
borderColor: "border-green-500",
|
||||
},
|
||||
{
|
||||
key: "duplicate",
|
||||
label: "Duplicate",
|
||||
value: 2,
|
||||
textColor: "text-brand-secondary",
|
||||
bgColor: "bg-gray-500/10",
|
||||
borderColor: "border-gray-500",
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -49,7 +49,7 @@ const WorkspacePage: NextPage = () => {
|
||||
)}
|
||||
<div className="p-8">
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="text-brand-muted-1 flex flex-col justify-between gap-x-2 gap-y-6 rounded-lg border border-brand-base bg-brand-base px-8 py-6 md:flex-row md:items-center md:py-3">
|
||||
<div className="text-brand-muted-1 flex flex-col justify-between gap-x-2 gap-y-6 rounded-lg border border-brand-base bg-brand-base px-4 py-6 md:flex-row md:items-center md:py-3">
|
||||
<p className="font-semibold">
|
||||
Plane is open source, support us by starring us on GitHub.
|
||||
</p>
|
||||
|
@ -1,29 +1,13 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import Router, { useRouter } from "next/router";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import inboxServices from "services/inbox.service";
|
||||
import projectService from "services/project.service";
|
||||
// hooks
|
||||
import useInboxView from "hooks/use-inbox-view";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
import useToast from "hooks/use-toast";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
// layouts
|
||||
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
||||
// contexts
|
||||
import { InboxViewContextProvider } from "contexts/inbox-view-context";
|
||||
// components
|
||||
import {
|
||||
InboxActionHeader,
|
||||
InboxMainContent,
|
||||
SelectDuplicateInboxIssueModal,
|
||||
DeclineIssueModal,
|
||||
DeleteIssueModal,
|
||||
IssuesListSidebar,
|
||||
} from "components/inbox";
|
||||
import { InboxActionHeader, InboxMainContent, IssuesListSidebar } from "components/inbox";
|
||||
// helper
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
// ui
|
||||
@ -31,123 +15,14 @@ import { PrimaryButton } from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// icons
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
import { InboxIcon } from "components/icons";
|
||||
// types
|
||||
import { IInboxIssueDetail, TInboxStatus } from "types";
|
||||
import type { NextPage } from "next";
|
||||
// fetch-keys
|
||||
import { INBOX_ISSUE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
const ProjectInbox: NextPage = () => {
|
||||
const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false);
|
||||
const [declineIssueModal, setDeclineIssueModal] = useState(false);
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query;
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { user } = useUserAuth();
|
||||
const { issues: inboxIssues, mutate: mutateInboxIssues } = useInboxView();
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { data: projectDetails } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => projectService.getProject(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (!inboxIssues || !inboxIssueId) return;
|
||||
|
||||
const currentIssueIndex = inboxIssues.findIndex((issue) => issue.bridge_id === inboxIssueId);
|
||||
|
||||
switch (e.key) {
|
||||
case "ArrowUp":
|
||||
Router.push({
|
||||
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
|
||||
query: {
|
||||
inboxIssueId:
|
||||
currentIssueIndex === 0
|
||||
? inboxIssues[inboxIssues.length - 1].bridge_id
|
||||
: inboxIssues[currentIssueIndex - 1].bridge_id,
|
||||
},
|
||||
});
|
||||
break;
|
||||
case "ArrowDown":
|
||||
Router.push({
|
||||
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
|
||||
query: {
|
||||
inboxIssueId:
|
||||
currentIssueIndex === inboxIssues.length - 1
|
||||
? inboxIssues[0].bridge_id
|
||||
: inboxIssues[currentIssueIndex + 1].bridge_id,
|
||||
},
|
||||
});
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[workspaceSlug, projectId, inboxIssueId, inboxId, inboxIssues]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [onKeyDown]);
|
||||
|
||||
const markInboxStatus = async (data: TInboxStatus) => {
|
||||
if (!workspaceSlug || !projectId || !inboxId || !inboxIssueId) return;
|
||||
|
||||
mutate<IInboxIssueDetail>(
|
||||
INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string),
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
issue_inbox: [{ ...prevData.issue_inbox[0], ...data }],
|
||||
};
|
||||
},
|
||||
false
|
||||
);
|
||||
mutateInboxIssues(
|
||||
(prevData) =>
|
||||
(prevData ?? []).map((i) =>
|
||||
i.bridge_id === inboxIssueId
|
||||
? { ...i, issue_inbox: [{ ...i.issue_inbox[0], ...data }] }
|
||||
: i
|
||||
),
|
||||
false
|
||||
);
|
||||
|
||||
await inboxServices
|
||||
.markInboxStatus(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
inboxId.toString(),
|
||||
inboxIssues?.find((inboxIssue) => inboxIssue.bridge_id === inboxIssueId)?.bridge_id!,
|
||||
data,
|
||||
user
|
||||
)
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Something went wrong while updating inbox status. Please try again.",
|
||||
})
|
||||
)
|
||||
.finally(() => {
|
||||
mutate(INBOX_ISSUE_DETAILS(inboxId as string, inboxIssueId as string));
|
||||
mutateInboxIssues();
|
||||
});
|
||||
};
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
return (
|
||||
<InboxViewContextProvider>
|
||||
@ -175,88 +50,15 @@ const ProjectInbox: NextPage = () => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<>
|
||||
<SelectDuplicateInboxIssueModal
|
||||
isOpen={selectDuplicateIssue}
|
||||
onClose={() => setSelectDuplicateIssue(false)}
|
||||
value={
|
||||
inboxIssues?.find((inboxIssue) => inboxIssue.bridge_id === inboxIssueId)
|
||||
?.issue_inbox[0].duplicate_to
|
||||
}
|
||||
onSubmit={(dupIssueId: string) => {
|
||||
markInboxStatus({
|
||||
status: 2,
|
||||
duplicate_to: dupIssueId,
|
||||
}).finally(() => setSelectDuplicateIssue(false));
|
||||
}}
|
||||
/>
|
||||
<DeclineIssueModal
|
||||
isOpen={declineIssueModal}
|
||||
handleClose={() => setDeclineIssueModal(false)}
|
||||
data={inboxIssues?.find((i) => i.bridge_id === inboxIssueId)}
|
||||
onSubmit={async () => {
|
||||
await markInboxStatus({
|
||||
status: -1,
|
||||
}).finally(() => setDeclineIssueModal(false));
|
||||
}}
|
||||
/>
|
||||
<DeleteIssueModal
|
||||
isOpen={deleteIssueModal}
|
||||
handleClose={() => setDeleteIssueModal(false)}
|
||||
data={inboxIssues?.find((i) => i.bridge_id === inboxIssueId)}
|
||||
/>
|
||||
<div className="flex flex-col h-full">
|
||||
<InboxActionHeader
|
||||
issue={inboxIssues?.find((issue) => issue.bridge_id === inboxIssueId)}
|
||||
currentIssueIndex={
|
||||
inboxIssues?.findIndex((issue) => issue.bridge_id === inboxIssueId) ?? 0
|
||||
}
|
||||
issueCount={inboxIssues?.length ?? 0}
|
||||
onAccept={() =>
|
||||
markInboxStatus({
|
||||
status: 1,
|
||||
})
|
||||
}
|
||||
onDecline={() => setDeclineIssueModal(true)}
|
||||
onMarkAsDuplicate={() => setSelectDuplicateIssue(true)}
|
||||
onSnooze={(date) => {
|
||||
markInboxStatus({
|
||||
status: 0,
|
||||
snoozed_till: new Date(date),
|
||||
});
|
||||
}}
|
||||
onDelete={() => setDeleteIssueModal(true)}
|
||||
/>
|
||||
<div className="grid grid-cols-4 flex-1 divide-x divide-brand-base overflow-hidden">
|
||||
<IssuesListSidebar />
|
||||
<div className="col-span-3 h-full overflow-auto">
|
||||
{inboxIssueId ? (
|
||||
<InboxMainContent />
|
||||
) : (
|
||||
<div className="h-full p-4 grid place-items-center text-brand-secondary">
|
||||
<div className="grid h-full place-items-center">
|
||||
<div className="my-5 flex flex-col items-center gap-4">
|
||||
<InboxIcon height={60} width={60} />
|
||||
{inboxIssues && inboxIssues.length > 0 ? (
|
||||
<span className="text-brand-secondary">
|
||||
{inboxIssues?.length} issues found. Select an issue from the sidebar to
|
||||
view its details.
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-brand-secondary">
|
||||
No issues found. Use{" "}
|
||||
<pre className="inline rounded bg-brand-surface-2 px-2 py-1">C</pre>{" "}
|
||||
shortcut to create a new issue
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col h-full">
|
||||
<InboxActionHeader />
|
||||
<div className="grid grid-cols-4 flex-1 divide-x divide-brand-base overflow-hidden">
|
||||
<IssuesListSidebar />
|
||||
<div className="col-span-3 h-full overflow-auto">
|
||||
<InboxMainContent />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</ProjectAuthorizationWrapper>
|
||||
</InboxViewContextProvider>
|
||||
);
|
||||
|
@ -162,7 +162,7 @@ class InboxServices extends APIService {
|
||||
inboxId: string,
|
||||
data: any,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<IInboxIssue> {
|
||||
): Promise<IInboxIssueDetail> {
|
||||
return this.post(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/`,
|
||||
data
|
||||
|
Loading…
Reference in New Issue
Block a user