chore: code refactoring (#5928)
* chore: de dupe code splitting * chore: code refactor
This commit is contained in:
parent
4bc751b7ab
commit
57eb08c8a2
21 changed files with 664 additions and 250 deletions
24
packages/types/src/de-dupe.d.ts
vendored
Normal file
24
packages/types/src/de-dupe.d.ts
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { TIssuePriorities } from "./issues";
|
||||
|
||||
export type TDuplicateIssuePayload = {
|
||||
title: string;
|
||||
workspace_id: string;
|
||||
issue_id?: string;
|
||||
project_id?: string;
|
||||
description_stripped?: string;
|
||||
};
|
||||
|
||||
export type TDeDupeIssue = {
|
||||
id: string;
|
||||
type_id: string | null;
|
||||
project_id: string;
|
||||
sequence_id: number;
|
||||
name: string;
|
||||
priority: TIssuePriorities;
|
||||
state_id: string;
|
||||
created_by: string;
|
||||
};
|
||||
|
||||
export type TDuplicateIssueResponse = {
|
||||
dupes: TDeDupeIssue[];
|
||||
};
|
||||
1
packages/types/src/index.d.ts
vendored
1
packages/types/src/index.d.ts
vendored
|
|
@ -2,6 +2,7 @@ export * from "./users";
|
|||
export * from "./workspace";
|
||||
export * from "./cycle";
|
||||
export * from "./dashboard";
|
||||
export * from "./de-dupe";
|
||||
export * from "./project";
|
||||
export * from "./state";
|
||||
export * from "./issues";
|
||||
|
|
|
|||
1
web/ce/components/de-dupe/duplicate-modal/index.ts
Normal file
1
web/ce/components/de-dupe/duplicate-modal/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./root";
|
||||
16
web/ce/components/de-dupe/duplicate-modal/root.tsx
Normal file
16
web/ce/components/de-dupe/duplicate-modal/root.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"use-client";
|
||||
|
||||
import { FC } from "react";
|
||||
// types
|
||||
import { TDeDupeIssue } from "@plane/types";
|
||||
|
||||
type TDuplicateModalRootProps = {
|
||||
workspaceSlug: string;
|
||||
issues: TDeDupeIssue[];
|
||||
handleDuplicateIssueModal: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export const DuplicateModalRoot: FC<TDuplicateModalRootProps> = (props) => {
|
||||
const { workspaceSlug, issues, handleDuplicateIssueModal } = props;
|
||||
return <></>;
|
||||
};
|
||||
1
web/ce/components/de-dupe/duplicate-popover/index.ts
Normal file
1
web/ce/components/de-dupe/duplicate-popover/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./root";
|
||||
32
web/ce/components/de-dupe/duplicate-popover/root.tsx
Normal file
32
web/ce/components/de-dupe/duplicate-popover/root.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
"use client";
|
||||
|
||||
import React, { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// types
|
||||
import { TDeDupeIssue } from "@plane/types";
|
||||
import { TIssueOperations } from "@/components/issues";
|
||||
|
||||
type TDeDupeIssuePopoverRootProps = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
rootIssueId: string;
|
||||
issues: TDeDupeIssue[];
|
||||
issueOperations: TIssueOperations;
|
||||
disabled?: boolean;
|
||||
renderDeDupeActionModals?: boolean;
|
||||
isIntakeIssue?: boolean;
|
||||
};
|
||||
|
||||
export const DeDupeIssuePopoverRoot: FC<TDeDupeIssuePopoverRootProps> = observer((props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
rootIssueId,
|
||||
issues,
|
||||
issueOperations,
|
||||
disabled = false,
|
||||
renderDeDupeActionModals = true,
|
||||
isIntakeIssue = false,
|
||||
} = props;
|
||||
return <></>;
|
||||
});
|
||||
3
web/ce/components/de-dupe/index.ts
Normal file
3
web/ce/components/de-dupe/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./duplicate-modal";
|
||||
export * from "./duplicate-popover";
|
||||
export * from "./issue-block";
|
||||
13
web/ce/components/de-dupe/issue-block/button-label.tsx
Normal file
13
web/ce/components/de-dupe/issue-block/button-label.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
|
||||
type TDeDupeIssueButtonLabelProps = {
|
||||
isOpen: boolean;
|
||||
buttonLabel: string;
|
||||
};
|
||||
|
||||
export const DeDupeIssueButtonLabel: FC<TDeDupeIssueButtonLabelProps> = (props) => {
|
||||
const { isOpen, buttonLabel } = props;
|
||||
return <></>;
|
||||
};
|
||||
1
web/ce/components/de-dupe/issue-block/index.ts
Normal file
1
web/ce/components/de-dupe/issue-block/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./button-label";
|
||||
11
web/ce/hooks/use-debounced-duplicate-issues.tsx
Normal file
11
web/ce/hooks/use-debounced-duplicate-issues.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { TDeDupeIssue } from "@plane/types";
|
||||
|
||||
export const useDebouncedDuplicateIssues = (
|
||||
workspaceId: string | undefined,
|
||||
projectId: string | undefined,
|
||||
formData: { name: string | undefined; description_html?: string | undefined; issueId?: string | undefined }
|
||||
) => {
|
||||
const duplicateIssues: TDeDupeIssue[] = [];
|
||||
|
||||
return { duplicateIssues };
|
||||
};
|
||||
|
|
@ -17,10 +17,16 @@ import {
|
|||
TIssueOperations,
|
||||
IssueAttachmentRoot,
|
||||
} from "@/components/issues";
|
||||
// constants
|
||||
import { ISSUE_ARCHIVED, ISSUE_DELETED } from "@/constants/event-tracker";
|
||||
// helpers
|
||||
import { getTextContent } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useProjectInbox, useUser } from "@/hooks/store";
|
||||
import { useEventTracker, useIssueDetail, useProject, useProjectInbox, useUser } from "@/hooks/store";
|
||||
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
|
||||
// store types
|
||||
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe";
|
||||
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
||||
import { IInboxIssueStore } from "@/store/inbox/inbox-issue.store";
|
||||
|
||||
type Props = {
|
||||
|
|
@ -40,6 +46,8 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const { loader } = useProjectInbox();
|
||||
const { getProjectById } = useProject();
|
||||
const { removeIssue, archiveIssue } = useIssueDetail();
|
||||
|
||||
useEffect(() => {
|
||||
if (isSubmitting === "submitted") {
|
||||
|
|
@ -52,7 +60,17 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||
}
|
||||
}, [isSubmitting, setShowAlert, setIsSubmitting]);
|
||||
|
||||
// dervied values
|
||||
const issue = inboxIssue.issue;
|
||||
const projectDetails = issue?.project_id ? getProjectById(issue?.project_id) : undefined;
|
||||
|
||||
// debounced duplicate issues swr
|
||||
const { duplicateIssues } = useDebouncedDuplicateIssues(projectDetails?.workspace.toString(), projectId, {
|
||||
name: issue?.name,
|
||||
description_html: getTextContent(issue?.description_html),
|
||||
issueId: issue?.id,
|
||||
});
|
||||
|
||||
if (!issue) return <></>;
|
||||
|
||||
const issueOperations: TIssueOperations = useMemo(
|
||||
|
|
@ -63,7 +81,31 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars, arrow-body-style
|
||||
remove: async (_workspaceSlug: string, _projectId: string, _issueId: string) => {
|
||||
return;
|
||||
try {
|
||||
await removeIssue(workspaceSlug, projectId, _issueId);
|
||||
setToast({
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Issue deleted successfully",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_DELETED,
|
||||
payload: { id: _issueId, state: "SUCCESS", element: "Issue detail page" },
|
||||
path: pathname,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error in deleting issue:", error);
|
||||
setToast({
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: "Issue delete failed",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_DELETED,
|
||||
payload: { id: _issueId, state: "FAILED", element: "Issue detail page" },
|
||||
path: pathname,
|
||||
});
|
||||
}
|
||||
},
|
||||
update: async (_workspaceSlug: string, _projectId: string, _issueId: string, data: Partial<TIssue>) => {
|
||||
try {
|
||||
|
|
@ -94,6 +136,23 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||
});
|
||||
}
|
||||
},
|
||||
archive: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
await archiveIssue(workspaceSlug, projectId, issueId);
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_ARCHIVED,
|
||||
payload: { id: issueId, state: "SUCCESS", element: "Issue details page" },
|
||||
path: pathname,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error in archiving issue:", error);
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_ARCHIVED,
|
||||
payload: { id: issueId, state: "FAILED", element: "Issue details page" },
|
||||
path: pathname,
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
[inboxIssue]
|
||||
);
|
||||
|
|
@ -103,6 +162,16 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||
return (
|
||||
<>
|
||||
<div className="rounded-lg space-y-4">
|
||||
{duplicateIssues.length > 0 && (
|
||||
<DeDupeIssuePopoverRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
rootIssueId={issue.id}
|
||||
issues={duplicateIssues}
|
||||
issueOperations={issueOperations}
|
||||
isIntakeIssue
|
||||
/>
|
||||
)}
|
||||
<IssueTitleInput
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { FC, FormEvent, useCallback, useRef, useState } from "react";
|
||||
import { FC, FormEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
// editor
|
||||
|
|
@ -17,18 +17,23 @@ import { ETabIndices } from "@/constants/tab-indices";
|
|||
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
import { getTabIndex } from "@/helpers/tab-indices.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useProjectInbox, useWorkspace } from "@/hooks/store";
|
||||
import { useEventTracker, useProject, useProjectInbox, useWorkspace } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import useKeypress from "@/hooks/use-keypress";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// services
|
||||
import { DeDupeIssueButtonLabel, DuplicateModalRoot } from "@/plane-web/components/de-dupe";
|
||||
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
||||
import { FileService } from "@/services/file.service";
|
||||
|
||||
const fileService = new FileService();
|
||||
|
||||
type TInboxIssueCreateRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
handleModalClose: () => void;
|
||||
isDuplicateModalOpen: boolean;
|
||||
handleDuplicateIssueModal: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export const defaultIssueData: Partial<TIssue> = {
|
||||
|
|
@ -44,7 +49,7 @@ export const defaultIssueData: Partial<TIssue> = {
|
|||
};
|
||||
|
||||
export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, handleModalClose } = props;
|
||||
const { workspaceSlug, projectId, handleModalClose, isDuplicateModalOpen, handleDuplicateIssueModal } = props;
|
||||
// states
|
||||
const [uploadedAssetIds, setUploadedAssetIds] = useState<string[]>([]);
|
||||
// router
|
||||
|
|
@ -53,12 +58,15 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
|
|||
// refs
|
||||
const descriptionEditorRef = useRef<EditorRefApi>(null);
|
||||
const submitBtnRef = useRef<HTMLButtonElement | null>(null);
|
||||
const formRef = useRef<HTMLFormElement | null>(null);
|
||||
const modalContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
// hooks
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const { createInboxIssue } = useProjectInbox();
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id;
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { getProjectById } = useProject();
|
||||
// states
|
||||
const [createMore, setCreateMore] = useState<boolean>(false);
|
||||
const [formSubmitting, setFormSubmitting] = useState(false);
|
||||
|
|
@ -73,8 +81,17 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
|
|||
[formData]
|
||||
);
|
||||
|
||||
// derived values
|
||||
const projectDetails = projectId ? getProjectById(projectId) : undefined;
|
||||
|
||||
const { getIndex } = getTabIndex(ETabIndices.INTAKE_ISSUE_FORM, isMobile);
|
||||
|
||||
// debounced duplicate issues swr
|
||||
const { duplicateIssues } = useDebouncedDuplicateIssues(projectDetails?.workspace.toString(), projectId, {
|
||||
name: formData?.name,
|
||||
description_html: formData?.description_html,
|
||||
});
|
||||
|
||||
const handleEscKeyDown = (event: KeyboardEvent) => {
|
||||
if (descriptionEditorRef.current?.isEditorReadyToDiscard()) {
|
||||
handleModalClose();
|
||||
|
|
@ -90,6 +107,23 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
|
|||
|
||||
useKeypress("Escape", handleEscKeyDown);
|
||||
|
||||
useEffect(() => {
|
||||
const formElement = formRef?.current;
|
||||
const modalElement = modalContainerRef?.current;
|
||||
|
||||
if (!formElement || !modalElement) return;
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
modalElement.style.maxHeight = `${formElement?.offsetHeight}px`;
|
||||
});
|
||||
|
||||
resizeObserver.observe(formElement);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [formRef, modalContainerRef]);
|
||||
|
||||
const handleFormSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
|
|
@ -165,74 +199,109 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
|
|||
|
||||
const isTitleLengthMoreThan255Character = formData?.name ? formData.name.length > 255 : false;
|
||||
|
||||
const shouldRenderDuplicateModal = isDuplicateModalOpen && duplicateIssues?.length > 0;
|
||||
|
||||
if (!workspaceSlug || !projectId || !workspaceId) return <></>;
|
||||
return (
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
<div className="space-y-5 p-5">
|
||||
<h3 className="text-xl font-medium text-custom-text-200">Create intake issue</h3>
|
||||
<div className="space-y-3">
|
||||
<InboxIssueTitle
|
||||
data={formData}
|
||||
handleData={handleFormData}
|
||||
isTitleLengthMoreThan255Character={isTitleLengthMoreThan255Character}
|
||||
/>
|
||||
<InboxIssueDescription
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
workspaceId={workspaceId}
|
||||
data={formData}
|
||||
handleData={handleFormData}
|
||||
editorRef={descriptionEditorRef}
|
||||
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
|
||||
onEnterKeyPress={() => submitBtnRef?.current?.click()}
|
||||
onAssetUpload={(assetId) => setUploadedAssetIds((prev) => [...prev, assetId])}
|
||||
/>
|
||||
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} />
|
||||
</div>
|
||||
<div className="flex gap-2 bg-transparent w-full">
|
||||
<div className="rounded-lg w-full">
|
||||
<form ref={formRef} onSubmit={handleFormSubmit} className="flex flex-col w-full">
|
||||
<div className="space-y-5 p-5 rounded-t-lg bg-custom-background-100">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h3 className="text-xl font-medium text-custom-text-200">Create intake issue</h3>
|
||||
{duplicateIssues?.length > 0 && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleDuplicateIssueModal(!isDuplicateModalOpen);
|
||||
}}
|
||||
>
|
||||
<DeDupeIssueButtonLabel
|
||||
isOpen={isDuplicateModalOpen}
|
||||
buttonLabel={`${duplicateIssues.length} duplicate issue${duplicateIssues.length > 1 ? "s" : ""} found!`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<InboxIssueTitle
|
||||
data={formData}
|
||||
handleData={handleFormData}
|
||||
isTitleLengthMoreThan255Character={isTitleLengthMoreThan255Character}
|
||||
/>
|
||||
<InboxIssueDescription
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
workspaceId={workspaceId}
|
||||
data={formData}
|
||||
handleData={handleFormData}
|
||||
editorRef={descriptionEditorRef}
|
||||
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
|
||||
onEnterKeyPress={() => submitBtnRef?.current?.click()}
|
||||
onAssetUpload={(assetId) => setUploadedAssetIds((prev) => [...prev, assetId])}
|
||||
/>
|
||||
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5 py-4 flex items-center justify-between gap-2 border-t-[0.5px] border-custom-border-200 rounded-b-lg bg-custom-background-100">
|
||||
<div
|
||||
className="inline-flex items-center gap-1.5 cursor-pointer"
|
||||
onClick={() => setCreateMore((prevData) => !prevData)}
|
||||
role="button"
|
||||
tabIndex={getIndex("create_more")}
|
||||
>
|
||||
<ToggleSwitch value={createMore} onChange={() => {}} size="sm" />
|
||||
<span className="text-xs">Create more</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (descriptionEditorRef.current?.isEditorReadyToDiscard()) {
|
||||
handleModalClose();
|
||||
} else {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Editor is still processing changes. Please wait before proceeding.",
|
||||
});
|
||||
}
|
||||
}}
|
||||
tabIndex={getIndex("discard_button")}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
ref={submitBtnRef}
|
||||
size="sm"
|
||||
type="submit"
|
||||
loading={formSubmitting}
|
||||
disabled={isTitleLengthMoreThan255Character}
|
||||
tabIndex={getIndex("submit_button")}
|
||||
>
|
||||
{formSubmitting ? "Creating" : "Create Issue"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="px-5 py-4 flex items-center justify-between gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
{shouldRenderDuplicateModal && (
|
||||
<div
|
||||
className="inline-flex items-center gap-1.5 cursor-pointer"
|
||||
onClick={() => setCreateMore((prevData) => !prevData)}
|
||||
role="button"
|
||||
tabIndex={getIndex("create_more")}
|
||||
ref={modalContainerRef}
|
||||
className="relative flex flex-col gap-2.5 h-full px-3 py-4 rounded-lg shadow-xl bg-pi-50"
|
||||
style={{ maxHeight: formRef?.current?.offsetHeight ? `${formRef.current.offsetHeight}px` : "436px" }}
|
||||
>
|
||||
<ToggleSwitch value={createMore} onChange={() => { }} size="sm" />
|
||||
<span className="text-xs">Create more</span>
|
||||
<DuplicateModalRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
issues={duplicateIssues}
|
||||
handleDuplicateIssueModal={handleDuplicateIssueModal}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (descriptionEditorRef.current?.isEditorReadyToDiscard()) {
|
||||
handleModalClose();
|
||||
} else {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Editor is still processing changes. Please wait before proceeding.",
|
||||
});
|
||||
}
|
||||
}}
|
||||
tabIndex={getIndex("discard_button")}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
ref={submitBtnRef}
|
||||
size="sm"
|
||||
type="submit"
|
||||
loading={formSubmitting}
|
||||
disabled={isTitleLengthMoreThan255Character}
|
||||
tabIndex={getIndex("submit_button")}
|
||||
>
|
||||
{formSubmitting ? "Creating" : "Create Issue"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { FC } from "react";
|
||||
"use-client";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
// ui
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
|
||||
// components
|
||||
|
|
@ -13,15 +15,29 @@ type TInboxIssueCreateModalRoot = {
|
|||
|
||||
export const InboxIssueCreateModalRoot: FC<TInboxIssueCreateModalRoot> = (props) => {
|
||||
const { workspaceSlug, projectId, modalState, handleModalClose } = props;
|
||||
// states
|
||||
const [isDuplicateModalOpen, setIsDuplicateModalOpen] = useState(false);
|
||||
// handlers
|
||||
const handleDuplicateIssueModal = (value: boolean) => setIsDuplicateModalOpen(value);
|
||||
|
||||
return (
|
||||
<ModalCore
|
||||
isOpen={modalState}
|
||||
handleClose={handleModalClose}
|
||||
handleClose={() => {
|
||||
handleModalClose();
|
||||
setIsDuplicateModalOpen(false);
|
||||
}}
|
||||
position={EModalPosition.TOP}
|
||||
width={EModalWidth.XXXXL}
|
||||
width={isDuplicateModalOpen ? EModalWidth.VIXL : EModalWidth.XXXXL}
|
||||
className="!bg-transparent rounded-lg shadow-none transition-[width] ease-linear"
|
||||
>
|
||||
<InboxIssueCreateRoot workspaceSlug={workspaceSlug} projectId={projectId} handleModalClose={handleModalClose} />
|
||||
<InboxIssueCreateRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
handleModalClose={handleModalClose}
|
||||
isDuplicateModalOpen={isDuplicateModalOpen}
|
||||
handleDuplicateIssueModal={handleDuplicateIssueModal}
|
||||
/>
|
||||
</ModalCore>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
import { useState, Fragment } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { TIssue } from "@plane/types";
|
||||
// hooks
|
||||
// types
|
||||
import { TDeDupeIssue, TIssue } from "@plane/types";
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
// ui
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
data?: TIssue;
|
||||
data?: TIssue | TDeDupeIssue;
|
||||
dataId?: string | null | undefined;
|
||||
handleClose: () => void;
|
||||
isOpen: boolean;
|
||||
|
|
|
|||
|
|
@ -2,19 +2,21 @@
|
|||
|
||||
import { useEffect, useState } from "react";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
import { TDeDupeIssue, TIssue } from "@plane/types";
|
||||
// ui
|
||||
import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// constants
|
||||
import { PROJECT_ERROR_MESSAGES } from "@/constants/project";
|
||||
// hooks
|
||||
import { useIssues, useProject, useUser, useUserPermissions } from "@/hooks/store";
|
||||
// plane-web
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
dataId?: string | null | undefined;
|
||||
data?: TIssue;
|
||||
data?: TIssue | TDeDupeIssue;
|
||||
isSubIssue?: boolean;
|
||||
onSubmit?: () => Promise<void>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,12 +13,16 @@ import {
|
|||
IssueDetailWidgets,
|
||||
PeekOverviewProperties,
|
||||
} from "@/components/issues";
|
||||
// helpers
|
||||
import { getTextContent } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useIssueDetail, useUser } from "@/hooks/store";
|
||||
import { useIssueDetail, useProject, useUser } from "@/hooks/store";
|
||||
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
|
||||
import useSize from "@/hooks/use-window-size";
|
||||
// plane web components
|
||||
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe";
|
||||
import { IssueTypeSwitcher } from "@/plane-web/components/issues";
|
||||
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
||||
// types
|
||||
import { TIssueOperations } from "./root";
|
||||
|
||||
|
|
@ -42,8 +46,20 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||
issue: { getIssueById },
|
||||
peekIssue,
|
||||
} = useIssueDetail();
|
||||
const { getProjectById } = useProject();
|
||||
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
|
||||
|
||||
// derived values
|
||||
const projectDetails = getProjectById(projectId);
|
||||
const issue = issueId ? getIssueById(issueId) : undefined;
|
||||
|
||||
// debounced duplicate issues swr
|
||||
const { duplicateIssues } = useDebouncedDuplicateIssues(projectDetails?.workspace.toString(), projectDetails?.id, {
|
||||
name: issue?.name,
|
||||
description_html: getTextContent(issue?.description_html),
|
||||
issueId: issue?.id,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isSubmitting === "submitted") {
|
||||
setShowAlert(false);
|
||||
|
|
@ -51,7 +67,6 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||
} else if (isSubmitting === "submitting") setShowAlert(true);
|
||||
}, [isSubmitting, setShowAlert, setIsSubmitting]);
|
||||
|
||||
const issue = issueId ? getIssueById(issueId) : undefined;
|
||||
if (!issue || !issue.project_id) return <></>;
|
||||
|
||||
const isPeekModeActive = Boolean(peekIssue);
|
||||
|
|
@ -71,7 +86,19 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||
|
||||
<div className="mb-2.5 flex items-center justify-between gap-4">
|
||||
<IssueTypeSwitcher issueId={issueId} disabled={isArchived || !isEditable} />
|
||||
<IssueUpdateStatus isSubmitting={isSubmitting} />
|
||||
<div className="flex items-center gap-3">
|
||||
<IssueUpdateStatus isSubmitting={isSubmitting} />
|
||||
{duplicateIssues?.length > 0 && (
|
||||
<DeDupeIssuePopoverRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
rootIssueId={issueId}
|
||||
issues={duplicateIssues}
|
||||
issueOperations={issueOperations}
|
||||
renderDeDupeActionModals={!isPeekModeActive}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<IssueTitleInput
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
|||
const [activeProjectId, setActiveProjectId] = useState<string | null>(null);
|
||||
const [description, setDescription] = useState<string | undefined>(undefined);
|
||||
const [uploadedAssetIds, setUploadedAssetIds] = useState<string[]>([]);
|
||||
const [isDuplicateModalOpen, setIsDuplicateModalOpen] = useState(false);
|
||||
// store hooks
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const { workspaceSlug, projectId: routerProjectId, cycleId, moduleId } = useParams();
|
||||
|
|
@ -139,6 +140,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
|||
setActiveProjectId(null);
|
||||
setChangesMade(null);
|
||||
onClose();
|
||||
handleDuplicateIssueModal(false);
|
||||
};
|
||||
|
||||
const handleCreateIssue = async (
|
||||
|
|
@ -325,6 +327,8 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
|||
|
||||
const handleUpdateUploadedAssetIds = (assetId: string) => setUploadedAssetIds((prev) => [...prev, assetId]);
|
||||
|
||||
const handleDuplicateIssueModal = (value: boolean) => setIsDuplicateModalOpen(value);
|
||||
|
||||
// don't open the modal if there are no projects
|
||||
if (!projectIdsWithCreatePermissions || projectIdsWithCreatePermissions.length === 0 || !activeProjectId) return null;
|
||||
|
||||
|
|
@ -333,7 +337,8 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
|||
isOpen={isOpen}
|
||||
handleClose={() => handleClose(true)}
|
||||
position={EModalPosition.TOP}
|
||||
width={EModalWidth.XXXXL}
|
||||
width={isDuplicateModalOpen ? EModalWidth.VIXL : EModalWidth.XXXXL}
|
||||
className="!bg-transparent rounded-lg shadow-none transition-[width] ease-linear"
|
||||
>
|
||||
{withDraftIssueWrapper ? (
|
||||
<DraftIssueLayout
|
||||
|
|
@ -354,6 +359,8 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
|||
onCreateMoreToggleChange={handleCreateMoreToggleChange}
|
||||
isDraft={isDraft}
|
||||
moveToIssue={moveToIssue}
|
||||
isDuplicateModalOpen={isDuplicateModalOpen}
|
||||
handleDuplicateIssueModal={handleDuplicateIssueModal}
|
||||
/>
|
||||
) : (
|
||||
<IssueFormRoot
|
||||
|
|
@ -374,6 +381,8 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
|||
moveToIssue={moveToIssue}
|
||||
modalTitle={modalTitle}
|
||||
primaryButtonText={primaryButtonText}
|
||||
isDuplicateModalOpen={isDuplicateModalOpen}
|
||||
handleDuplicateIssueModal={handleDuplicateIssueModal}
|
||||
/>
|
||||
)}
|
||||
</ModalCore>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ export interface DraftIssueProps {
|
|||
default: string;
|
||||
loading: string;
|
||||
};
|
||||
isDuplicateModalOpen: boolean;
|
||||
handleDuplicateIssueModal: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
|
||||
|
|
@ -54,6 +56,8 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
|
|||
moveToIssue = false,
|
||||
modalTitle,
|
||||
primaryButtonText,
|
||||
isDuplicateModalOpen,
|
||||
handleDuplicateIssueModal,
|
||||
} = props;
|
||||
// states
|
||||
const [issueDiscardModal, setIssueDiscardModal] = useState(false);
|
||||
|
|
@ -173,6 +177,8 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
|
|||
moveToIssue={moveToIssue}
|
||||
modalTitle={modalTitle}
|
||||
primaryButtonText={primaryButtonText}
|
||||
isDuplicateModalOpen={isDuplicateModalOpen}
|
||||
handleDuplicateIssueModal={handleDuplicateIssueModal}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { CreateLabelModal } from "@/components/labels";
|
|||
import { ETabIndices } from "@/constants/tab-indices";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getTextContent } from "@/helpers/editor.helper";
|
||||
import { getChangedIssuefields } from "@/helpers/issue.helper";
|
||||
import { getTabIndex } from "@/helpers/tab-indices.helper";
|
||||
// hooks
|
||||
|
|
@ -30,7 +31,9 @@ import { useIssueDetail, useProject, useProjectState, useWorkspaceDraftIssues }
|
|||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { useProjectIssueProperties } from "@/hooks/use-project-issue-properties";
|
||||
// plane web components
|
||||
import { DeDupeIssueButtonLabel, DuplicateModalRoot } from "@/plane-web/components/de-dupe";
|
||||
import { IssueAdditionalProperties, IssueTypeSelect } from "@/plane-web/components/issues/issue-modal";
|
||||
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
||||
|
||||
const defaultValues: Partial<TIssue> = {
|
||||
project_id: "",
|
||||
|
|
@ -66,6 +69,8 @@ export interface IssueFormProps {
|
|||
default: string;
|
||||
loading: string;
|
||||
};
|
||||
isDuplicateModalOpen: boolean;
|
||||
handleDuplicateIssueModal: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
|
|
@ -86,6 +91,8 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||
default: `${data?.id ? "Update" : isDraft ? "Save to Drafts" : "Save"}`,
|
||||
loading: `${data?.id ? "Updating" : "Saving"}`,
|
||||
},
|
||||
isDuplicateModalOpen,
|
||||
handleDuplicateIssueModal,
|
||||
} = props;
|
||||
|
||||
// states
|
||||
|
|
@ -96,6 +103,8 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||
// refs
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
const submitBtnRef = useRef<HTMLButtonElement | null>(null);
|
||||
const formRef = useRef<HTMLFormElement | null>(null);
|
||||
const modalContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// router
|
||||
const { workspaceSlug, projectId: routeProjectId } = useParams();
|
||||
|
|
@ -134,6 +143,9 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||
watch: watch,
|
||||
});
|
||||
|
||||
// derived values
|
||||
const projectDetails = projectId ? getProjectById(projectId) : undefined;
|
||||
|
||||
const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile);
|
||||
|
||||
//reset few fields on projectId change
|
||||
|
|
@ -232,6 +244,16 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||
else onChange(null);
|
||||
};
|
||||
|
||||
// debounced duplicate issues swr
|
||||
const { duplicateIssues } = useDebouncedDuplicateIssues(
|
||||
projectDetails?.workspace.toString(),
|
||||
projectId ?? undefined,
|
||||
{
|
||||
name: watch("name"),
|
||||
description_html: getTextContent(watch("description_html")),
|
||||
}
|
||||
);
|
||||
|
||||
// executing this useEffect when the parent_id coming from the component prop
|
||||
useEffect(() => {
|
||||
const parentId = watch("parent_id") || undefined;
|
||||
|
|
@ -267,6 +289,27 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isDirty]);
|
||||
|
||||
useEffect(() => {
|
||||
const formElement = formRef?.current;
|
||||
const modalElement = modalContainerRef?.current;
|
||||
|
||||
if (!formElement || !modalElement) return;
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
modalElement.style.maxHeight = `${formElement?.offsetHeight}px`;
|
||||
});
|
||||
|
||||
resizeObserver.observe(formElement);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [formRef, modalContainerRef]);
|
||||
|
||||
// TODO: Remove this after the de-dupe feature is implemented
|
||||
|
||||
const shouldRenderDuplicateModal = isDuplicateModalOpen && duplicateIssues?.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{projectId && (
|
||||
|
|
@ -280,175 +323,211 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
<form onSubmit={handleSubmit((data) => handleFormSubmit(data))}>
|
||||
<div className="p-5">
|
||||
<h3 className="text-xl font-medium text-custom-text-200 pb-2">{modalTitle}</h3>
|
||||
{/* Disable project selection if editing an issue */}
|
||||
<div className="flex items-center pt-2 pb-4 gap-x-1">
|
||||
<IssueProjectSelect
|
||||
control={control}
|
||||
disabled={!!data?.id || !!data?.sourceIssueId}
|
||||
handleFormChange={handleFormChange}
|
||||
/>
|
||||
{projectId && (
|
||||
<IssueTypeSelect
|
||||
control={control}
|
||||
projectId={projectId}
|
||||
disabled={!!data?.sourceIssueId}
|
||||
handleFormChange={handleFormChange}
|
||||
renderChevron
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{watch("parent_id") && selectedParentIssue && (
|
||||
<div className="pb-4">
|
||||
<IssueParentTag
|
||||
control={control}
|
||||
selectedParentIssue={selectedParentIssue}
|
||||
handleFormChange={handleFormChange}
|
||||
setSelectedParentIssue={setSelectedParentIssue}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
<IssueTitleInput
|
||||
control={control}
|
||||
issueTitleRef={issueTitleRef}
|
||||
errors={errors}
|
||||
handleFormChange={handleFormChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"pb-4 space-y-3",
|
||||
activeAdditionalPropertiesLength > 4 &&
|
||||
"max-h-[45vh] overflow-hidden overflow-y-auto vertical-scrollbar scrollbar-sm"
|
||||
)}
|
||||
>
|
||||
<div className="px-5">
|
||||
<IssueDescriptionEditor
|
||||
control={control}
|
||||
isDraft={isDraft}
|
||||
issueName={watch("name")}
|
||||
issueId={data?.id}
|
||||
descriptionHtmlData={data?.description_html}
|
||||
editorRef={editorRef}
|
||||
submitBtnRef={submitBtnRef}
|
||||
gptAssistantModal={gptAssistantModal}
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={projectId}
|
||||
handleFormChange={handleFormChange}
|
||||
handleDescriptionHTMLDataChange={(description_html) =>
|
||||
setValue<"description_html">("description_html", description_html)
|
||||
}
|
||||
setGptAssistantModal={setGptAssistantModal}
|
||||
handleGptAssistantClose={() => reset(getValues())}
|
||||
onAssetUpload={onAssetUpload}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"px-5",
|
||||
activeAdditionalPropertiesLength <= 4 &&
|
||||
"max-h-[25vh] overflow-hidden overflow-y-auto vertical-scrollbar scrollbar-sm"
|
||||
)}
|
||||
<div className="flex gap-2 bg-transparent">
|
||||
<div className="rounded-lg w-full">
|
||||
<form
|
||||
ref={formRef}
|
||||
onSubmit={handleSubmit((data) => handleFormSubmit(data))}
|
||||
className="flex flex-col w-full"
|
||||
>
|
||||
{projectId && (
|
||||
<IssueAdditionalProperties
|
||||
issueId={data?.id ?? data?.sourceIssueId}
|
||||
issueTypeId={watch("type_id")}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
isDraft={isDraft}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-5 rounded-t-lg bg-custom-background-100">
|
||||
<h3 className="text-xl font-medium text-custom-text-200 pb-2">{modalTitle}</h3>
|
||||
<div className="flex items-center justify-between pt-2 pb-4">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<IssueProjectSelect
|
||||
control={control}
|
||||
disabled={!!data?.id || !!data?.sourceIssueId}
|
||||
handleFormChange={handleFormChange}
|
||||
/>
|
||||
{projectId && (
|
||||
<IssueTypeSelect
|
||||
control={control}
|
||||
projectId={projectId}
|
||||
disabled={!!data?.sourceIssueId}
|
||||
handleFormChange={handleFormChange}
|
||||
renderChevron
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{duplicateIssues.length > 0 && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleDuplicateIssueModal(!isDuplicateModalOpen);
|
||||
}}
|
||||
>
|
||||
<DeDupeIssueButtonLabel
|
||||
isOpen={isDuplicateModalOpen}
|
||||
buttonLabel={`${duplicateIssues.length} duplicate issue${duplicateIssues.length > 1 ? "s" : ""} found!`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{watch("parent_id") && selectedParentIssue && (
|
||||
<div className="pb-4">
|
||||
<IssueParentTag
|
||||
control={control}
|
||||
selectedParentIssue={selectedParentIssue}
|
||||
handleFormChange={handleFormChange}
|
||||
setSelectedParentIssue={setSelectedParentIssue}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
<IssueTitleInput
|
||||
control={control}
|
||||
issueTitleRef={issueTitleRef}
|
||||
errors={errors}
|
||||
handleFormChange={handleFormChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"pb-4 space-y-3 bg-custom-background-100",
|
||||
activeAdditionalPropertiesLength > 4 &&
|
||||
"max-h-[45vh] overflow-hidden overflow-y-auto vertical-scrollbar scrollbar-sm"
|
||||
)}
|
||||
>
|
||||
<div className="px-5">
|
||||
<IssueDescriptionEditor
|
||||
control={control}
|
||||
isDraft={isDraft}
|
||||
issueName={watch("name")}
|
||||
issueId={data?.id}
|
||||
descriptionHtmlData={data?.description_html}
|
||||
editorRef={editorRef}
|
||||
submitBtnRef={submitBtnRef}
|
||||
gptAssistantModal={gptAssistantModal}
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={projectId}
|
||||
handleFormChange={handleFormChange}
|
||||
handleDescriptionHTMLDataChange={(description_html) =>
|
||||
setValue<"description_html">("description_html", description_html)
|
||||
}
|
||||
setGptAssistantModal={setGptAssistantModal}
|
||||
handleGptAssistantClose={() => reset(getValues())}
|
||||
onAssetUpload={onAssetUpload}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"px-5",
|
||||
activeAdditionalPropertiesLength <= 4 &&
|
||||
"max-h-[25vh] overflow-hidden overflow-y-auto vertical-scrollbar scrollbar-sm"
|
||||
)}
|
||||
>
|
||||
{projectId && (
|
||||
<IssueAdditionalProperties
|
||||
issueId={data?.id ?? data?.sourceIssueId}
|
||||
issueTypeId={watch("type_id")}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
isDraft={isDraft}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 py-3 border-t-[0.5px] border-custom-border-200 shadow-custom-shadow-xs rounded-b-lg bg-custom-background-100">
|
||||
<div className="pb-3 border-b-[0.5px] border-custom-border-200">
|
||||
<IssueDefaultProperties
|
||||
control={control}
|
||||
id={data?.id}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
selectedParentIssue={selectedParentIssue}
|
||||
startDate={watch("start_date")}
|
||||
targetDate={watch("target_date")}
|
||||
parentId={watch("parent_id")}
|
||||
isDraft={isDraft}
|
||||
handleFormChange={handleFormChange}
|
||||
setLabelModal={setLabelModal}
|
||||
setSelectedParentIssue={setSelectedParentIssue}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-4 py-3">
|
||||
{!data?.id && (
|
||||
<div
|
||||
className="inline-flex items-center gap-1.5 cursor-pointer"
|
||||
onClick={() => onCreateMoreToggleChange(!isCreateMoreToggleEnabled)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") onCreateMoreToggleChange(!isCreateMoreToggleEnabled);
|
||||
}}
|
||||
tabIndex={getIndex("create_more")}
|
||||
role="button"
|
||||
>
|
||||
<ToggleSwitch value={isCreateMoreToggleEnabled} onChange={() => {}} size="sm" />
|
||||
<span className="text-xs">Create more</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (editorRef.current?.isEditorReadyToDiscard()) {
|
||||
onClose();
|
||||
} else {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Editor is still processing changes. Please wait before proceeding.",
|
||||
});
|
||||
}
|
||||
}}
|
||||
tabIndex={getIndex("discard_button")}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
variant={moveToIssue ? "neutral-primary" : "primary"}
|
||||
type="submit"
|
||||
size="sm"
|
||||
ref={submitBtnRef}
|
||||
loading={isSubmitting}
|
||||
tabIndex={isDraft ? getIndex("submit_button") : getIndex("draft_button")}
|
||||
>
|
||||
{isSubmitting ? primaryButtonText.loading : primaryButtonText.default}
|
||||
</Button>
|
||||
{moveToIssue && (
|
||||
<Button
|
||||
variant="primary"
|
||||
type="button"
|
||||
size="sm"
|
||||
loading={isSubmitting}
|
||||
onClick={() => {
|
||||
if (data?.id && data) {
|
||||
moveIssue(workspaceSlug.toString(), data?.id, {
|
||||
...data,
|
||||
...getValues(),
|
||||
} as TWorkspaceDraftIssue);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add to project
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="px-4 py-3 border-t-[0.5px] border-custom-border-200 shadow-custom-shadow-xs rounded-b-lg">
|
||||
<div className="pb-3 border-b-[0.5px] border-custom-border-200">
|
||||
<IssueDefaultProperties
|
||||
control={control}
|
||||
id={data?.id}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
selectedParentIssue={selectedParentIssue}
|
||||
startDate={watch("start_date")}
|
||||
targetDate={watch("target_date")}
|
||||
parentId={watch("parent_id")}
|
||||
isDraft={isDraft}
|
||||
handleFormChange={handleFormChange}
|
||||
setLabelModal={setLabelModal}
|
||||
setSelectedParentIssue={setSelectedParentIssue}
|
||||
{shouldRenderDuplicateModal && (
|
||||
<div
|
||||
ref={modalContainerRef}
|
||||
className="relative flex flex-col gap-2.5 h-full px-3 py-4 rounded-lg shadow-xl bg-pi-50"
|
||||
style={{ maxHeight: formRef?.current?.offsetHeight ? `${formRef.current.offsetHeight}px` : "436px" }}
|
||||
>
|
||||
<DuplicateModalRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
issues={duplicateIssues}
|
||||
handleDuplicateIssueModal={handleDuplicateIssueModal}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-4 py-3">
|
||||
{!data?.id && (
|
||||
<div
|
||||
className="inline-flex items-center gap-1.5 cursor-pointer"
|
||||
onClick={() => onCreateMoreToggleChange(!isCreateMoreToggleEnabled)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") onCreateMoreToggleChange(!isCreateMoreToggleEnabled);
|
||||
}}
|
||||
tabIndex={getIndex("create_more")}
|
||||
role="button"
|
||||
>
|
||||
<ToggleSwitch value={isCreateMoreToggleEnabled} onChange={() => {}} size="sm" />
|
||||
<span className="text-xs">Create more</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (editorRef.current?.isEditorReadyToDiscard()) {
|
||||
onClose();
|
||||
} else {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Editor is still processing changes. Please wait before proceeding.",
|
||||
});
|
||||
}
|
||||
}}
|
||||
tabIndex={getIndex("discard_button")}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
variant={moveToIssue ? "neutral-primary" : "primary"}
|
||||
type="submit"
|
||||
size="sm"
|
||||
ref={submitBtnRef}
|
||||
loading={isSubmitting}
|
||||
tabIndex={isDraft ? getIndex("submit_button") : getIndex("draft_button")}
|
||||
>
|
||||
{isSubmitting ? primaryButtonText.loading : primaryButtonText.default}
|
||||
</Button>
|
||||
{moveToIssue && (
|
||||
<Button
|
||||
variant="primary"
|
||||
type="button"
|
||||
size="sm"
|
||||
loading={isSubmitting}
|
||||
onClick={() => {
|
||||
if (data?.id && data) {
|
||||
moveIssue(workspaceSlug.toString(), data?.id, {
|
||||
...data,
|
||||
...getValues(),
|
||||
} as TWorkspaceDraftIssue);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add to project
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
"use-client";
|
||||
import { FC, useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { IssueParentDetail, TIssueOperations } from "@/components/issues";
|
||||
// helpers
|
||||
import { getTextContent } from "@/helpers/editor.helper";
|
||||
// store hooks
|
||||
import { useIssueDetail, useUser } from "@/hooks/store";
|
||||
import { useIssueDetail, useProject, useUser } from "@/hooks/store";
|
||||
// hooks
|
||||
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
|
||||
// plane web components
|
||||
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe";
|
||||
import { IssueTypeSwitcher } from "@/plane-web/components/issues";
|
||||
// local components
|
||||
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
||||
import { IssueDescriptionInput } from "../description-input";
|
||||
import { IssueReaction } from "../issue-detail/reactions";
|
||||
import { IssueTitleInput } from "../title-input";
|
||||
|
|
@ -31,6 +36,7 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
|
|||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
const { getProjectById } = useProject();
|
||||
// hooks
|
||||
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
|
||||
|
||||
|
|
@ -45,7 +51,16 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
|
|||
}
|
||||
}, [isSubmitting, setShowAlert, setIsSubmitting]);
|
||||
|
||||
// derived values
|
||||
const issue = issueId ? getIssueById(issueId) : undefined;
|
||||
const projectDetails = issue?.project_id ? getProjectById(issue?.project_id) : undefined;
|
||||
// debounced duplicate issues swr
|
||||
const { duplicateIssues } = useDebouncedDuplicateIssues(projectDetails?.workspace.toString(), projectDetails?.id, {
|
||||
name: issue?.name,
|
||||
description_html: getTextContent(issue?.description_html),
|
||||
issueId: issue?.id,
|
||||
});
|
||||
|
||||
if (!issue || !issue.project_id) return <></>;
|
||||
|
||||
const issueDescription =
|
||||
|
|
@ -66,7 +81,18 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
|
|||
issueOperations={issueOperations}
|
||||
/>
|
||||
)}
|
||||
<IssueTypeSwitcher issueId={issueId} disabled={isArchived || disabled} />
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<IssueTypeSwitcher issueId={issueId} disabled={isArchived || disabled} />
|
||||
{duplicateIssues?.length > 0 && (
|
||||
<DeDupeIssuePopoverRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
rootIssueId={issueId}
|
||||
issues={duplicateIssues}
|
||||
issueOperations={issueOperations}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<IssueTitleInput
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
|
|
|
|||
|
|
@ -265,3 +265,11 @@ export const replaceCustomComponentsFromMarkdownContent = (props: {
|
|||
parsedMarkdownContent = parsedMarkdownContent.replace(issueEmbedRegex, "");
|
||||
return parsedMarkdownContent;
|
||||
};
|
||||
|
||||
export const getTextContent = (jsx: JSX.Element | React.ReactNode | null | undefined): string => {
|
||||
if (!jsx) return "";
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = jsx.toString();
|
||||
return div.textContent?.trim() ?? "";
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue