plane/web/components/inbox/inbox-issue-actions.tsx
guru_sainath b66f07845a
chore: inbox issue restructure the components and store (#3456)
* chore: inbox-issues store and type updates

* chore: issue inbox payload change for GET and POST

* chore: issue inbox payload change for PATCH

* chore: inbox-issue new hooks and store updates

* chore: update inbox issue template.

* chore: UI root

* chore: sidebar issues render

* chore: inbox issue details page layout.

* chore: inbox issue filters

* chore: inbox issue status card.

* chore: add loader.

* chore: active inbox issue styles.

* chore: inbox filters

* chore: inbox applied filters UI

* chore: inbox issue approval header

* chore: inbox issue approval header operations

* chore: issue reaction and activity fetch in issue_inbox store

* chore: posthog enabled

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
2024-01-24 20:33:54 +05:30

330 lines
11 KiB
TypeScript

import { FC, useEffect, useMemo, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import DatePicker from "react-datepicker";
import { Popover } from "@headlessui/react";
// hooks
import { useApplication, useUser, useInboxIssues, useIssueDetail, useWorkspace } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import {
AcceptIssueModal,
DeclineIssueModal,
DeleteInboxIssueModal,
SelectDuplicateInboxIssueModal,
} from "components/inbox";
// ui
import { Button } from "@plane/ui";
// icons
import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Inbox, Trash2, XCircle } from "lucide-react";
// types
import type { TInboxStatus, TInboxDetailedStatus } from "@plane/types";
import { EUserProjectRoles } from "constants/project";
type TInboxIssueActionsHeader = {
workspaceSlug: string;
projectId: string;
inboxId: string;
inboxIssueId: string | undefined;
};
type TInboxIssueOperations = {
updateInboxIssueStatus: (data: TInboxStatus) => Promise<void>;
removeInboxIssue: () => Promise<void>;
};
export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((props) => {
const { workspaceSlug, projectId, inboxId, inboxIssueId } = props;
// router
const router = useRouter();
// hooks
const {
eventTracker: { postHogEventTracker },
} = useApplication();
const { currentWorkspace } = useWorkspace();
const {
issues: { getInboxIssuesByInboxId, getInboxIssueByIssueId, updateInboxIssueStatus, removeInboxIssue },
} = useInboxIssues();
const {
issue: { getIssueById },
} = useIssueDetail();
const {
currentUser,
membership: { currentProjectRole },
} = useUser();
const { setToastAlert } = useToast();
// states
const [date, setDate] = useState(new Date());
const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false);
const [acceptIssueModal, setAcceptIssueModal] = useState(false);
const [declineIssueModal, setDeclineIssueModal] = useState(false);
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
// derived values
const inboxIssues = getInboxIssuesByInboxId(inboxId);
const issueStatus = (inboxIssueId && inboxId && getInboxIssueByIssueId(inboxId, inboxIssueId)) || undefined;
const issue = (inboxIssueId && getIssueById(inboxIssueId)) || undefined;
const currentIssueIndex = inboxIssues?.findIndex((issue) => issue === inboxIssueId) ?? 0;
const inboxIssueOperations: TInboxIssueOperations = useMemo(
() => ({
updateInboxIssueStatus: async (data: TInboxDetailedStatus) => {
try {
if (!workspaceSlug || !projectId || !inboxId || !inboxIssueId) throw new Error("Missing required parameters");
await updateInboxIssueStatus(workspaceSlug, projectId, inboxId, inboxIssueId, data);
} catch (error) {
setToastAlert({
type: "error",
title: "Error!",
message: "Something went wrong while updating inbox status. Please try again.",
});
}
},
removeInboxIssue: async () => {
try {
if (!workspaceSlug || !projectId || !inboxId || !inboxIssueId || !currentWorkspace)
throw new Error("Missing required parameters");
await removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId);
postHogEventTracker(
"ISSUE_DELETED",
{
state: "SUCCESS",
},
{
isGrouping: true,
groupType: "Workspace_metrics",
groupId: currentWorkspace?.id!,
}
);
router.push({
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
});
} catch (error) {
setToastAlert({
type: "error",
title: "Error!",
message: "Something went wrong while deleting inbox issue. Please try again.",
});
postHogEventTracker(
"ISSUE_DELETED",
{
state: "FAILED",
},
{
isGrouping: true,
groupType: "Workspace_metrics",
groupId: currentWorkspace?.id!,
}
);
}
},
}),
[
currentWorkspace,
workspaceSlug,
projectId,
inboxId,
inboxIssueId,
updateInboxIssueStatus,
removeInboxIssue,
setToastAlert,
postHogEventTracker,
router,
]
);
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
useEffect(() => {
if (!issueStatus || !issueStatus.snoozed_till) return;
setDate(new Date(issueStatus.snoozed_till));
}, [issueStatus]);
if (!issueStatus || !issue || !inboxIssues) return <></>;
return (
<>
{issue && (
<>
<SelectDuplicateInboxIssueModal
isOpen={selectDuplicateIssue}
onClose={() => setSelectDuplicateIssue(false)}
value={issueStatus.duplicate_to}
onSubmit={(dupIssueId) => {
inboxIssueOperations
.updateInboxIssueStatus({
status: 2,
duplicate_to: dupIssueId,
})
.finally(() => setSelectDuplicateIssue(false));
}}
/>
<AcceptIssueModal
data={issue}
isOpen={acceptIssueModal}
onClose={() => setAcceptIssueModal(false)}
onSubmit={async () => {
await inboxIssueOperations
.updateInboxIssueStatus({
status: 1,
})
.finally(() => setAcceptIssueModal(false));
}}
/>
<DeclineIssueModal
data={issue}
isOpen={declineIssueModal}
onClose={() => setDeclineIssueModal(false)}
onSubmit={async () => {
await inboxIssueOperations
.updateInboxIssueStatus({
status: -1,
})
.finally(() => setDeclineIssueModal(false));
}}
/>
<DeleteInboxIssueModal
data={issue}
isOpen={deleteIssueModal}
onClose={() => setDeleteIssueModal(false)}
onSubmit={async () => {
await inboxIssueOperations.removeInboxIssue().finally(() => setDeclineIssueModal(false));
}}
/>
</>
)}
{inboxIssueId && (
<div className="px-4 w-full h-full relative flex items-center gap-2 justify-between">
<div className="flex items-center gap-x-2">
<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);
}}
>
<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);
}}
>
<ChevronDown size={14} strokeWidth={2} />
</button>
<div className="text-sm">
{currentIssueIndex + 1}/{inboxIssues?.length ?? 0}
</div>
</div>
<div className="flex flex-wrap items-center gap-3">
{isAllowed && (issueStatus.status === 0 || issueStatus.status === -2) && (
<div className="flex-shrink-0">
<Popover className="relative">
<Popover.Button as="button" type="button">
<Button variant="neutral-primary" prependIcon={<Clock size={14} strokeWidth={2} />} size="sm">
Snooze
</Button>
</Popover.Button>
<Popover.Panel className="absolute right-0 z-10 mt-2 w-80 rounded-md bg-custom-background-100 p-2 shadow-lg">
{({ close }) => (
<div className="flex h-full w-full 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
/>
<Button
variant="primary"
onClick={() => {
close();
inboxIssueOperations.updateInboxIssueStatus({
status: 0,
snoozed_till: new Date(date),
});
}}
>
Snooze
</Button>
</div>
)}
</Popover.Panel>
</Popover>
</div>
)}
{isAllowed && issueStatus.status === -2 && (
<div className="flex-shrink-0">
<Button
variant="neutral-primary"
size="sm"
prependIcon={<FileStack size={14} strokeWidth={2} />}
onClick={() => setSelectDuplicateIssue(true)}
>
Mark as duplicate
</Button>
</div>
)}
{isAllowed && (issueStatus.status === 0 || issueStatus.status === -2) && (
<div className="flex-shrink-0">
<Button
variant="neutral-primary"
size="sm"
prependIcon={<CheckCircle2 className="text-green-500" size={14} strokeWidth={2} />}
onClick={() => setAcceptIssueModal(true)}
>
Accept
</Button>
</div>
)}
{isAllowed && issueStatus.status === -2 && (
<div className="flex-shrink-0">
<Button
variant="neutral-primary"
size="sm"
prependIcon={<XCircle className="text-red-500" size={14} strokeWidth={2} />}
onClick={() => setDeclineIssueModal(true)}
>
Decline
</Button>
</div>
)}
{(isAllowed || currentUser?.id === issue?.created_by) && (
<div className="flex-shrink-0">
<Button
variant="neutral-primary"
size="sm"
prependIcon={<Trash2 className="text-red-500" size={14} strokeWidth={2} />}
onClick={() => setDeleteIssueModal(true)}
>
Delete
</Button>
</div>
)}
</div>
</div>
)}
</>
);
});