diff --git a/web/components/command-palette/command-palette.tsx b/web/components/command-palette/command-palette.tsx index e5f781dd9..04b2fb714 100644 --- a/web/components/command-palette/command-palette.tsx +++ b/web/components/command-palette/command-palette.tsx @@ -60,7 +60,6 @@ export const CommandPalette: FC = observer(() => { isDeleteIssueModalOpen, toggleDeleteIssueModal, isAnyModalOpen, - createIssueStoreType, } = commandPalette; const { setToastAlert } = useToast(); @@ -215,11 +214,8 @@ export const CommandPalette: FC = observer(() => { toggleCreateIssueModal(false)} - prePopulateData={ - cycleId ? { cycle_id: cycleId.toString() } : moduleId ? { module_id: moduleId.toString() } : undefined - } - currentStore={createIssueStoreType} + onClose={() => toggleCreateIssueModal(false)} + data={cycleId ? { cycle_id: cycleId.toString() } : moduleId ? { module_id: moduleId.toString() } : undefined} /> {workspaceSlug && projectId && issueId && issueDetails && ( diff --git a/web/components/issues/draft-issue-form.tsx b/web/components/issues/draft-issue-form.tsx index 2d79f4ee1..9c6a9bb04 100644 --- a/web/components/issues/draft-issue-form.tsx +++ b/web/components/issues/draft-issue-form.tsx @@ -4,7 +4,7 @@ import { Controller, useForm } from "react-hook-form"; import { observer } from "mobx-react-lite"; import { Sparkle, X } from "lucide-react"; // hooks -import { useApplication, useEstimate, useMention } from "hooks/store"; +import { useApplication, useEstimate, useMention, useProject } from "hooks/store"; import useToast from "hooks/use-toast"; import useLocalStorage from "hooks/use-local-storage"; // services @@ -18,8 +18,10 @@ import { CreateStateModal } from "components/states"; import { CreateLabelModal } from "components/labels"; import { RichTextEditorWithRef } from "@plane/rich-text-editor"; import { + CycleDropdown, DateDropdown, EstimateDropdown, + ModuleDropdown, PriorityDropdown, ProjectDropdown, ProjectMemberDropdown, @@ -103,7 +105,7 @@ export const DraftIssueForm: FC = observer((props) => { const [gptAssistantModal, setGptAssistantModal] = useState(false); const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); // store hooks - const { areEstimatesActiveForProject } = useEstimate(); + const { areEstimatesEnabledForProject } = useEstimate(); const { mentionHighlights, mentionSuggestions } = useMention(); // hooks const { setValue: setLocalStorageValue } = useLocalStorage("draftedIssue", {}); @@ -117,6 +119,7 @@ export const DraftIssueForm: FC = observer((props) => { const { config: { envConfig }, } = useApplication(); + const { getProjectById } = useProject(); // form info const { formState: { errors, isSubmitting }, @@ -277,6 +280,8 @@ export const DraftIssueForm: FC = observer((props) => { const maxDate = targetDate ? new Date(targetDate) : null; maxDate?.setDate(maxDate.getDate()); + const projectDetails = getProjectById(projectId); + return ( <> {projectId && ( @@ -302,19 +307,21 @@ export const DraftIssueForm: FC = observer((props) => { control={control} name="project_id" render={({ field: { value, onChange } }) => ( - { - onChange(val); - setActiveProject(val); - }} - buttonVariant="background-with-text" - /> +
+ { + onChange(val); + setActiveProject(val); + }} + buttonVariant="border-with-text" + /> +
)} /> )}

- {status ? "Update" : "Create"} Issue + {status ? "Update" : "Create"} issue

