[WEB-927, WEB-928] fix: inbox issue bug fixes and improvement (#4160)

* chore: inbox duplicate issue modal improvement

* chore: handled tab navigation in inbox issues and handled cross project inbox issues

* chore: fetch inbox issue activity once the issue is updated in inbox issue

* chore: disable duplicate inbox issue actions

* chore: duplicate issue mutation in the inbox issue

* chore: inbox create modal sidebar tab change updated

* chore: multiple date selection in the inbox issue filters

* chore: code refactor

* chore: removed project dependancy on the inbox store structure

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
This commit is contained in:
guru_sainath 2024-04-10 16:08:31 +05:30 committed by GitHub
parent d0cb00f28a
commit f45c2d12fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 96 additions and 108 deletions

View File

@ -17,12 +17,12 @@ type Props = {
projectId: string; projectId: string;
issue: Partial<TIssue>; issue: Partial<TIssue>;
issueOperations: TIssueOperations; issueOperations: TIssueOperations;
is_editable: boolean; isEditable: boolean;
duplicateIssueDetails: TInboxDuplicateIssueDetails | undefined; duplicateIssueDetails: TInboxDuplicateIssueDetails | undefined;
}; };
export const InboxIssueProperties: React.FC<Props> = observer((props) => { export const InboxIssueProperties: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, issue, issueOperations, is_editable, duplicateIssueDetails } = props; const { workspaceSlug, projectId, issue, issueOperations, isEditable, duplicateIssueDetails } = props;
const router = useRouter(); const router = useRouter();
// store hooks // store hooks
@ -35,7 +35,7 @@ export const InboxIssueProperties: React.FC<Props> = observer((props) => {
<div className="flex h-min w-full flex-col divide-y-2 divide-custom-border-200 overflow-hidden"> <div className="flex h-min w-full flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
<div className="h-min w-full overflow-y-auto px-5"> <div className="h-min w-full overflow-y-auto px-5">
<h5 className="text-sm font-medium my-4">Properties</h5> <h5 className="text-sm font-medium my-4">Properties</h5>
<div className={`divide-y-2 divide-custom-border-200 ${!is_editable ? "opacity-60" : ""}`}> <div className={`divide-y-2 divide-custom-border-200 ${!isEditable ? "opacity-60" : ""}`}>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{/* State */} {/* State */}
<div className="flex items-center gap-2 h-8"> <div className="flex items-center gap-2 h-8">
@ -50,7 +50,7 @@ export const InboxIssueProperties: React.FC<Props> = observer((props) => {
issue?.id && issueOperations.update(workspaceSlug, projectId, issue?.id, { state_id: val }) issue?.id && issueOperations.update(workspaceSlug, projectId, issue?.id, { state_id: val })
} }
projectId={projectId?.toString() ?? ""} projectId={projectId?.toString() ?? ""}
disabled={!is_editable} disabled={!isEditable}
buttonVariant="transparent-with-text" buttonVariant="transparent-with-text"
className="w-3/5 flex-grow group" className="w-3/5 flex-grow group"
buttonContainerClassName="w-full text-left" buttonContainerClassName="w-full text-left"
@ -71,7 +71,7 @@ export const InboxIssueProperties: React.FC<Props> = observer((props) => {
onChange={(val) => onChange={(val) =>
issue?.id && issueOperations.update(workspaceSlug, projectId, issue?.id, { assignee_ids: val }) issue?.id && issueOperations.update(workspaceSlug, projectId, issue?.id, { assignee_ids: val })
} }
disabled={!is_editable} disabled={!isEditable}
projectId={projectId?.toString() ?? ""} projectId={projectId?.toString() ?? ""}
placeholder="Add assignees" placeholder="Add assignees"
multiple multiple
@ -99,7 +99,7 @@ export const InboxIssueProperties: React.FC<Props> = observer((props) => {
onChange={(val) => onChange={(val) =>
issue?.id && issueOperations.update(workspaceSlug, projectId, issue?.id, { priority: val }) issue?.id && issueOperations.update(workspaceSlug, projectId, issue?.id, { priority: val })
} }
disabled={!is_editable} disabled={!isEditable}
buttonVariant="border-with-text" buttonVariant="border-with-text"
className="w-3/5 flex-grow rounded px-2 hover:bg-custom-background-80" className="w-3/5 flex-grow rounded px-2 hover:bg-custom-background-80"
buttonContainerClassName="w-full text-left" buttonContainerClassName="w-full text-left"
@ -108,7 +108,7 @@ export const InboxIssueProperties: React.FC<Props> = observer((props) => {
</div> </div>
</div> </div>
</div> </div>
<div className={`divide-y-2 divide-custom-border-200 mt-3 ${!is_editable ? "opacity-60" : ""}`}> <div className={`divide-y-2 divide-custom-border-200 mt-3 ${!isEditable ? "opacity-60" : ""}`}>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{/* Due Date */} {/* Due Date */}
<div className="flex items-center gap-2 h-8"> <div className="flex items-center gap-2 h-8">
@ -126,7 +126,7 @@ export const InboxIssueProperties: React.FC<Props> = observer((props) => {
}) })
} }
minDate={minDate ?? undefined} minDate={minDate ?? undefined}
disabled={!is_editable} disabled={!isEditable}
buttonVariant="transparent-with-text" buttonVariant="transparent-with-text"
className="w-3/5 flex-grow group" className="w-3/5 flex-grow group"
buttonContainerClassName="w-full text-left" buttonContainerClassName="w-full text-left"
@ -147,7 +147,7 @@ export const InboxIssueProperties: React.FC<Props> = observer((props) => {
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
issueId={issue?.id} issueId={issue?.id}
disabled={!is_editable} disabled={!isEditable}
isInboxIssue isInboxIssue
onLabelUpdate={(val: string[]) => onLabelUpdate={(val: string[]) =>
issue?.id && issueOperations.update(workspaceSlug, projectId, issue?.id, { label_ids: val }) issue?.id && issueOperations.update(workspaceSlug, projectId, issue?.id, { label_ids: val })

View File

@ -22,14 +22,14 @@ type Props = {
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
inboxIssue: IInboxIssueStore; inboxIssue: IInboxIssueStore;
is_editable: boolean; isEditable: boolean;
isSubmitting: "submitting" | "submitted" | "saved"; isSubmitting: "submitting" | "submitted" | "saved";
setIsSubmitting: Dispatch<SetStateAction<"submitting" | "submitted" | "saved">>; setIsSubmitting: Dispatch<SetStateAction<"submitting" | "submitted" | "saved">>;
}; };
export const InboxIssueMainContent: React.FC<Props> = observer((props) => { export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, inboxIssue, is_editable, isSubmitting, setIsSubmitting } = props; const { workspaceSlug, projectId, inboxIssue, isEditable, isSubmitting, setIsSubmitting } = props;
// hooks // hooks
const { currentUser } = useUser(); const { currentUser } = useUser();
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting"); const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
@ -126,7 +126,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
setIsSubmitting={(value) => setIsSubmitting(value)} setIsSubmitting={(value) => setIsSubmitting(value)}
issueOperations={issueOperations} issueOperations={issueOperations}
disabled={!is_editable} disabled={!isEditable}
value={issue.name} value={issue.name}
/> />
@ -136,7 +136,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
issueId={issue.id} issueId={issue.id}
value={issueDescription} value={issueDescription}
initialValue={issueDescription} initialValue={issueDescription}
disabled={!is_editable} disabled={!isEditable}
issueOperations={issueOperations} issueOperations={issueOperations}
setIsSubmitting={(value) => setIsSubmitting(value)} setIsSubmitting={(value) => setIsSubmitting(value)}
/> />
@ -156,7 +156,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
projectId={projectId} projectId={projectId}
issue={issue} issue={issue}
issueOperations={issueOperations} issueOperations={issueOperations}
is_editable={is_editable} isEditable={isEditable}
duplicateIssueDetails={inboxIssue?.duplicate_issue_detail} duplicateIssueDetails={inboxIssue?.duplicate_issue_detail}
/> />

View File

@ -32,11 +32,11 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
{ revalidateOnFocus: false } { revalidateOnFocus: false }
); );
const is_editable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const isEditable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
if (!inboxIssue) return <></>; if (!inboxIssue) return <></>;
const isIssueAcceptedOrDeclined = [-1, 1].includes(inboxIssue.status); const isIssueDisabled = [-1, 1, 2].includes(inboxIssue.status);
return ( return (
<> <>
@ -54,7 +54,7 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
inboxIssue={inboxIssue} inboxIssue={inboxIssue}
is_editable={is_editable && !isIssueAcceptedOrDeclined} isEditable={isEditable && !isIssueDisabled}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
setIsSubmitting={setIsSubmitting} setIsSubmitting={setIsSubmitting}
/> />

View File

@ -38,12 +38,6 @@ export const FilterDate: FC<Props> = observer((props) => {
const handleFilterValue = (value: string): string[] => (filterValue?.includes(value) ? [] : uniq(concat(value))); const handleFilterValue = (value: string): string[] => (filterValue?.includes(value) ? [] : uniq(concat(value)));
const handleCustomFilterValue = (value: string[]): string[] => {
const finalOptions: string[] = [...filterValue];
value.forEach((v) => (finalOptions?.includes(v) ? [] : finalOptions.push(v)));
return uniq(finalOptions);
};
const isCustomDateSelected = () => { const isCustomDateSelected = () => {
const isValidDateSelected = filterValue?.filter((f) => isDate(f.split(";")[0])) || []; const isValidDateSelected = filterValue?.filter((f) => isDate(f.split(";")[0])) || [];
return isValidDateSelected.length > 0 ? true : false; return isValidDateSelected.length > 0 ? true : false;
@ -64,7 +58,7 @@ export const FilterDate: FC<Props> = observer((props) => {
<DateFilterModal <DateFilterModal
handleClose={() => setIsDateFilterModalOpen(false)} handleClose={() => setIsDateFilterModalOpen(false)}
isOpen={isDateFilterModalOpen} isOpen={isDateFilterModalOpen}
onSelect={(val) => handleInboxIssueFilters(filterKey, handleCustomFilterValue(val))} onSelect={(val) => handleInboxIssueFilters(filterKey, val)}
title="Created date" title="Created date"
/> />
)} )}

View File

@ -52,7 +52,7 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
const workspaceStore = useWorkspace(); const workspaceStore = useWorkspace();
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string; const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string;
// store hooks // store hooks
const { createInboxIssue } = useProjectInbox(); const { createInboxIssue, handleCurrentTab } = useProjectInbox();
const { const {
config: { envConfig }, config: { envConfig },
} = useApplication(); } = useApplication();
@ -79,7 +79,8 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
await createInboxIssue(workspaceSlug.toString(), projectId.toString(), formData) await createInboxIssue(workspaceSlug.toString(), projectId.toString(), formData)
.then((res) => { .then((res) => {
if (!createMore) { if (!createMore) {
router.push(`/${workspaceSlug}/projects/${projectId}/inbox/?inboxIssueId=${res?.issue?.id}`); router.push(`/${workspaceSlug}/projects/${projectId}/inbox/?currentTab=open&inboxIssueId=${res?.issue?.id}`);
handleCurrentTab("open");
handleClose(); handleClose();
} else { } else {
reset(defaultValues); reset(defaultValues);

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import React, { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
import { Search } from "lucide-react"; import { Search } from "lucide-react";
@ -7,7 +7,7 @@ import { Combobox, Dialog, Transition } from "@headlessui/react";
// icons // icons
// components // components
// ui // ui
import { Button, TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
import { EmptyState } from "@/components/empty-state"; import { EmptyState } from "@/components/empty-state";
// services // services
// constants // constants
@ -29,7 +29,6 @@ export const SelectDuplicateInboxIssueModal: React.FC<Props> = (props) => {
const { isOpen, onClose, onSubmit, value } = props; const { isOpen, onClose, onSubmit, value } = props;
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const [selectedItem, setSelectedItem] = useState<string>("");
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query; const { workspaceSlug, projectId, issueId } = router.query;
@ -48,18 +47,11 @@ export const SelectDuplicateInboxIssueModal: React.FC<Props> = (props) => {
: null : null
); );
useEffect(() => {
if (!value) {
setSelectedItem("");
return;
} else setSelectedItem(value);
}, [value]);
const handleClose = () => { const handleClose = () => {
onClose(); onClose();
}; };
const handleSubmit = () => { const handleSubmit = (selectedItem: string) => {
if (!selectedItem || selectedItem.length === 0) if (!selectedItem || selectedItem.length === 0)
return setToast({ return setToast({
title: "Error", title: "Error",
@ -99,12 +91,7 @@ export const SelectDuplicateInboxIssueModal: React.FC<Props> = (props) => {
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-lg bg-custom-background-100 shadow-custom-shadow-md transition-all"> <Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-lg bg-custom-background-100 shadow-custom-shadow-md transition-all">
<Combobox <Combobox value={value} onChange={handleSubmit}>
value={selectedItem}
onChange={(value) => {
setSelectedItem(value);
}}
>
<div className="relative m-1"> <div className="relative m-1">
<Search <Search
className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-custom-text-100 text-opacity-40" className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-custom-text-100 text-opacity-40"
@ -175,17 +162,6 @@ export const SelectDuplicateInboxIssueModal: React.FC<Props> = (props) => {
)} )}
</Combobox.Options> </Combobox.Options>
</Combobox> </Combobox>
{filteredIssues.length > 0 && (
<div className="flex items-center justify-end gap-2 p-3">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
Cancel
</Button>
<Button variant="primary" size="sm" onClick={handleSubmit}>
Mark as original
</Button>
</div>
)}
</Dialog.Panel> </Dialog.Panel>
</Transition.Child> </Transition.Child>
</div> </div>

View File

@ -9,7 +9,7 @@ import { InboxIssueStatus } from "@/components/inbox";
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { renderFormattedDate } from "@/helpers/date-time.helper"; import { renderFormattedDate } from "@/helpers/date-time.helper";
// hooks // hooks
import { useLabel } from "@/hooks/store"; import { useLabel, useProjectInbox } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
// store // store
import { IInboxIssueStore } from "@/store/inbox/inbox-issue.store"; import { IInboxIssueStore } from "@/store/inbox/inbox-issue.store";
@ -27,6 +27,7 @@ export const InboxIssueListItem: FC<InboxIssueListItemProps> = observer((props)
const router = useRouter(); const router = useRouter();
const { inboxIssueId } = router.query; const { inboxIssueId } = router.query;
// store // store
const { currentTab } = useProjectInbox();
const { projectLabels } = useLabel(); const { projectLabels } = useLabel();
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
const issue = inboxIssue.issue; const issue = inboxIssue.issue;
@ -54,7 +55,7 @@ export const InboxIssueListItem: FC<InboxIssueListItemProps> = observer((props)
<Link <Link
id={`inbox-issue-list-item-${issue.id}`} id={`inbox-issue-list-item-${issue.id}`}
key={`${projectId}_${issue.id}`} key={`${projectId}_${issue.id}`}
href={`/${workspaceSlug}/projects/${projectId}/inbox?inboxIssueId=${issue.id}`} href={`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${currentTab}&inboxIssueId=${issue.id}`}
onClick={(e) => handleIssueRedirection(e, issue.id)} onClick={(e) => handleIssueRedirection(e, issue.id)}
> >
<div <div

View File

@ -75,7 +75,7 @@ export const InboxSidebar: FC<IInboxSidebarProps> = observer((props) => {
)} )}
onClick={() => { onClick={() => {
if (currentTab != option?.key) handleCurrentTab(option?.key); if (currentTab != option?.key) handleCurrentTab(option?.key);
router.push(`/${workspaceSlug}/projects/${projectId}/inbox`); router.push(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${option?.key}`);
}} }}
> >
<div>{option?.label}</div> <div>{option?.label}</div>

View File

@ -21,11 +21,11 @@ type Props = {
projectId: string; projectId: string;
issueId: string; issueId: string;
issueOperations: TIssueOperations; issueOperations: TIssueOperations;
is_editable: boolean; isEditable: boolean;
}; };
export const IssueMainContent: React.FC<Props> = observer((props) => { export const IssueMainContent: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, issueId, issueOperations, is_editable } = props; const { workspaceSlug, projectId, issueId, issueOperations, isEditable } = props;
// states // states
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
// hooks // hooks
@ -90,7 +90,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
setIsSubmitting={(value) => setIsSubmitting(value)} setIsSubmitting={(value) => setIsSubmitting(value)}
issueOperations={issueOperations} issueOperations={issueOperations}
disabled={!is_editable} disabled={!isEditable}
value={issue.name} value={issue.name}
/> />
@ -100,7 +100,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
issueId={issue.id} issueId={issue.id}
value={issueDescription} value={issueDescription}
initialValue={issueDescription} initialValue={issueDescription}
disabled={!is_editable} disabled={!isEditable}
issueOperations={issueOperations} issueOperations={issueOperations}
setIsSubmitting={(value) => setIsSubmitting(value)} setIsSubmitting={(value) => setIsSubmitting(value)}
/> />
@ -120,7 +120,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
projectId={projectId} projectId={projectId}
parentIssueId={issueId} parentIssueId={issueId}
currentUser={currentUser} currentUser={currentUser}
disabled={!is_editable} disabled={!isEditable}
/> />
)} )}
</div> </div>
@ -129,10 +129,10 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
issueId={issueId} issueId={issueId}
disabled={!is_editable} disabled={!isEditable}
/> />
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={!is_editable} /> <IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={!isEditable} />
</> </>
); );
}); });

View File

@ -340,7 +340,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
// issue details // issue details
const issue = getIssueById(issueId); const issue = getIssueById(issueId);
// checking if issue is editable, based on user role // checking if issue is editable, based on user role
const is_editable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const isEditable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
return ( return (
<> <>
@ -362,7 +362,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
projectId={projectId} projectId={projectId}
issueId={issueId} issueId={issueId}
issueOperations={issueOperations} issueOperations={issueOperations}
is_editable={!is_archived && is_editable} isEditable={!is_archived && isEditable}
/> />
</div> </div>
<div <div
@ -375,7 +375,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
issueId={issueId} issueId={issueId}
issueOperations={issueOperations} issueOperations={issueOperations}
is_archived={is_archived} is_archived={is_archived}
is_editable={!is_archived && is_editable} isEditable={!is_archived && isEditable}
/> />
</div> </div>
</div> </div>

View File

@ -69,11 +69,11 @@ type Props = {
issueId: string; issueId: string;
issueOperations: TIssueOperations; issueOperations: TIssueOperations;
is_archived: boolean; is_archived: boolean;
is_editable: boolean; isEditable: boolean;
}; };
export const IssueDetailsSidebar: React.FC<Props> = observer((props) => { export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, issueId, issueOperations, is_archived, is_editable } = props; const { workspaceSlug, projectId, issueId, issueOperations, is_archived, isEditable } = props;
// states // states
const [deleteIssueModal, setDeleteIssueModal] = useState(false); const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const [archiveIssueModal, setArchiveIssueModal] = useState(false); const [archiveIssueModal, setArchiveIssueModal] = useState(false);
@ -116,7 +116,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
const projectDetails = getProjectById(issue.project_id); const projectDetails = getProjectById(issue.project_id);
const stateDetails = getStateById(issue.state_id); const stateDetails = getStateById(issue.state_id);
// auth // auth
const isArchivingAllowed = !is_archived && issueOperations.archive && is_editable; const isArchivingAllowed = !is_archived && issueOperations.archive && isEditable;
const isInArchivableGroup = const isInArchivableGroup =
!!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group); !!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group);
@ -179,7 +179,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
</button> </button>
</Tooltip> </Tooltip>
)} )}
{is_editable && ( {isEditable && (
<Tooltip tooltipContent="Delete" isMobile={isMobile}> <Tooltip tooltipContent="Delete" isMobile={isMobile}>
<button <button
type="button" type="button"
@ -197,7 +197,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="h-full w-full overflow-y-auto px-6"> <div className="h-full w-full overflow-y-auto px-6">
<h5 className="mt-6 text-sm font-medium">Properties</h5> <h5 className="mt-6 text-sm font-medium">Properties</h5>
{/* TODO: render properties using a common component */} {/* TODO: render properties using a common component */}
<div className={`mb-2 mt-3 space-y-2.5 ${!is_editable ? "opacity-60" : ""}`}> <div className={`mb-2 mt-3 space-y-2.5 ${!isEditable ? "opacity-60" : ""}`}>
<div className="flex h-8 items-center gap-2"> <div className="flex h-8 items-center gap-2">
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300"> <div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
<DoubleCircleIcon className="h-4 w-4 flex-shrink-0" /> <DoubleCircleIcon className="h-4 w-4 flex-shrink-0" />
@ -207,7 +207,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
value={issue?.state_id ?? undefined} value={issue?.state_id ?? undefined}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })} onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })}
projectId={projectId?.toString() ?? ""} projectId={projectId?.toString() ?? ""}
disabled={!is_editable} disabled={!isEditable}
buttonVariant="transparent-with-text" buttonVariant="transparent-with-text"
className="group w-3/5 flex-grow" className="group w-3/5 flex-grow"
buttonContainerClassName="w-full text-left" buttonContainerClassName="w-full text-left"
@ -225,7 +225,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<MemberDropdown <MemberDropdown
value={issue?.assignee_ids ?? undefined} value={issue?.assignee_ids ?? undefined}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })} onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
disabled={!is_editable} disabled={!isEditable}
projectId={projectId?.toString() ?? ""} projectId={projectId?.toString() ?? ""}
placeholder="Add assignees" placeholder="Add assignees"
multiple multiple
@ -249,7 +249,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<PriorityDropdown <PriorityDropdown
value={issue?.priority || undefined} value={issue?.priority || undefined}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })} onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })}
disabled={!is_editable} disabled={!isEditable}
buttonVariant="border-with-text" buttonVariant="border-with-text"
className="w-3/5 flex-grow rounded px-2 hover:bg-custom-background-80" className="w-3/5 flex-grow rounded px-2 hover:bg-custom-background-80"
buttonContainerClassName="w-full text-left" buttonContainerClassName="w-full text-left"
@ -271,7 +271,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
}) })
} }
maxDate={maxDate ?? undefined} maxDate={maxDate ?? undefined}
disabled={!is_editable} disabled={!isEditable}
buttonVariant="transparent-with-text" buttonVariant="transparent-with-text"
className="group w-3/5 flex-grow" className="group w-3/5 flex-grow"
buttonContainerClassName="w-full text-left" buttonContainerClassName="w-full text-left"
@ -297,7 +297,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
}) })
} }
minDate={minDate ?? undefined} minDate={minDate ?? undefined}
disabled={!is_editable} disabled={!isEditable}
buttonVariant="transparent-with-text" buttonVariant="transparent-with-text"
className="group w-3/5 flex-grow" className="group w-3/5 flex-grow"
buttonContainerClassName="w-full text-left" buttonContainerClassName="w-full text-left"
@ -322,7 +322,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
value={issue?.estimate_point !== null ? issue.estimate_point : null} value={issue?.estimate_point !== null ? issue.estimate_point : null}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })} onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })}
projectId={projectId} projectId={projectId}
disabled={!is_editable} disabled={!isEditable}
buttonVariant="transparent-with-text" buttonVariant="transparent-with-text"
className="group w-3/5 flex-grow" className="group w-3/5 flex-grow"
buttonContainerClassName="w-full text-left" buttonContainerClassName="w-full text-left"
@ -347,7 +347,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
projectId={projectId} projectId={projectId}
issueId={issueId} issueId={issueId}
issueOperations={issueOperations} issueOperations={issueOperations}
disabled={!is_editable} disabled={!isEditable}
/> />
</div> </div>
)} )}
@ -364,7 +364,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
projectId={projectId} projectId={projectId}
issueId={issueId} issueId={issueId}
issueOperations={issueOperations} issueOperations={issueOperations}
disabled={!is_editable} disabled={!isEditable}
/> />
</div> </div>
)} )}
@ -380,7 +380,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
projectId={projectId} projectId={projectId}
issueId={issueId} issueId={issueId}
issueOperations={issueOperations} issueOperations={issueOperations}
disabled={!is_editable} disabled={!isEditable}
/> />
</div> </div>
@ -395,7 +395,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
projectId={projectId} projectId={projectId}
issueId={issueId} issueId={issueId}
relationKey="relates_to" relationKey="relates_to"
disabled={!is_editable} disabled={!isEditable}
/> />
</div> </div>
@ -410,7 +410,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
projectId={projectId} projectId={projectId}
issueId={issueId} issueId={issueId}
relationKey="blocking" relationKey="blocking"
disabled={!is_editable} disabled={!isEditable}
/> />
</div> </div>
@ -425,7 +425,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
projectId={projectId} projectId={projectId}
issueId={issueId} issueId={issueId}
relationKey="blocked_by" relationKey="blocked_by"
disabled={!is_editable} disabled={!isEditable}
/> />
</div> </div>
@ -440,7 +440,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
projectId={projectId} projectId={projectId}
issueId={issueId} issueId={issueId}
relationKey="duplicate" relationKey="duplicate"
disabled={!is_editable} disabled={!isEditable}
/> />
</div> </div>
@ -454,7 +454,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
issueId={issueId} issueId={issueId}
disabled={!is_editable} disabled={!isEditable}
/> />
</div> </div>
</div> </div>
@ -464,7 +464,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
issueId={issueId} issueId={issueId}
disabled={!is_editable} disabled={!isEditable}
/> />
</div> </div>
</div> </div>

View File

@ -372,7 +372,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const currentProjectRole = currentWorkspaceAllProjectsRole?.[peekIssue?.projectId]; const currentProjectRole = currentWorkspaceAllProjectsRole?.[peekIssue?.projectId];
// Check if issue is editable, based on user role // Check if issue is editable, based on user role
const is_editable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const isEditable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const isLoading = !issue || loader ? true : false; const isLoading = !issue || loader ? true : false;
return ( return (
@ -382,7 +382,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
issueId={peekIssue.issueId} issueId={peekIssue.issueId}
isLoading={isLoading} isLoading={isLoading}
is_archived={is_archived} is_archived={is_archived}
disabled={!is_editable} disabled={!isEditable}
issueOperations={issueOperations} issueOperations={issueOperations}
/> />
); );

View File

@ -1,4 +1,4 @@
import { ReactElement } from "react"; import { ReactElement, useEffect } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// components // components
@ -9,7 +9,7 @@ import { InboxIssueRoot } from "@/components/inbox";
// constants // constants
import { EmptyStateType } from "@/constants/empty-state"; import { EmptyStateType } from "@/constants/empty-state";
// hooks // hooks
import { useProject } from "@/hooks/store"; import { useProject, useProjectInbox } from "@/hooks/store";
// layouts // layouts
import { AppLayout } from "@/layouts/app-layout"; import { AppLayout } from "@/layouts/app-layout";
// types // types
@ -18,9 +18,10 @@ import { NextPageWithLayout } from "@/lib/types";
const ProjectInboxPage: NextPageWithLayout = observer(() => { const ProjectInboxPage: NextPageWithLayout = observer(() => {
/// router /// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, inboxIssueId } = router.query; const { workspaceSlug, projectId, currentTab: navigationTab, inboxIssueId } = router.query;
// hooks // hooks
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { currentTab, handleCurrentTab } = useProjectInbox();
if (!workspaceSlug || !projectId) return <></>; if (!workspaceSlug || !projectId) return <></>;
@ -38,6 +39,10 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
// derived values // derived values
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Inbox` : "Plane - Inbox"; const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Inbox` : "Plane - Inbox";
useEffect(() => {
if (navigationTab && currentTab != navigationTab) handleCurrentTab(navigationTab === "open" ? "open" : "closed");
}, [currentTab, navigationTab, handleCurrentTab]);
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<PageHead title={pageTitle} /> <PageHead title={pageTitle} />

View File

@ -1,9 +1,10 @@
import set from "lodash/set"; import set from "lodash/set";
import { makeObservable, observable, runInAction, action } from "mobx"; import { makeObservable, observable, runInAction, action } from "mobx";
// services
// types
import { TIssue, TInboxIssue, TInboxIssueStatus, TInboxDuplicateIssueDetails } from "@plane/types"; import { TIssue, TInboxIssue, TInboxIssueStatus, TInboxDuplicateIssueDetails } from "@plane/types";
// services
import { InboxIssueService } from "@/services/inbox"; import { InboxIssueService } from "@/services/inbox";
// root store
import { RootStore } from "@/store/root.store";
export interface IInboxIssueStore { export interface IInboxIssueStore {
isLoading: boolean; isLoading: boolean;
@ -36,7 +37,7 @@ export class InboxIssueStore implements IInboxIssueStore {
// services // services
inboxIssueService; inboxIssueService;
constructor(workspaceSlug: string, projectId: string, data: TInboxIssue) { constructor(workspaceSlug: string, projectId: string, data: TInboxIssue, private store: RootStore) {
this.id = data.id; this.id = data.id;
this.status = data.status; this.status = data.status;
this.issue = data?.issue; this.issue = data?.issue;
@ -90,10 +91,14 @@ export class InboxIssueStore implements IInboxIssueStore {
if (!this.issue.id) return; if (!this.issue.id) return;
set(this, "status", inboxStatus); set(this, "status", inboxStatus);
set(this, "duplicate_to", issueId); set(this, "duplicate_to", issueId);
await this.inboxIssueService.update(this.workspaceSlug, this.projectId, this.issue.id, { const issueResponse = await this.inboxIssueService.update(this.workspaceSlug, this.projectId, this.issue.id, {
status: inboxStatus, status: inboxStatus,
duplicate_to: issueId, duplicate_to: issueId,
}); });
runInAction(() => {
this.duplicate_to = issueResponse.duplicate_to;
this.duplicate_issue_detail = issueResponse.duplicate_issue_detail;
});
} catch { } catch {
runInAction(() => { runInAction(() => {
set(this, "status", previousData.status); set(this, "status", previousData.status);
@ -133,6 +138,8 @@ export class InboxIssueStore implements IInboxIssueStore {
set(inboxIssue, issueKey, issue[issueKey]); set(inboxIssue, issueKey, issue[issueKey]);
}); });
await this.inboxIssueService.updateIssue(this.workspaceSlug, this.projectId, this.issue.id, issue); await this.inboxIssueService.updateIssue(this.workspaceSlug, this.projectId, this.issue.id, issue);
// fetching activity
await this.store.issue.issueDetail.fetchActivities(this.workspaceSlug, this.projectId, this.issue.id);
} catch { } catch {
Object.keys(issue).forEach((key) => { Object.keys(issue).forEach((key) => {
const issueKey = key as keyof TIssue; const issueKey = key as keyof TIssue;

View File

@ -29,7 +29,7 @@ export interface IProjectInboxStore {
inboxFilters: Partial<TInboxIssueFilter>; inboxFilters: Partial<TInboxIssueFilter>;
inboxSorting: Partial<TInboxIssueSorting>; inboxSorting: Partial<TInboxIssueSorting>;
inboxIssuePaginationInfo: TInboxIssuePaginationInfo | undefined; inboxIssuePaginationInfo: TInboxIssuePaginationInfo | undefined;
inboxIssues: Record<string, IInboxIssueStore>; inboxIssues: Record<string, IInboxIssueStore>; // issue_id -> IInboxIssueStore
// computed // computed
getAppliedFiltersCount: number; getAppliedFiltersCount: number;
inboxIssuesArray: IInboxIssueStore[]; inboxIssuesArray: IInboxIssueStore[];
@ -177,8 +177,6 @@ export class ProjectInboxStore implements IProjectInboxStore {
set(this, "inboxFilters", undefined); set(this, "inboxFilters", undefined);
set(this, ["inboxSorting", "order_by"], "issue__created_at"); set(this, ["inboxSorting", "order_by"], "issue__created_at");
set(this, ["inboxSorting", "sort_by"], "desc"); set(this, ["inboxSorting", "sort_by"], "desc");
set(this, ["inboxIssues"], {});
set(this, ["inboxIssuePaginationInfo"], undefined);
if (tab === "closed") set(this, ["inboxFilters", "status"], [-1, 1, 2]); if (tab === "closed") set(this, ["inboxFilters", "status"], [-1, 1, 2]);
else set(this, ["inboxFilters", "status"], [-2]); else set(this, ["inboxFilters", "status"], [-2]);
const { workspaceSlug, projectId } = this.store.app.router; const { workspaceSlug, projectId } = this.store.app.router;
@ -187,16 +185,12 @@ export class ProjectInboxStore implements IProjectInboxStore {
handleInboxIssueFilters = <T extends keyof TInboxIssueFilter>(key: T, value: TInboxIssueFilter[T]) => { handleInboxIssueFilters = <T extends keyof TInboxIssueFilter>(key: T, value: TInboxIssueFilter[T]) => {
set(this.inboxFilters, key, value); set(this.inboxFilters, key, value);
set(this, ["inboxIssues"], {});
set(this, ["inboxIssuePaginationInfo"], undefined);
const { workspaceSlug, projectId } = this.store.app.router; const { workspaceSlug, projectId } = this.store.app.router;
if (workspaceSlug && projectId) this.fetchInboxIssues(workspaceSlug, projectId, "filter-loading"); if (workspaceSlug && projectId) this.fetchInboxIssues(workspaceSlug, projectId, "filter-loading");
}; };
handleInboxIssueSorting = <T extends keyof TInboxIssueSorting>(key: T, value: TInboxIssueSorting[T]) => { handleInboxIssueSorting = <T extends keyof TInboxIssueSorting>(key: T, value: TInboxIssueSorting[T]) => {
set(this.inboxSorting, key, value); set(this.inboxSorting, key, value);
set(this, ["inboxIssues"], {});
set(this, ["inboxIssuePaginationInfo"], undefined);
const { workspaceSlug, projectId } = this.store.app.router; const { workspaceSlug, projectId } = this.store.app.router;
if (workspaceSlug && projectId) this.fetchInboxIssues(workspaceSlug, projectId, "filter-loading"); if (workspaceSlug && projectId) this.fetchInboxIssues(workspaceSlug, projectId, "filter-loading");
}; };
@ -210,6 +204,8 @@ export class ProjectInboxStore implements IProjectInboxStore {
try { try {
if (loadingType) this.isLoading = loadingType; if (loadingType) this.isLoading = loadingType;
else if (Object.keys(this.inboxIssues).length === 0) this.isLoading = "init-loading"; else if (Object.keys(this.inboxIssues).length === 0) this.isLoading = "init-loading";
set(this, ["inboxIssues"], {});
set(this, ["inboxIssuePaginationInfo"], undefined);
const queryParams = this.inboxIssueQueryParams( const queryParams = this.inboxIssueQueryParams(
this.inboxFilters, this.inboxFilters,
@ -225,7 +221,11 @@ export class ProjectInboxStore implements IProjectInboxStore {
if (results && results.length > 0) if (results && results.length > 0)
results.forEach((value: TInboxIssue) => { results.forEach((value: TInboxIssue) => {
if (this.getIssueInboxByIssueId(value?.issue?.id) === undefined) if (this.getIssueInboxByIssueId(value?.issue?.id) === undefined)
set(this.inboxIssues, value?.issue?.id, new InboxIssueStore(workspaceSlug, projectId, value)); set(
this.inboxIssues,
[value?.issue?.id],
new InboxIssueStore(workspaceSlug, projectId, value, this.store)
);
}); });
}); });
} catch (error) { } catch (error) {
@ -267,7 +267,11 @@ export class ProjectInboxStore implements IProjectInboxStore {
if (results && results.length > 0) if (results && results.length > 0)
results.forEach((value: TInboxIssue) => { results.forEach((value: TInboxIssue) => {
if (this.getIssueInboxByIssueId(value?.issue?.id) === undefined) if (this.getIssueInboxByIssueId(value?.issue?.id) === undefined)
set(this.inboxIssues, value?.issue?.id, new InboxIssueStore(workspaceSlug, projectId, value)); set(
this.inboxIssues,
[value?.issue?.id],
new InboxIssueStore(workspaceSlug, projectId, value, this.store)
);
}); });
}); });
} else set(this, ["inboxIssuePaginationInfo", "next_page_results"], false); } else set(this, ["inboxIssuePaginationInfo", "next_page_results"], false);
@ -302,7 +306,7 @@ export class ProjectInboxStore implements IProjectInboxStore {
// fetching comments // fetching comments
await this.store.issue.issueDetail.fetchComments(workspaceSlug, projectId, issueId); await this.store.issue.issueDetail.fetchComments(workspaceSlug, projectId, issueId);
runInAction(() => { runInAction(() => {
set(this.inboxIssues, issueId, new InboxIssueStore(workspaceSlug, projectId, inboxIssue)); set(this.inboxIssues, [issueId], new InboxIssueStore(workspaceSlug, projectId, inboxIssue, this.store));
}); });
this.isLoading = undefined; this.isLoading = undefined;
} }
@ -325,8 +329,8 @@ export class ProjectInboxStore implements IProjectInboxStore {
runInAction(() => { runInAction(() => {
set( set(
this.inboxIssues, this.inboxIssues,
inboxIssueResponse?.issue?.id, [inboxIssueResponse?.issue?.id],
new InboxIssueStore(workspaceSlug, projectId, inboxIssueResponse) new InboxIssueStore(workspaceSlug, projectId, inboxIssueResponse, this.store)
); );
set( set(
this, this,