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,26 +1,37 @@
|
|||||||
|
// hooks
|
||||||
|
import useInboxView from "hooks/use-inbox-view";
|
||||||
// ui
|
// ui
|
||||||
import { MultiLevelDropdown } from "components/ui";
|
import { MultiLevelDropdown } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { getPriorityIcon } from "components/icons";
|
import { getPriorityIcon } from "components/icons";
|
||||||
// types
|
|
||||||
import { IInboxFilterOptions } from "types";
|
|
||||||
// constants
|
// constants
|
||||||
import { PRIORITIES } from "constants/project";
|
import { PRIORITIES } from "constants/project";
|
||||||
import { INBOX_STATUS } from "constants/inbox";
|
import { INBOX_STATUS } from "constants/inbox";
|
||||||
|
|
||||||
type Props = {
|
export const FiltersDropdown: React.FC = () => {
|
||||||
filters: Partial<IInboxFilterOptions>;
|
const { filters, setFilters, filtersLength } = useInboxView();
|
||||||
onSelect: (option: any) => void;
|
|
||||||
direction?: "left" | "right";
|
|
||||||
height?: "sm" | "md" | "rg" | "lg";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FiltersDropdown: React.FC<Props> = ({ filters, onSelect, direction, height }) => (
|
return (
|
||||||
|
<div className="relative">
|
||||||
<MultiLevelDropdown
|
<MultiLevelDropdown
|
||||||
label="Filters"
|
label="Filters"
|
||||||
onSelect={onSelect}
|
onSelect={(option) => {
|
||||||
direction={direction}
|
const key = option.key as keyof typeof filters;
|
||||||
height={height}
|
|
||||||
|
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={[
|
options={[
|
||||||
{
|
{
|
||||||
id: "priority",
|
id: "priority",
|
||||||
@ -60,4 +71,11 @@ export const FiltersDropdown: React.FC<Props> = ({ filters, onSelect, direction,
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
);
|
{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 { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { mutate } from "swr";
|
||||||
|
|
||||||
// react-datepicker
|
// react-datepicker
|
||||||
import DatePicker from "react-datepicker";
|
import DatePicker from "react-datepicker";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Popover } from "@headlessui/react";
|
import { Popover } from "@headlessui/react";
|
||||||
// contexts
|
// contexts
|
||||||
import { useProjectMyMembership } from "contexts/project-member.context";
|
import { useProjectMyMembership } from "contexts/project-member.context";
|
||||||
|
// services
|
||||||
|
import inboxServices from "services/inbox.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useInboxView from "hooks/use-inbox-view";
|
import useInboxView from "hooks/use-inbox-view";
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { FiltersDropdown } from "components/inbox";
|
import {
|
||||||
|
DeclineIssueModal,
|
||||||
|
DeleteIssueModal,
|
||||||
|
FiltersDropdown,
|
||||||
|
SelectDuplicateInboxIssueModal,
|
||||||
|
} from "components/inbox";
|
||||||
// ui
|
// ui
|
||||||
import { PrimaryButton, SecondaryButton } from "components/ui";
|
import { PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -26,47 +36,84 @@ import {
|
|||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import type { IInboxIssue } from "types";
|
import type { IInboxIssueDetail, TInboxStatus } from "types";
|
||||||
|
// fetch-keys
|
||||||
type Props = {
|
import { INBOX_ISSUE_DETAILS } from "constants/fetch-keys";
|
||||||
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;
|
|
||||||
|
|
||||||
|
export const InboxActionHeader = () => {
|
||||||
const [isAccepting, setIsAccepting] = useState(false);
|
const [isAccepting, setIsAccepting] = useState(false);
|
||||||
const [date, setDate] = useState(new Date());
|
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 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 { 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 = () => {
|
const handleAcceptIssue = () => {
|
||||||
setIsAccepting(true);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!issue?.issue_inbox[0].snoozed_till) return;
|
if (!issue?.issue_inbox[0].snoozed_till) return;
|
||||||
|
|
||||||
@ -82,41 +129,43 @@ export const InboxActionHeader: React.FC<Props> = (props) => {
|
|||||||
tomorrow.setDate(today.getDate() + 1);
|
tomorrow.setDate(today.getDate() + 1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<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="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="col-span-1 flex justify-between p-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<InboxIcon className="h-4 w-4 text-brand-secondary" />
|
<InboxIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
<h3 className="font-medium">Inbox</h3>
|
<h3 className="font-medium">Inbox</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<FiltersDropdown />
|
||||||
<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>
|
</div>
|
||||||
{inboxIssueId && (
|
{inboxIssueId && (
|
||||||
<div className="flex justify-between items-center gap-4 px-4 col-span-3">
|
<div className="flex justify-between items-center gap-4 px-4 col-span-3">
|
||||||
@ -142,28 +191,15 @@ export const InboxActionHeader: React.FC<Props> = (props) => {
|
|||||||
<ChevronDownIcon className="h-3.5 w-3.5" />
|
<ChevronDownIcon className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
{currentIssueIndex + 1}/{issueCount}
|
{currentIssueIndex + 1}/{inboxIssues?.length ?? 0}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 flex-wrap">
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
{isAllowed && (
|
{isAllowed && (issueStatus === 0 || issueStatus === -2) && (
|
||||||
<div
|
<div className="flex-shrink-0">
|
||||||
className={`flex-shrink-0 ${
|
|
||||||
issueStatus === 0 || issueStatus === -2 ? "" : "opacity-70"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
<Popover.Button
|
<Popover.Button as="button" type="button">
|
||||||
as="button"
|
<SecondaryButton className="flex gap-x-1 items-center" size="sm">
|
||||||
type="button"
|
|
||||||
disabled={!(issueStatus === 0 || issueStatus === -2)}
|
|
||||||
>
|
|
||||||
<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" />
|
<ClockIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
<span>Snooze</span>
|
<span>Snooze</span>
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
@ -185,7 +221,10 @@ export const InboxActionHeader: React.FC<Props> = (props) => {
|
|||||||
className="ml-auto"
|
className="ml-auto"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
close();
|
close();
|
||||||
onSnooze(date);
|
markInboxStatus({
|
||||||
|
status: 0,
|
||||||
|
snoozed_till: new Date(date),
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Snooze
|
Snooze
|
||||||
@ -196,32 +235,37 @@ export const InboxActionHeader: React.FC<Props> = (props) => {
|
|||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isAllowed && (
|
{isAllowed && issueStatus === -2 && (
|
||||||
<div className={`flex gap-3 flex-wrap ${issueStatus !== -2 ? "opacity-70" : ""}`}>
|
<div className="flex-shrink-0">
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex gap-2 items-center"
|
className="flex gap-2 items-center"
|
||||||
onClick={onMarkAsDuplicate}
|
onClick={() => setSelectDuplicateIssue(true)}
|
||||||
disabled={issueStatus !== -2}
|
|
||||||
>
|
>
|
||||||
<StackedLayersHorizontalIcon className="h-4 w-4 text-brand-secondary" />
|
<StackedLayersHorizontalIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
<span>Mark as duplicate</span>
|
<span>Mark as duplicate</span>
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isAllowed && (issueStatus === 0 || issueStatus === -2) && (
|
||||||
|
<div className="flex-shrink-0">
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex gap-2 items-center"
|
className="flex gap-2 items-center"
|
||||||
onClick={handleAcceptIssue}
|
onClick={handleAcceptIssue}
|
||||||
disabled={issueStatus !== -2}
|
|
||||||
loading={isAccepting}
|
loading={isAccepting}
|
||||||
>
|
>
|
||||||
<CheckCircleIcon className="h-4 w-4 text-green-500" />
|
<CheckCircleIcon className="h-4 w-4 text-green-500" />
|
||||||
<span>{isAccepting ? "Accepting..." : "Accept"}</span>
|
<span>{isAccepting ? "Accepting..." : "Accept"}</span>
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isAllowed && issueStatus === -2 && (
|
||||||
|
<div className="flex-shrink-0">
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex gap-2 items-center"
|
className="flex gap-2 items-center"
|
||||||
onClick={onDecline}
|
onClick={() => setDeclineIssueModal(true)}
|
||||||
disabled={issueStatus !== -2}
|
|
||||||
>
|
>
|
||||||
<XCircleIcon className="h-4 w-4 text-red-500" />
|
<XCircleIcon className="h-4 w-4 text-red-500" />
|
||||||
<span>Decline</span>
|
<span>Decline</span>
|
||||||
@ -230,7 +274,11 @@ export const InboxActionHeader: React.FC<Props> = (props) => {
|
|||||||
)}
|
)}
|
||||||
{(isAllowed || user?.id === issue?.created_by) && (
|
{(isAllowed || user?.id === issue?.created_by) && (
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<SecondaryButton size="sm" className="flex gap-2 items-center" onClick={onDelete}>
|
<SecondaryButton
|
||||||
|
size="sm"
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
onClick={() => setDeleteIssueModal(true)}
|
||||||
|
>
|
||||||
<TrashIcon className="h-4 w-4 text-red-500" />
|
<TrashIcon className="h-4 w-4 text-red-500" />
|
||||||
<span>Delete</span>
|
<span>Delete</span>
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
@ -240,5 +288,6 @@ export const InboxActionHeader: React.FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,13 +4,21 @@ import Link from "next/link";
|
|||||||
// ui
|
// ui
|
||||||
import { Tooltip } from "components/ui";
|
import { Tooltip } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
import { getPriorityIcon } from "components/icons";
|
||||||
import { CalendarDaysIcon, ClockIcon } from "@heroicons/react/24/outline";
|
import {
|
||||||
|
CalendarDaysIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
|
ClockIcon,
|
||||||
|
DocumentDuplicateIcon,
|
||||||
|
ExclamationTriangleIcon,
|
||||||
|
XCircleIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
|
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
|
||||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
|
||||||
// types
|
// types
|
||||||
import type { IInboxIssue } from "types";
|
import type { IInboxIssue } from "types";
|
||||||
|
// constants
|
||||||
|
import { INBOX_STATUS } from "constants/inbox";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IInboxIssue;
|
issue: IInboxIssue;
|
||||||
@ -30,20 +38,6 @@ export const InboxIssueCard: React.FC<Props> = (props) => {
|
|||||||
href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}?inboxIssueId=${issue.bridge_id}`}
|
href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}?inboxIssueId=${issue.bridge_id}`}
|
||||||
>
|
>
|
||||||
<a>
|
<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
|
<div
|
||||||
id={issue.id}
|
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 ${
|
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 ${
|
||||||
@ -57,20 +51,6 @@ export const InboxIssueCard: React.FC<Props> = (props) => {
|
|||||||
<h5 className="truncate text-sm">{issue.name}</h5>
|
<h5 className="truncate text-sm">{issue.name}</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<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"}`}>
|
<Tooltip tooltipHeading="Priority" tooltipContent={`${issue.priority ?? "None"}`}>
|
||||||
<div
|
<div
|
||||||
className={`grid h-6 w-6 place-items-center rounded border items-center shadow-sm ${
|
className={`grid h-6 w-6 place-items-center rounded border items-center shadow-sm ${
|
||||||
@ -100,23 +80,46 @@ export const InboxIssueCard: React.FC<Props> = (props) => {
|
|||||||
<span>{renderShortNumericDateFormat(issue.created_at ?? "")}</span>
|
<span>{renderShortNumericDateFormat(issue.created_at ?? "")}</span>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{issue.issue_inbox[0].snoozed_till && (
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`text-xs flex items-center gap-1 ${
|
className={`text-xs flex items-center justify-end gap-1 w-full ${
|
||||||
new Date(issue.issue_inbox[0].snoozed_till ?? "") < new Date()
|
issueStatus === 0 && new Date(issue.issue_inbox[0].snoozed_till ?? "") < new Date()
|
||||||
? "text-red-500"
|
? "text-red-500"
|
||||||
: "text-blue-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" />
|
<ClockIcon className="h-3.5 w-3.5" />
|
||||||
<span>
|
<span>
|
||||||
Snoozed till {renderShortNumericDateFormat(issue.issue_inbox[0].snoozed_till)}
|
{new Date(issue.issue_inbox[0].snoozed_till ?? "") < new Date()
|
||||||
|
? "Snoozed date passed"
|
||||||
|
: "Snoozed"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</>
|
||||||
|
) : 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>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import Router, { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
|
|
||||||
@ -29,6 +29,7 @@ import {
|
|||||||
ClockIcon,
|
ClockIcon,
|
||||||
DocumentDuplicateIcon,
|
DocumentDuplicateIcon,
|
||||||
ExclamationTriangleIcon,
|
ExclamationTriangleIcon,
|
||||||
|
InboxIcon,
|
||||||
XCircleIcon,
|
XCircleIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
@ -37,6 +38,8 @@ import { renderShortNumericDateFormat } from "helpers/date-time.helper";
|
|||||||
import type { IInboxIssue, IIssue } from "types";
|
import type { IInboxIssue, IIssue } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { INBOX_ISSUES, INBOX_ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
import { INBOX_ISSUES, INBOX_ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||||
|
// constants
|
||||||
|
import { INBOX_STATUS } from "constants/inbox";
|
||||||
|
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
name: "",
|
name: "",
|
||||||
@ -55,7 +58,7 @@ export const InboxMainContent: React.FC = () => {
|
|||||||
|
|
||||||
const { user } = useUserAuth();
|
const { user } = useUserAuth();
|
||||||
const { memberRole } = useProjectMyMembership();
|
const { memberRole } = useProjectMyMembership();
|
||||||
const { params } = useInboxView();
|
const { params, issues: inboxIssues } = useInboxView();
|
||||||
|
|
||||||
const { reset, control, watch } = useForm<IIssue>({
|
const { reset, control, watch } = useForm<IIssue>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
@ -76,17 +79,6 @@ export const InboxMainContent: React.FC = () => {
|
|||||||
: null
|
: 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(
|
const submitChanges = useCallback(
|
||||||
async (formData: Partial<IInboxIssue>) => {
|
async (formData: Partial<IInboxIssue>) => {
|
||||||
if (!workspaceSlug || !projectId || !inboxIssueId || !inboxId || !issueDetails) return;
|
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 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 (
|
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="basis-2/3 h-full overflow-auto p-5 space-y-3">
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-2 p-3 text-sm border rounded-md ${
|
className={`flex items-center gap-2 p-3 text-sm border rounded-md ${
|
||||||
issueStatus === -2
|
issueStatus === 0 &&
|
||||||
? "text-orange-500 border-orange-500 bg-orange-500/10"
|
new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date()
|
||||||
: issueStatus === -1
|
|
||||||
? "text-red-500 border-red-500 bg-red-500/10"
|
? "text-red-500 border-red-500 bg-red-500/10"
|
||||||
: issueStatus === 0
|
: `${inboxStatusDetails?.textColor} ${inboxStatusDetails?.bgColor} ${inboxStatusDetails?.borderColor}`
|
||||||
? 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"
|
|
||||||
: ""
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{issueStatus === -2 ? (
|
{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>) => {
|
const handleCreateUpdateIssue = async (formData: Partial<IIssue>) => {
|
||||||
await handleFormSubmit(formData);
|
await handleFormSubmit(formData);
|
||||||
|
|
||||||
|
setGptAssistantModal(false);
|
||||||
|
|
||||||
reset({
|
reset({
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
project: projectId,
|
project: projectId,
|
||||||
|
@ -188,6 +188,10 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
message: "Issue created successfully.",
|
message: "Issue created successfully.",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.push(
|
||||||
|
`/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}?inboxIssueId=${res.issue_inbox[0].id}`
|
||||||
|
);
|
||||||
|
|
||||||
mutate(INBOX_ISSUES(inboxId.toString(), inboxParams));
|
mutate(INBOX_ISSUES(inboxId.toString(), inboxParams));
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
@ -3,26 +3,41 @@ export const INBOX_STATUS = [
|
|||||||
key: "pending",
|
key: "pending",
|
||||||
label: "Pending",
|
label: "Pending",
|
||||||
value: -2,
|
value: -2,
|
||||||
|
textColor: "text-yellow-500",
|
||||||
|
bgColor: "bg-yellow-500/10",
|
||||||
|
borderColor: "border-yellow-500",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "declined",
|
key: "declined",
|
||||||
label: "Declined",
|
label: "Declined",
|
||||||
value: -1,
|
value: -1,
|
||||||
|
textColor: "text-red-500",
|
||||||
|
bgColor: "bg-red-500/10",
|
||||||
|
borderColor: "border-red-500",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "snoozed",
|
key: "snoozed",
|
||||||
label: "Snoozed",
|
label: "Snoozed",
|
||||||
value: 0,
|
value: 0,
|
||||||
|
textColor: "text-brand-secondary",
|
||||||
|
bgColor: "bg-gray-500/10",
|
||||||
|
borderColor: "border-gray-500",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "accepted",
|
key: "accepted",
|
||||||
label: "Accepted",
|
label: "Accepted",
|
||||||
value: 1,
|
value: 1,
|
||||||
|
textColor: "text-green-500",
|
||||||
|
bgColor: "bg-green-500/10",
|
||||||
|
borderColor: "border-green-500",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "duplicate",
|
key: "duplicate",
|
||||||
label: "Duplicate",
|
label: "Duplicate",
|
||||||
value: 2,
|
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="p-8">
|
||||||
<div className="flex flex-col gap-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">
|
<p className="font-semibold">
|
||||||
Plane is open source, support us by starring us on GitHub.
|
Plane is open source, support us by starring us on GitHub.
|
||||||
</p>
|
</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
|
// hooks
|
||||||
import useInboxView from "hooks/use-inbox-view";
|
import useProjectDetails from "hooks/use-project-details";
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// layouts
|
// layouts
|
||||||
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
||||||
// contexts
|
// contexts
|
||||||
import { InboxViewContextProvider } from "contexts/inbox-view-context";
|
import { InboxViewContextProvider } from "contexts/inbox-view-context";
|
||||||
// components
|
// components
|
||||||
import {
|
import { InboxActionHeader, InboxMainContent, IssuesListSidebar } from "components/inbox";
|
||||||
InboxActionHeader,
|
|
||||||
InboxMainContent,
|
|
||||||
SelectDuplicateInboxIssueModal,
|
|
||||||
DeclineIssueModal,
|
|
||||||
DeleteIssueModal,
|
|
||||||
IssuesListSidebar,
|
|
||||||
} from "components/inbox";
|
|
||||||
// helper
|
// helper
|
||||||
import { truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
// ui
|
// ui
|
||||||
@ -31,123 +15,14 @@ import { PrimaryButton } from "components/ui";
|
|||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// icons
|
// icons
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
import { InboxIcon } from "components/icons";
|
|
||||||
// types
|
// types
|
||||||
import { IInboxIssueDetail, TInboxStatus } from "types";
|
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
// fetch-keys
|
|
||||||
import { INBOX_ISSUE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
const ProjectInbox: NextPage = () => {
|
const ProjectInbox: NextPage = () => {
|
||||||
const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false);
|
|
||||||
const [declineIssueModal, setDeclineIssueModal] = useState(false);
|
|
||||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const { user } = useUserAuth();
|
const { projectDetails } = useProjectDetails();
|
||||||
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();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InboxViewContextProvider>
|
<InboxViewContextProvider>
|
||||||
@ -175,88 +50,15 @@ const ProjectInbox: NextPage = () => {
|
|||||||
</div>
|
</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">
|
<div className="flex flex-col h-full">
|
||||||
<InboxActionHeader
|
<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">
|
<div className="grid grid-cols-4 flex-1 divide-x divide-brand-base overflow-hidden">
|
||||||
<IssuesListSidebar />
|
<IssuesListSidebar />
|
||||||
<div className="col-span-3 h-full overflow-auto">
|
<div className="col-span-3 h-full overflow-auto">
|
||||||
{inboxIssueId ? (
|
|
||||||
<InboxMainContent />
|
<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>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</ProjectAuthorizationWrapper>
|
</ProjectAuthorizationWrapper>
|
||||||
</InboxViewContextProvider>
|
</InboxViewContextProvider>
|
||||||
);
|
);
|
||||||
|
@ -162,7 +162,7 @@ class InboxServices extends APIService {
|
|||||||
inboxId: string,
|
inboxId: string,
|
||||||
data: any,
|
data: any,
|
||||||
user: ICurrentUserResponse | undefined
|
user: ICurrentUserResponse | undefined
|
||||||
): Promise<IInboxIssue> {
|
): Promise<IInboxIssueDetail> {
|
||||||
return this.post(
|
return this.post(
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/`,
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/`,
|
||||||
data
|
data
|
||||||
|
Loading…
Reference in New Issue
Block a user