{watch("parent_id") && @@ -374,11 +381,11 @@ export const DraftIssueForm: FC = observer((props) => { )} {(fieldsToShow.includes("all") || fieldsToShow.includes("description")) && (
-
+
{issueName && issueName !== "" && ( @@ -408,10 +415,10 @@ export const DraftIssueForm: FC = observer((props) => { button={ } @@ -470,7 +477,7 @@ export const DraftIssueForm: FC = observer((props) => { name="priority" render={({ field: { value, onChange } }) => (
- +
)} /> @@ -485,8 +492,10 @@ export const DraftIssueForm: FC = observer((props) => { projectId={projectId} value={value} onChange={onChange} + buttonVariant={value?.length > 0 ? "transparent-without-text" : "border-with-text"} + buttonClassName={value?.length > 0 ? "hover:bg-transparent px-0" : ""} + placeholder="Assignees" multiple - buttonVariant="background-with-text" />
)} @@ -542,8 +551,40 @@ export const DraftIssueForm: FC = observer((props) => { )} /> )} + {projectDetails?.cycle_view && ( + ( +
+ onChange(cycleId)} + value={value} + buttonVariant="border-with-text" + /> +
+ )} + /> + )} + {projectDetails?.module_view && ( + ( +
+ onChange(moduleId)} + buttonVariant="border-with-text" + /> +
+ )} + /> + )} {(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && - areEstimatesActiveForProject(projectId) && ( + areEstimatesEnabledForProject(projectId) && ( = observer((props) => { value={value} onChange={onChange} projectId={projectId} - buttonVariant="background-with-text" + buttonVariant="border-with-text" />
)} diff --git a/web/components/issues/draft-issue-modal.tsx b/web/components/issues/draft-issue-modal.tsx index 4008e6383..39a5fbc5f 100644 --- a/web/components/issues/draft-issue-modal.tsx +++ b/web/components/issues/draft-issue-modal.tsx @@ -322,7 +322,7 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - + = { - project_id: "", - name: "", - description_html: "

", - estimate_point: null, - state_id: "", - parent_id: null, - priority: "none", - assignee_ids: [], - label_ids: [], - start_date: undefined, - target_date: undefined, -}; - -export interface IssueFormProps { - handleFormSubmit: (values: Partial) => Promise; - initialData?: Partial; - projectId: string; - setActiveProject: React.Dispatch>; - createMore: boolean; - setCreateMore: React.Dispatch>; - handleDiscardClose: () => void; - status: boolean; - handleFormDirty: (payload: Partial | null) => void; - fieldsToShow: ( - | "project" - | "name" - | "description" - | "state" - | "priority" - | "assignee" - | "label" - | "startDate" - | "dueDate" - | "estimate" - | "parent" - | "all" - | "module" - | "cycle" - )[]; -} - -// services -const aiService = new AIService(); -const fileService = new FileService(); - -export const IssueForm: FC = observer((props) => { - const { - handleFormSubmit, - initialData, - projectId, - setActiveProject, - createMore, - setCreateMore, - handleDiscardClose, - status, - fieldsToShow, - handleFormDirty, - } = props; - // states - const [stateModal, setStateModal] = useState(false); - const [labelModal, setLabelModal] = useState(false); - const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false); - const [selectedParentIssue, setSelectedParentIssue] = useState(null); - const [gptAssistantModal, setGptAssistantModal] = useState(false); - const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); - // refs - const editorRef = useRef(null); - // router - const router = useRouter(); - const { workspaceSlug } = router.query; - // store hooks - const { - config: { envConfig }, - } = useApplication(); - const { getProjectById } = useProject(); - const { areEstimatesActiveForProject } = useEstimate(); - const { mentionHighlights, mentionSuggestions } = useMention(); - // toast alert - const { setToastAlert } = useToast(); - // form info - const { - formState: { errors, isSubmitting, isDirty }, - handleSubmit, - reset, - watch, - control, - getValues, - setValue, - setFocus, - } = useForm({ - defaultValues: initialData ?? defaultValues, - reValidateMode: "onChange", - }); - - const issueName = watch("name"); - - const payload: Partial = { - name: getValues("name"), - state_id: getValues("state_id"), - priority: getValues("priority"), - assignee_ids: getValues("assignee_ids"), - label_ids: getValues("label_ids"), - start_date: getValues("start_date"), - target_date: getValues("target_date"), - project_id: getValues("project_id"), - parent_id: getValues("parent_id"), - cycle_id: getValues("cycle_id"), - module_id: getValues("module_id"), - }; - - // derived values - const projectDetails = getProjectById(projectId); - - useEffect(() => { - if (isDirty) handleFormDirty(payload); - else handleFormDirty(null); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(payload), isDirty]); - - const handleCreateUpdateIssue = async (formData: Partial) => { - await handleFormSubmit(formData); - - setGptAssistantModal(false); - - reset({ - ...defaultValues, - project_id: projectId, - description_html: "

", - }); - editorRef?.current?.clearEditor(); - }; - - const handleAiAssistance = async (response: string) => { - if (!workspaceSlug || !projectId) return; - - setValue("description_html", `${watch("description_html")}

${response}

`); - editorRef.current?.setEditorValue(`${watch("description_html")}`); - }; - - const handleAutoGenerateDescription = async () => { - if (!workspaceSlug || !projectId) return; - - setIAmFeelingLucky(true); - - aiService - .createGptTask(workspaceSlug.toString(), projectId.toString(), { - prompt: issueName, - task: "Generate a proper description for this issue.", - }) - .then((res) => { - if (res.response === "") - setToastAlert({ - type: "error", - title: "Error!", - message: - "Issue title isn't informative enough to generate the description. Please try with a different title.", - }); - else handleAiAssistance(res.response_html); - }) - .catch((err) => { - const error = err?.data?.error; - - if (err.status === 429) - setToastAlert({ - type: "error", - title: "Error!", - message: error || "You have reached the maximum number of requests of 50 requests per month per user.", - }); - else - setToastAlert({ - type: "error", - title: "Error!", - message: error || "Some error occurred. Please try again.", - }); - }) - .finally(() => setIAmFeelingLucky(false)); - }; - - useEffect(() => { - setFocus("name"); - - reset({ - ...defaultValues, - ...initialData, - }); - }, [setFocus, initialData, reset]); - - // update projectId in form when projectId changes - useEffect(() => { - reset({ - ...getValues(), - project_id: projectId, - }); - }, [getValues, projectId, reset]); - - const startDate = watch("start_date"); - const targetDate = watch("target_date"); - - const minDate = startDate ? new Date(startDate) : null; - minDate?.setDate(minDate.getDate()); - - const maxDate = targetDate ? new Date(targetDate) : null; - maxDate?.setDate(maxDate.getDate()); - - return ( - <> - {projectId && ( - <> - setStateModal(false)} projectId={projectId} /> - setLabelModal(false)} - projectId={projectId} - onSuccess={(response) => setValue("label_ids", [...watch("label_ids"), response.id])} - /> - - )} -
-
-
- {(fieldsToShow.includes("all") || fieldsToShow.includes("project")) && !status && ( - ( -
- { - onChange(val); - setActiveProject(val); - }} - buttonVariant="border-with-text" - /> -
- )} - /> - )} -

- {status ? "Update" : "Create"} Issue -

-
- {watch("parent_id") && - (fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && - selectedParentIssue && ( -
-
- - - {selectedParentIssue.project__identifier}-{selectedParentIssue.sequence_id} - - {selectedParentIssue.name.substring(0, 50)} - { - setValue("parent_id", null); - setSelectedParentIssue(null); - }} - /> -
-
- )} -
-
- {(fieldsToShow.includes("all") || fieldsToShow.includes("name")) && ( -
- ( - - )} - /> -
- )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("description")) && ( -
-
- {issueName && issueName !== "" && ( - - )} - {envConfig?.has_openai_configured && ( - { - setGptAssistantModal((prevData) => !prevData); - // this is done so that the title do not reset after gpt popover closed - reset(getValues()); - }} - onResponse={(response) => { - handleAiAssistance(response); - }} - placement="top-end" - button={ - - } - /> - )} -
- ( - { - onChange(description_html); - }} - mentionHighlights={mentionHighlights} - mentionSuggestions={mentionSuggestions} - /> - )} - /> -
- )} -
- {(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && ( - ( -
- -
- )} - /> - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && ( - ( -
- -
- )} - /> - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && ( - ( -
- 0 ? "transparent-without-text" : "border-with-text"} - buttonClassName={value?.length > 0 ? "hover:bg-transparent px-0" : ""} - placeholder="Assignees" - multiple - /> -
- )} - /> - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && ( - ( -
- -
- )} - /> - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && ( - ( -
- onChange(date ? renderFormattedPayloadDate(date) : null)} - buttonVariant="border-with-text" - placeholder="Start date" - maxDate={maxDate ?? undefined} - /> -
- )} - /> - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && ( -
- ( -
- onChange(date ? renderFormattedPayloadDate(date) : null)} - buttonVariant="border-with-text" - placeholder="Due date" - minDate={minDate ?? undefined} - /> -
- )} - /> -
- )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("cycle")) && projectDetails?.cycle_view && ( - ( -
- -
- )} - /> - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("module")) && projectDetails?.module_view && ( - ( -
- -
- )} - /> - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && - areEstimatesActiveForProject(projectId) && ( - ( -
- -
- )} - /> - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( - <> - {watch("parent_id") ? ( - -
- - - {selectedParentIssue && - `${selectedParentIssue.project__identifier}- - ${selectedParentIssue.sequence_id}`} - -
- - } - placement="bottom-start" - > - setParentIssueListModalOpen(true)}> - Change parent issue - - setValue("parent_id", null)}> - Remove parent issue - -
- ) : ( - - )} - - ( - setParentIssueListModalOpen(false)} - onChange={(issue) => { - onChange(issue.id); - setSelectedParentIssue(issue); - }} - projectId={projectId} - /> - )} - /> - - )} -
-
-
-
-
- {!status && ( -
setCreateMore((prevData) => !prevData)} - > -
- {}} size="sm" /> -
- Create more -
- )} -
- - -
-
-
- - ); -}); diff --git a/web/components/issues/index.ts b/web/components/issues/index.ts index 4a58e6547..b8af27d40 100644 --- a/web/components/issues/index.ts +++ b/web/components/issues/index.ts @@ -1,15 +1,14 @@ export * from "./attachment"; export * from "./comment"; +export * from "./issue-modal"; export * from "./sidebar-select"; export * from "./view-select"; export * from "./activity"; export * from "./delete-issue-modal"; export * from "./description-form"; -export * from "./form"; export * from "./issue-layouts"; export * from "./peek-overview"; export * from "./main-content"; -export * from "./modal"; export * from "./parent-issues-list-modal"; export * from "./sidebar"; export * from "./label"; diff --git a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx index 232fa6ebd..4d4776d38 100644 --- a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx @@ -2,9 +2,8 @@ import React, { FC } from "react"; import { useRouter } from "next/router"; // components import { CustomMenu } from "@plane/ui"; -import { CreateUpdateIssueModal } from "components/issues/modal"; -import { CreateUpdateDraftIssueModal } from "components/issues/draft-issue-modal"; import { ExistingIssuesListModal } from "components/core"; +import { CreateUpdateIssueModal, CreateUpdateDraftIssueModal } from "components/issues"; // lucide icons import { Minimize2, Maximize2, Circle, Plus } from "lucide-react"; // hooks @@ -85,12 +84,7 @@ export const HeaderGroupByCard: FC = observer((props) => { fieldsToShow={["all"]} /> ) : ( - setIsOpen(false)} - prePopulateData={issuePayload} - currentStore={currentStore} - /> + setIsOpen(false)} data={issuePayload} /> )} {renderExistingIssueModal && ( ) : ( - setIsOpen(false)} - currentStore={currentStore} - prePopulateData={issuePayload} - /> + setIsOpen(false)} data={issuePayload} /> )} {renderExistingIssueModal && ( diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx index 2e69ee129..efd9490d7 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx @@ -11,19 +11,17 @@ import { copyUrlToClipboard } from "helpers/string.helper"; // types import { TIssue } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; -import { EIssuesStoreType } from "constants/issue"; export const AllIssueQuickActions: React.FC = (props) => { const { issue, handleDelete, handleUpdate, customActionButton } = props; - - const router = useRouter(); - const { workspaceSlug } = router.query; - // states const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); - const [issueToEdit, setIssueToEdit] = useState(null); + const [issueToEdit, setIssueToEdit] = useState(undefined); const [deleteIssueModal, setDeleteIssueModal] = useState(false); - + // router + const router = useRouter(); + const { workspaceSlug } = router.query; + // toast alert const { setToastAlert } = useToast(); const handleCopyIssueLink = () => { @@ -36,6 +34,12 @@ export const AllIssueQuickActions: React.FC = (props) => { ); }; + const duplicateIssuePayload = { + ...issue, + name: `${issue.name} (copy)`, + }; + delete duplicateIssuePayload.id; + return ( <> = (props) => { /> { + onClose={() => { setCreateUpdateIssueModal(false); - setIssueToEdit(null); + setIssueToEdit(undefined); }} - // pre-populate date only if not editing - prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}} - data={issueToEdit} + data={issueToEdit ?? duplicateIssuePayload} onSubmit={async (data) => { - if (issueToEdit && handleUpdate) handleUpdate({ ...issueToEdit, ...data }); + if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); }} - currentStore={EIssuesStoreType.PROJECT} /> = (props) => { const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton } = props; - - const router = useRouter(); - const { workspaceSlug, cycleId } = router.query; - // states const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); - const [issueToEdit, setIssueToEdit] = useState(null); + const [issueToEdit, setIssueToEdit] = useState(undefined); const [deleteIssueModal, setDeleteIssueModal] = useState(false); - + // router + const router = useRouter(); + const { workspaceSlug, cycleId } = router.query; + // toast alert const { setToastAlert } = useToast(); const handleCopyIssueLink = () => { @@ -36,6 +34,12 @@ export const CycleIssueQuickActions: React.FC = (props) => { ); }; + const duplicateIssuePayload = { + ...issue, + name: `${issue.name} (copy)`, + }; + delete duplicateIssuePayload.id; + return ( <> = (props) => { /> { + onClose={() => { setCreateUpdateIssueModal(false); - setIssueToEdit(null); + setIssueToEdit(undefined); }} - // pre-populate date only if not editing - prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}} - data={issueToEdit} + data={issueToEdit ?? duplicateIssuePayload} onSubmit={async (data) => { - if (issueToEdit && handleUpdate) handleUpdate({ ...issueToEdit, ...data }); + if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); }} - currentStore={EIssuesStoreType.CYCLE} /> = (props) => { const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton } = props; - - const router = useRouter(); - const { workspaceSlug, moduleId } = router.query; - // states const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); - const [issueToEdit, setIssueToEdit] = useState(null); + const [issueToEdit, setIssueToEdit] = useState(undefined); const [deleteIssueModal, setDeleteIssueModal] = useState(false); - + // router + const router = useRouter(); + const { workspaceSlug, moduleId } = router.query; + // toast alert const { setToastAlert } = useToast(); const handleCopyIssueLink = () => { @@ -36,6 +34,12 @@ export const ModuleIssueQuickActions: React.FC = (props) => { ); }; + const duplicateIssuePayload = { + ...issue, + name: `${issue.name} (copy)`, + }; + delete duplicateIssuePayload.id; + return ( <> = (props) => { /> { + onClose={() => { setCreateUpdateIssueModal(false); - setIssueToEdit(null); + setIssueToEdit(undefined); }} - // pre-populate date only if not editing - prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}} - data={issueToEdit} + data={issueToEdit ?? duplicateIssuePayload} onSubmit={async (data) => { - if (issueToEdit && handleUpdate) handleUpdate({ ...issueToEdit, ...data }); + if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); }} - currentStore={EIssuesStoreType.MODULE} /> = (props) => { const { issue, handleDelete, handleUpdate, customActionButton } = props; @@ -23,7 +22,7 @@ export const ProjectIssueQuickActions: React.FC = (props) => const { workspaceSlug } = router.query; // states const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); - const [issueToEdit, setIssueToEdit] = useState(null); + const [issueToEdit, setIssueToEdit] = useState(undefined); const [deleteIssueModal, setDeleteIssueModal] = useState(false); // store hooks const { @@ -44,6 +43,12 @@ export const ProjectIssueQuickActions: React.FC = (props) => ); }; + const duplicateIssuePayload = { + ...issue, + name: `${issue.name} (copy)`, + }; + delete duplicateIssuePayload.id; + return ( <> = (props) => /> { + onClose={() => { setCreateUpdateIssueModal(false); - setIssueToEdit(null); + setIssueToEdit(undefined); }} - // pre-populate date only if not editing - prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}} - data={issueToEdit} + data={issueToEdit ?? duplicateIssuePayload} onSubmit={async (data) => { - if (issueToEdit && handleUpdate) handleUpdate({ ...issueToEdit, ...data }); + if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); }} - currentStore={EIssuesStoreType.PROJECT} /> | null; + data?: Partial; + onChange: (formData: Partial | null) => void; + onClose: (saveDraftIssueInLocalStorage?: boolean) => void; + onSubmit: (formData: Partial) => Promise; + projectId: string; +} + +const issueDraftService = new IssueDraftService(); + +export const DraftIssueLayout: React.FC = observer((props) => { + const { changesMade, data, onChange, onClose, onSubmit, projectId } = props; + // states + const [issueDiscardModal, setIssueDiscardModal] = useState(false); + // router + const router = useRouter(); + const { workspaceSlug } = router.query; + // toast alert + const { setToastAlert } = useToast(); + + const handleClose = () => { + if (changesMade) setIssueDiscardModal(true); + else onClose(false); + }; + + const handleCreateDraftIssue = async () => { + if (!changesMade || !workspaceSlug || !projectId) return; + + const payload = { ...changesMade }; + + await issueDraftService + .createDraftIssue(workspaceSlug.toString(), projectId.toString(), payload) + .then(() => { + setToastAlert({ + type: "success", + title: "Success!", + message: "Draft Issue created successfully.", + }); + + onChange(null); + setIssueDiscardModal(false); + onClose(false); + }) + .catch(() => + setToastAlert({ + type: "error", + title: "Error!", + message: "Issue could not be created. Please try again.", + }) + ); + }; + + return ( + <> + setIssueDiscardModal(false)} + onConfirm={handleCreateDraftIssue} + onDiscard={() => { + onChange(null); + setIssueDiscardModal(false); + onClose(false); + }} + /> + + + ); +}); diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx new file mode 100644 index 000000000..7f00f6216 --- /dev/null +++ b/web/components/issues/issue-modal/form.tsx @@ -0,0 +1,599 @@ +import React, { FC, useState, useRef } from "react"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +import { Controller, useForm } from "react-hook-form"; +import { LayoutPanelTop, Sparkle, X } from "lucide-react"; +// editor +import { RichTextEditorWithRef } from "@plane/rich-text-editor"; +// hooks +import { useApplication, useEstimate, useMention, useProject } from "hooks/store"; +import useToast from "hooks/use-toast"; +// services +import { AIService } from "services/ai.service"; +import { FileService } from "services/file.service"; +// components +import { GptAssistantPopover } from "components/core"; +import { ParentIssuesListModal } from "components/issues"; +import { IssueLabelSelect } from "components/issues/select"; +import { CreateLabelModal } from "components/labels"; +import { + CycleDropdown, + DateDropdown, + EstimateDropdown, + ModuleDropdown, + PriorityDropdown, + ProjectDropdown, + ProjectMemberDropdown, + StateDropdown, +} from "components/dropdowns"; +// ui +import { Button, CustomMenu, Input, ToggleSwitch } from "@plane/ui"; +// helpers +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +// types +import type { TIssue, ISearchIssueResponse } from "@plane/types"; + +const defaultValues: Partial = { + project_id: "", + name: "", + description_html: "", + estimate_point: null, + state_id: "", + parent_id: null, + priority: "none", + assignee_ids: [], + label_ids: [], + cycle_id: null, + module_id: null, + start_date: null, + target_date: null, +}; + +export interface IssueFormProps { + data?: Partial; + onChange?: (formData: Partial | null) => void; + onClose: () => void; + onSubmit: (values: Partial) => Promise; + projectId: string; +} + +// services +const aiService = new AIService(); +const fileService = new FileService(); + +export const IssueFormRoot: FC = observer((props) => { + const { data, onChange, onClose, onSubmit, projectId } = props; + // states + const [labelModal, setLabelModal] = useState(false); + const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false); + const [selectedParentIssue, setSelectedParentIssue] = useState(null); + const [gptAssistantModal, setGptAssistantModal] = useState(false); + const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); + const [createMore, setCreateMore] = useState(false); + // refs + const editorRef = useRef(null); + // router + const router = useRouter(); + const { workspaceSlug } = router.query; + // store hooks + const { + config: { envConfig }, + } = useApplication(); + const { getProjectById } = useProject(); + const { areEstimatesEnabledForProject } = useEstimate(); + const { mentionHighlights, mentionSuggestions } = useMention(); + // toast alert + const { setToastAlert } = useToast(); + // form info + const { + formState: { errors, isDirty, isSubmitting }, + handleSubmit, + reset, + watch, + control, + getValues, + setValue, + } = useForm({ + defaultValues: { ...defaultValues, project_id: projectId, ...data }, + reValidateMode: "onChange", + }); + + const issueName = watch("name"); + + const handleFormSubmit = async (formData: Partial) => { + await onSubmit(formData); + + setGptAssistantModal(false); + + reset({ + ...defaultValues, + project_id: getValues("project_id"), + }); + editorRef?.current?.clearEditor(); + }; + + const handleAiAssistance = async (response: string) => { + if (!workspaceSlug || !projectId) return; + + setValue("description_html", `${watch("description_html")}

${response}

`); + editorRef.current?.setEditorValue(`${watch("description_html")}`); + }; + + const handleAutoGenerateDescription = async () => { + if (!workspaceSlug || !projectId) return; + + setIAmFeelingLucky(true); + + aiService + .createGptTask(workspaceSlug.toString(), projectId.toString(), { + prompt: issueName, + task: "Generate a proper description for this issue.", + }) + .then((res) => { + if (res.response === "") + setToastAlert({ + type: "error", + title: "Error!", + message: + "Issue title isn't informative enough to generate the description. Please try with a different title.", + }); + else handleAiAssistance(res.response_html); + }) + .catch((err) => { + const error = err?.data?.error; + + if (err.status === 429) + setToastAlert({ + type: "error", + title: "Error!", + message: error || "You have reached the maximum number of requests of 50 requests per month per user.", + }); + else + setToastAlert({ + type: "error", + title: "Error!", + message: error || "Some error occurred. Please try again.", + }); + }) + .finally(() => setIAmFeelingLucky(false)); + }; + + const handleFormChange = () => { + if (!onChange) return; + + if (isDirty) onChange(watch()); + else onChange(null); + }; + + const startDate = watch("start_date"); + const targetDate = watch("target_date"); + + const minDate = startDate ? new Date(startDate) : null; + minDate?.setDate(minDate.getDate()); + + const maxDate = targetDate ? new Date(targetDate) : null; + maxDate?.setDate(maxDate.getDate()); + + const projectDetails = getProjectById(projectId); + + return ( + <> + {projectId && ( + setLabelModal(false)} + projectId={projectId} + onSuccess={(response) => { + setValue("label_ids", [...watch("label_ids"), response.id]); + handleFormChange(); + }} + /> + )} +
+
+
+ {/* Don't show project selection if editing an issue */} + {!data?.id && ( + ( +
+ { + onChange(projectId); + handleFormChange(); + }} + buttonVariant="border-with-text" + /> +
+ )} + /> + )} +

+ {data?.id ? "Update" : "Create"} issue +

+
+ {watch("parent_id") && selectedParentIssue && ( +
+
+ + + {selectedParentIssue.project__identifier}-{selectedParentIssue.sequence_id} + + {selectedParentIssue.name.substring(0, 50)} + { + setValue("parent_id", null); + handleFormChange(); + setSelectedParentIssue(null); + }} + /> +
+
+ )} +
+
+ ( + { + onChange(e.target.value); + handleFormChange(); + }} + ref={ref} + hasError={Boolean(errors.name)} + placeholder="Issue Title" + className="resize-none text-xl w-full" + /> + )} + /> +
+
+ {issueName && issueName.trim() !== "" && ( + + )} + {envConfig?.has_openai_configured && ( + { + setGptAssistantModal((prevData) => !prevData); + // this is done so that the title do not reset after gpt popover closed + reset(getValues()); + }} + onResponse={(response) => { + handleAiAssistance(response); + }} + placement="top-end" + button={ + + } + /> + )} +
+ ( + { + onChange(description_html); + handleFormChange(); + }} + mentionHighlights={mentionHighlights} + mentionSuggestions={mentionSuggestions} + /> + )} + /> +
+
+ ( +
+ { + onChange(stateId); + handleFormChange(); + }} + projectId={projectId} + buttonVariant="border-with-text" + /> +
+ )} + /> + ( +
+ { + onChange(priority); + handleFormChange(); + }} + buttonVariant="border-with-text" + /> +
+ )} + /> + ( +
+ { + onChange(assigneeIds); + handleFormChange(); + }} + buttonVariant={value?.length > 0 ? "transparent-without-text" : "border-with-text"} + buttonClassName={value?.length > 0 ? "hover:bg-transparent px-0" : ""} + placeholder="Assignees" + multiple + /> +
+ )} + /> + ( +
+ { + onChange(labelIds); + handleFormChange(); + }} + projectId={projectId} + /> +
+ )} + /> + ( +
+ { + onChange(date ? renderFormattedPayloadDate(date) : null); + handleFormChange(); + }} + buttonVariant="border-with-text" + placeholder="Start date" + maxDate={maxDate ?? undefined} + /> +
+ )} + /> + ( +
+ { + onChange(date ? renderFormattedPayloadDate(date) : null); + handleFormChange(); + }} + buttonVariant="border-with-text" + placeholder="Due date" + minDate={minDate ?? undefined} + /> +
+ )} + /> + {projectDetails?.cycle_view && ( + ( +
+ { + onChange(cycleId); + handleFormChange(); + }} + value={value} + buttonVariant="border-with-text" + /> +
+ )} + /> + )} + {projectDetails?.module_view && ( + ( +
+ { + onChange(moduleId); + handleFormChange(); + }} + buttonVariant="border-with-text" + /> +
+ )} + /> + )} + {areEstimatesEnabledForProject(projectId) && ( + ( +
+ { + onChange(estimatePoint); + handleFormChange(); + }} + projectId={projectId} + buttonVariant="border-with-text" + /> +
+ )} + /> + )} + + {watch("parent_id") ? ( +
+ + + {selectedParentIssue && + `${selectedParentIssue.project__identifier}- + ${selectedParentIssue.sequence_id}`} + +
+ ) : ( +
+ + Add parent +
+ )} + + } + placement="bottom-start" + > + {watch("parent_id") ? ( + <> + setParentIssueListModalOpen(true)}> + Change parent issue + + { + setValue("parent_id", null); + handleFormChange(); + }} + > + Remove parent issue + + + ) : ( + setParentIssueListModalOpen(true)}> + Select parent Issue + + )} +
+ ( + setParentIssueListModalOpen(false)} + onChange={(issue) => { + onChange(issue.id); + handleFormChange(); + setSelectedParentIssue(issue); + }} + projectId={projectId} + /> + )} + /> +
+
+
+
+
+
setCreateMore((prevData) => !prevData)} + > +
+ {}} size="sm" /> +
+ Create more +
+
+ + +
+
+
+ + ); +}); diff --git a/web/components/issues/issue-modal/index.ts b/web/components/issues/issue-modal/index.ts new file mode 100644 index 000000000..feac885d4 --- /dev/null +++ b/web/components/issues/issue-modal/index.ts @@ -0,0 +1,3 @@ +export * from "./draft-issue-layout"; +export * from "./form"; +export * from "./modal"; diff --git a/web/components/issues/issue-modal/modal.tsx b/web/components/issues/issue-modal/modal.tsx new file mode 100644 index 000000000..975d4f09e --- /dev/null +++ b/web/components/issues/issue-modal/modal.tsx @@ -0,0 +1,188 @@ +import React, { useState } from "react"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +import { Dialog, Transition } from "@headlessui/react"; +// hooks +import { useIssues, useProject } from "hooks/store"; +import useToast from "hooks/use-toast"; +import useLocalStorage from "hooks/use-local-storage"; +// components +import { DraftIssueLayout } from "./draft-issue-layout"; +import { IssueFormRoot } from "./form"; +// types +import type { TIssue } from "@plane/types"; +// constants +import { EIssuesStoreType } from "constants/issue"; + +export interface IssuesModalProps { + data?: Partial; + isOpen: boolean; + onClose: () => void; + onSubmit?: (res: Partial) => Promise; + withDraftIssueWrapper?: boolean; +} + +export const CreateUpdateIssueModal: React.FC = observer((props) => { + const { data, isOpen, onClose, onSubmit, withDraftIssueWrapper = true } = props; + // states + const [changesMade, setChangesMade] = useState | null>(null); + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + // store hooks + const { workspaceProjectIds } = useProject(); + const { + issues: { createIssue, updateIssue }, + } = useIssues(EIssuesStoreType.PROJECT); + const { + issues: { addIssueToCycle }, + } = useIssues(EIssuesStoreType.CYCLE); + const { + issues: { addIssueToModule }, + } = useIssues(EIssuesStoreType.MODULE); + // toast alert + const { setToastAlert } = useToast(); + // local storage + const { setValue: setLocalStorageDraftIssue } = useLocalStorage("draftedIssue", {}); + + const handleClose = (saveDraftIssueInLocalStorage?: boolean) => { + if (changesMade && saveDraftIssueInLocalStorage) { + const draftIssue = JSON.stringify(changesMade); + + setLocalStorageDraftIssue(draftIssue); + } + + onClose(); + }; + + const handleCreateIssue = async (payload: Partial): Promise => { + if (!workspaceSlug || !payload.project_id) return null; + + await createIssue(workspaceSlug.toString(), payload.project_id, payload) + .then(async (res) => { + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue created successfully.", + }); + handleClose(); + return res; + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Issue could not be created. Please try again.", + }); + }); + + return null; + }; + + const handleUpdateIssue = async (payload: Partial): Promise => { + if (!workspaceSlug || !payload.project_id || !data?.id) return null; + + await updateIssue(workspaceSlug.toString(), payload.project_id, data.id, payload) + .then((res) => { + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue updated successfully.", + }); + handleClose(); + return { ...payload, ...res }; + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Issue could not be updated. Please try again.", + }); + }); + + return null; + }; + + const handleFormSubmit = async (formData: Partial) => { + if (!workspaceSlug || !formData.project_id) return; + + const payload: Partial = { + ...formData, + description_html: formData.description_html ?? "

", + }; + + let res: TIssue | null = null; + if (!data?.id) res = await handleCreateIssue(payload); + else res = await handleUpdateIssue(payload); + + // add issue to cycle if cycle is selected, and cycle is different from current cycle + if (formData.cycle_id && res && (!data?.id || formData.cycle_id !== data?.cycle_id)) + await addIssueToCycle(workspaceSlug.toString(), formData.project_id, formData.cycle_id, [res.id]); + + // add issue to module if module is selected, and module is different from current module + if (formData.module_id && res && (!data?.id || formData.module_id !== data?.module_id)) + await addIssueToModule(workspaceSlug.toString(), formData.project_id, formData.module_id, [res.id]); + + if (res && onSubmit) await onSubmit(res); + }; + + const handleFormChange = (formData: Partial | null) => setChangesMade(formData); + + // don't open the modal if there are no projects + if (!workspaceProjectIds || workspaceProjectIds.length === 0) return null; + + // if project id is present in the router query, use that as the selected project id, otherwise use the first project id + const selectedProjectId = projectId ? projectId.toString() : workspaceProjectIds[0]; + + return ( + + handleClose(true)}> + +
+ + +
+
+ + + {withDraftIssueWrapper ? ( + + ) : ( + handleClose(false)} + onSubmit={handleFormSubmit} + projectId={selectedProjectId} + /> + )} + + +
+
+
+
+ ); +}); diff --git a/web/components/issues/modal.tsx b/web/components/issues/modal.tsx deleted file mode 100644 index 402e94651..000000000 --- a/web/components/issues/modal.tsx +++ /dev/null @@ -1,448 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; -import { mutate } from "swr"; -import { Dialog, Transition } from "@headlessui/react"; -// hooks -import { useApplication, useCycle, useIssues, useModule, useProject, useUser, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; -import useLocalStorage from "hooks/use-local-storage"; -// services -import { IssueDraftService } from "services/issue"; -// components -import { IssueForm, ConfirmIssueDiscard } from "components/issues"; -// types -import type { TIssue } from "@plane/types"; -// fetch-keys -import { USER_ISSUE, SUB_ISSUES } from "constants/fetch-keys"; -import { EIssuesStoreType, TCreateModalStoreTypes } from "constants/issue"; - -export interface IssuesModalProps { - data?: TIssue | null; - handleClose: () => void; - isOpen: boolean; - prePopulateData?: Partial; - fieldsToShow?: ( - | "project" - | "name" - | "description" - | "state" - | "priority" - | "assignee" - | "label" - | "startDate" - | "dueDate" - | "estimate" - | "parent" - | "all" - | "module" - | "cycle" - )[]; - onSubmit?: (data: Partial) => Promise; - handleSubmit?: (data: Partial) => Promise; - currentStore?: TCreateModalStoreTypes; -} - -const issueDraftService = new IssueDraftService(); - -export const CreateUpdateIssueModal: React.FC = observer((props) => { - const { - data, - handleClose, - isOpen, - prePopulateData: prePopulateDataProps, - fieldsToShow = ["all"], - onSubmit, - handleSubmit, - currentStore = EIssuesStoreType.PROJECT, - } = props; - // states - const [createMore, setCreateMore] = useState(false); - const [formDirtyState, setFormDirtyState] = useState(null); - const [showConfirmDiscard, setShowConfirmDiscard] = useState(false); - const [activeProject, setActiveProject] = useState(null); - const [prePopulateData, setPreloadedData] = useState>({}); - // router - const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId } = router.query as { - workspaceSlug: string; - projectId: string | undefined; - cycleId: string | undefined; - moduleId: string | undefined; - }; - // store hooks - - const { issues: projectIssues } = useIssues(EIssuesStoreType.PROJECT); - const { issues: moduleIssues } = useIssues(EIssuesStoreType.MODULE); - const { issues: cycleIssues } = useIssues(EIssuesStoreType.CYCLE); - const { issues: viewIssues } = useIssues(EIssuesStoreType.PROJECT_VIEW); - const { issues: profileIssues } = useIssues(EIssuesStoreType.PROFILE); - - const { - eventTracker: { postHogEventTracker }, - } = useApplication(); - const { currentUser } = useUser(); - const { currentWorkspace } = useWorkspace(); - const { workspaceProjectIds } = useProject(); - const { fetchCycleDetails } = useCycle(); - const { fetchModuleDetails } = useModule(); - - const issueStores = { - [EIssuesStoreType.PROJECT]: { - store: projectIssues, - dataIdToUpdate: activeProject, - viewId: undefined, - }, - [EIssuesStoreType.PROJECT_VIEW]: { - store: viewIssues, - dataIdToUpdate: activeProject, - viewId: undefined, - }, - [EIssuesStoreType.PROFILE]: { - store: profileIssues, - dataIdToUpdate: currentUser?.id || undefined, - viewId: undefined, - }, - [EIssuesStoreType.CYCLE]: { - store: cycleIssues, - dataIdToUpdate: activeProject, - viewId: cycleId, - }, - [EIssuesStoreType.MODULE]: { - store: moduleIssues, - dataIdToUpdate: activeProject, - viewId: moduleId, - }, - }; - - const { store: currentIssueStore, viewId, dataIdToUpdate } = issueStores[currentStore]; - - const { setValue: setValueInLocalStorage, clearValue: clearLocalStorageValue } = useLocalStorage( - "draftedIssue", - {} - ); - - const { setToastAlert } = useToast(); - - useEffect(() => { - setPreloadedData(prePopulateDataProps ?? {}); - - if (cycleId && !prePopulateDataProps?.cycle_id) { - setPreloadedData((prevData) => ({ - ...(prevData ?? {}), - ...prePopulateDataProps, - cycle_id: cycleId.toString(), - })); - } - if (moduleId && !prePopulateDataProps?.module_id) { - setPreloadedData((prevData) => ({ - ...(prevData ?? {}), - ...prePopulateDataProps, - module_id: moduleId.toString(), - })); - } - if ( - (router.asPath.includes("my-issues") || router.asPath.includes("assigned")) && - !prePopulateDataProps?.assignee_ids - ) { - setPreloadedData((prevData) => ({ - ...(prevData ?? {}), - ...prePopulateDataProps, - assignees: prePopulateDataProps?.assignee_ids ?? [currentUser?.id ?? ""], - })); - } - }, [prePopulateDataProps, cycleId, moduleId, router.asPath, currentUser?.id]); - - /** - * - * @description This function is used to close the modals. This function will show a confirm discard modal if the form is dirty. - * @returns void - */ - - const onClose = () => { - if (!showConfirmDiscard) handleClose(); - if (formDirtyState === null) return setActiveProject(null); - const data = JSON.stringify(formDirtyState); - setValueInLocalStorage(data); - }; - - /** - * @description This function is used to close the modals. This function is to be used when the form is submitted, - * meaning we don't need to show the confirm discard modal or store the form data in local storage. - */ - - const onFormSubmitClose = () => { - setFormDirtyState(null); - handleClose(); - }; - - /** - * @description This function is used to close the modals. This function is to be used when we click outside the modal, - * meaning we don't need to show the confirm discard modal but will store the form data in local storage. - * Use this function when you want to store the form data in local storage. - */ - - const onDiscardClose = () => { - if (formDirtyState !== null && formDirtyState.name.trim() !== "") { - setShowConfirmDiscard(true); - } else { - handleClose(); - setActiveProject(null); - } - }; - - const handleFormDirty = (data: any) => { - setFormDirtyState(data); - }; - - useEffect(() => { - // if modal is closed, reset active project to null - // and return to avoid activeProject being set to some other project - if (!isOpen) { - setActiveProject(null); - return; - } - - // if data is present, set active project to the project of the - // issue. This has more priority than the project in the url. - if (data && data.project_id) { - setActiveProject(data.project_id); - return; - } - - // if data is not present, set active project to the project - // in the url. This has the least priority. - if (workspaceProjectIds && workspaceProjectIds.length > 0 && !activeProject) - setActiveProject(projectId ?? workspaceProjectIds?.[0] ?? null); - }, [data, projectId, workspaceProjectIds, isOpen, activeProject]); - - const addIssueToCycle = async (issue: TIssue, cycleId: string) => { - if (!workspaceSlug || !activeProject) return; - - await cycleIssues.addIssueToCycle(workspaceSlug, issue.project_id, cycleId, [issue.id]); - fetchCycleDetails(workspaceSlug, activeProject, cycleId); - }; - - const addIssueToModule = async (issue: TIssue, moduleId: string) => { - if (!workspaceSlug || !activeProject) return; - - await moduleIssues.addIssueToModule(workspaceSlug, activeProject, moduleId, [issue.id]); - fetchModuleDetails(workspaceSlug, activeProject, moduleId); - }; - - const createIssue = async (payload: Partial) => { - if (!workspaceSlug || !dataIdToUpdate) return; - - await currentIssueStore - .createIssue(workspaceSlug, dataIdToUpdate, payload, viewId) - .then(async (res) => { - if (!res) throw new Error(); - - if (handleSubmit) { - await handleSubmit(res); - } else { - currentIssueStore.fetchIssues(workspaceSlug, dataIdToUpdate, "mutation", viewId); - - if (payload.cycle_id && payload.cycle_id !== "") await addIssueToCycle(res, payload.cycle_id); - if (payload.module_id && payload.module_id !== "") await addIssueToModule(res, payload.module_id); - - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - postHogEventTracker( - "ISSUE_CREATED", - { - ...res, - state: "SUCCESS", - }, - { - isGrouping: true, - groupType: "Workspace_metrics", - groupId: currentWorkspace?.id!, - } - ); - if (payload.parent_id && payload.parent_id !== "") mutate(SUB_ISSUES(payload.parent_id)); - } - }) - .catch((err) => { - setToastAlert({ - type: "error", - title: "Error!", - message: err.detail ?? "Issue could not be created. Please try again.", - }); - postHogEventTracker( - "ISSUE_CREATED", - { - state: "FAILED", - }, - { - isGrouping: true, - groupType: "Workspace_metrics", - groupId: currentWorkspace?.id!, - } - ); - }); - - if (!createMore) onFormSubmitClose(); - }; - - const createDraftIssue = async () => { - if (!workspaceSlug || !activeProject || !currentUser) return; - - const payload: Partial = { - ...formDirtyState, - }; - - await issueDraftService - .createDraftIssue(workspaceSlug as string, activeProject ?? "", payload) - .then(() => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Draft Issue created successfully.", - }); - handleClose(); - setActiveProject(null); - setFormDirtyState(null); - setShowConfirmDiscard(false); - - if (payload.assignee_ids?.some((assignee) => assignee === currentUser?.id)) - mutate(USER_ISSUE(workspaceSlug as string)); - - if (payload.parent_id && payload.parent_id !== "") mutate(SUB_ISSUES(payload.parent_id)); - }) - .catch((err) => { - setToastAlert({ - type: "error", - title: "Error!", - message: err.detail ?? "Issue could not be created. Please try again.", - }); - }); - }; - - const updateIssue = async (payload: Partial) => { - if (!workspaceSlug || !dataIdToUpdate || !data) return; - - await currentIssueStore - .updateIssue(workspaceSlug, dataIdToUpdate, data.id, payload, viewId) - .then((res) => { - if (!createMore) onFormSubmitClose(); - - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue updated successfully.", - }); - postHogEventTracker( - "ISSUE_UPDATED", - { - ...res, - state: "SUCCESS", - }, - { - isGrouping: true, - groupType: "Workspace_metrics", - groupId: currentWorkspace?.id!, - } - ); - }) - .catch((err) => { - setToastAlert({ - type: "error", - title: "Error!", - message: err.detail ?? "Issue could not be updated. Please try again.", - }); - postHogEventTracker( - "ISSUE_UPDATED", - { - state: "FAILED", - }, - { - isGrouping: true, - groupType: "Workspace_metrics", - groupId: currentWorkspace?.id!, - } - ); - }); - }; - - const handleFormSubmit = async (formData: Partial) => { - if (!workspaceSlug || !dataIdToUpdate || !currentStore) return; - - const payload: Partial = { - ...formData, - description_html: formData.description_html ?? "

", - }; - - if (!data) await createIssue(payload); - else await updateIssue(payload); - - if (onSubmit) await onSubmit(payload); - }; - - if (!workspaceProjectIds || workspaceProjectIds.length === 0) return null; - - return ( - <> - setShowConfirmDiscard(false)} - onConfirm={createDraftIssue} - onDiscard={() => { - handleClose(); - setActiveProject(null); - setFormDirtyState(null); - setShowConfirmDiscard(false); - clearLocalStorageValue(); - }} - /> - - - - -
- - -
-
- - - - - -
-
-
-
- - ); -}); diff --git a/web/components/issues/sub-issues/root.tsx b/web/components/issues/sub-issues/root.tsx index 25d85be15..025e4741f 100644 --- a/web/components/issues/sub-issues/root.tsx +++ b/web/components/issues/sub-issues/root.tsx @@ -320,10 +320,10 @@ export const SubIssuesRoot: React.FC = observer((props) => { {isEditable && issueCrudOperation?.create?.toggle && ( { + onClose={() => { mutateSubIssues(issueCrudOperation?.create?.issueId); handleIssueCrudOperation("create", null); }} @@ -342,11 +342,11 @@ export const SubIssuesRoot: React.FC = observer((props) => { <> { + onClose={() => { mutateSubIssues(issueCrudOperation?.edit?.issueId); handleIssueCrudOperation("edit", null, null); }} - data={issueCrudOperation?.edit?.issue} + data={issueCrudOperation?.edit?.issue ?? undefined} /> )} diff --git a/web/store/estimate.store.ts b/web/store/estimate.store.ts index af938c52e..19a05b544 100644 --- a/web/store/estimate.store.ts +++ b/web/store/estimate.store.ts @@ -14,7 +14,7 @@ export interface IEstimateStore { projectEstimates: IEstimate[] | null; activeEstimateDetails: IEstimate | null; // computed actions - areEstimatesActiveForProject: (projectId: string) => boolean; + areEstimatesEnabledForProject: (projectId: string) => boolean; getEstimatePointValue: (estimateKey: number | null) => string; getProjectEstimateById: (estimateId: string) => IEstimate | null; getProjectActiveEstimateDetails: (projectId: string) => IEstimate | null; @@ -48,7 +48,7 @@ export class EstimateStore implements IEstimateStore { projectEstimates: computed, activeEstimateDetails: computed, // computed actions - areEstimatesActiveForProject: action, + areEstimatesEnabledForProject: action, getProjectEstimateById: action, getEstimatePointValue: action, getProjectActiveEstimateDetails: action, @@ -96,7 +96,7 @@ export class EstimateStore implements IEstimateStore { * @description returns true if estimates are enabled for a project using project id * @param projectId */ - areEstimatesActiveForProject = (projectId: string) => { + areEstimatesEnabledForProject = (projectId: string) => { const projectDetails = this.rootStore.projectRoot.project.getProjectById(projectId); if (!projectDetails) return false; return Boolean(projectDetails.estimate) ?? false;