diff --git a/web/components/core/views/issues-view.tsx b/web/components/core/views/issues-view.tsx index 55eefc39696..9a2d482faa2 100644 --- a/web/components/core/views/issues-view.tsx +++ b/web/components/core/views/issues-view.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useRouter } from "next/router"; @@ -87,8 +87,16 @@ export const IssuesView: React.FC = ({ const { setToastAlert } = useToast(); - const { groupedByIssues, mutateIssues, displayFilters, filters, isEmpty, setFilters, params } = - useIssuesView(); + const { + groupedByIssues, + mutateIssues, + displayFilters, + filters, + isEmpty, + setFilters, + params, + setDisplayFilters, + } = useIssuesView(); const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); const { data: stateGroups } = useSWR( @@ -108,6 +116,17 @@ export const IssuesView: React.FC = ({ const { members } = useProjectMembers(workspaceSlug?.toString(), projectId?.toString()); + useEffect(() => { + if (!isDraftIssues) return; + + if ( + displayFilters.layout === "calendar" || + displayFilters.layout === "gantt_chart" || + displayFilters.layout === "spreadsheet" + ) + setDisplayFilters({ layout: "list" }); + }, [isDraftIssues, displayFilters, setDisplayFilters]); + const handleDeleteIssue = useCallback( (issue: IIssue) => { setDeleteIssueModal(true); diff --git a/web/components/issues/confirm-issue-discard.tsx b/web/components/issues/confirm-issue-discard.tsx index 1294913cc4f..f8feab73d65 100644 --- a/web/components/issues/confirm-issue-discard.tsx +++ b/web/components/issues/confirm-issue-discard.tsx @@ -57,7 +57,7 @@ export const ConfirmIssueDiscard: React.FC = (props) => {
-
+
= { }; interface IssueFormProps { - handleFormSubmit: (formData: Partial) => Promise; + handleFormSubmit: ( + formData: Partial, + action?: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" + ) => Promise; data?: Partial | null; prePopulatedData?: Partial | null; projectId: string; @@ -134,12 +137,16 @@ export const DraftIssueForm: FC = (props) => { const handleCreateUpdateIssue = async ( formData: Partial, - action: "saveDraft" | "createToNewIssue" = "saveDraft" + action: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" = "createDraft" ) => { - await handleFormSubmit({ - ...formData, - is_draft: action === "saveDraft", - }); + await handleFormSubmit( + { + ...(data ?? {}), + ...formData, + is_draft: action === "createDraft" || action === "updateDraft", + }, + action + ); setGptAssistantModal(false); @@ -263,7 +270,9 @@ export const DraftIssueForm: FC = (props) => { )}
handleCreateUpdateIssue(formData, "createToNewIssue"))} + onSubmit={handleSubmit((formData) => + handleCreateUpdateIssue(formData, "convertToNewIssue") + )} >
@@ -563,15 +572,20 @@ export const DraftIssueForm: FC = (props) => { Discard handleCreateUpdateIssue(formData, "saveDraft"))} + onClick={handleSubmit((formData) => + handleCreateUpdateIssue(formData, data?.id ? "updateDraft" : "createDraft") + )} > {isSubmitting ? "Saving..." : "Save Draft"} - {data && ( - - {isSubmitting ? "Saving..." : "Add Issue"} - - )} + + handleCreateUpdateIssue(formData, data ? "convertToNewIssue" : "createNewIssue") + )} + > + {isSubmitting ? "Saving..." : "Add Issue"} +
diff --git a/web/components/issues/draft-issue-modal.tsx b/web/components/issues/draft-issue-modal.tsx index 3b0664cb804..c060c72f6c3 100644 --- a/web/components/issues/draft-issue-modal.tsx +++ b/web/components/issues/draft-issue-modal.tsx @@ -31,7 +31,10 @@ import { MODULE_ISSUES_WITH_PARAMS, VIEW_ISSUES, PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS, + CYCLE_DETAILS, + MODULE_DETAILS, } from "constants/fetch-keys"; +import modulesService from "services/modules.service"; interface IssuesModalProps { data?: IIssue | null; @@ -56,18 +59,21 @@ interface IssuesModalProps { onSubmit?: (data: Partial) => Promise | void; } -export const CreateUpdateDraftIssueModal: React.FC = ({ - data, - handleClose, - isOpen, - isUpdatingSingleIssue = false, - prePopulateData, - fieldsToShow = ["all"], - onSubmit, -}) => { +export const CreateUpdateDraftIssueModal: React.FC = (props) => { + const { + data, + handleClose, + isOpen, + isUpdatingSingleIssue = false, + prePopulateData: prePopulateDataProps, + fieldsToShow = ["all"], + onSubmit, + } = props; + // states const [createMore, setCreateMore] = useState(false); const [activeProject, setActiveProject] = useState(null); + const [prePopulateData, setPreloadedData] = useState | undefined>(undefined); const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; @@ -86,19 +92,40 @@ export const CreateUpdateDraftIssueModal: React.FC = ({ const { setToastAlert } = useToast(); - if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string }; - if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId as string }; - if (router.asPath.includes("my-issues") || router.asPath.includes("assigned")) - prePopulateData = { - ...prePopulateData, - assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""], - }; - const onClose = () => { handleClose(); setActiveProject(null); }; + useEffect(() => { + setPreloadedData(prePopulateDataProps ?? {}); + + if (cycleId && !prePopulateDataProps?.cycle) { + setPreloadedData((prevData) => ({ + ...(prevData ?? {}), + ...prePopulateDataProps, + cycle: cycleId.toString(), + })); + } + if (moduleId && !prePopulateDataProps?.module) { + setPreloadedData((prevData) => ({ + ...(prevData ?? {}), + ...prePopulateDataProps, + module: moduleId.toString(), + })); + } + if ( + (router.asPath.includes("my-issues") || router.asPath.includes("assigned")) && + !prePopulateDataProps?.assignees + ) { + setPreloadedData((prevData) => ({ + ...(prevData ?? {}), + ...prePopulateDataProps, + assignees: prePopulateDataProps?.assignees ?? [user?.id ?? ""], + })); + } + }, [prePopulateDataProps, cycleId, moduleId, router.asPath, user?.id]); + useEffect(() => { // if modal is closed, reset active project to null // and return to avoid activeProject being set to some other project @@ -109,10 +136,10 @@ export const CreateUpdateDraftIssueModal: React.FC = ({ // 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) { - setActiveProject(data.project); - return; - } + if (data && data.project) return setActiveProject(data.project); + + if (prePopulateData && prePopulateData.project && !activeProject) + return setActiveProject(prePopulateData.project); if (prePopulateData && prePopulateData.project) return setActiveProject(prePopulateData.project); @@ -147,7 +174,7 @@ export const CreateUpdateDraftIssueModal: React.FC = ({ ? VIEW_ISSUES(viewId.toString(), viewGanttParams) : PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? ""); - const createIssue = async (payload: Partial) => { + const createDraftIssue = async (payload: Partial) => { if (!workspaceSlug || !activeProject || !user) return; await issuesService @@ -187,7 +214,7 @@ export const CreateUpdateDraftIssueModal: React.FC = ({ if (!createMore) onClose(); }; - const updateIssue = async (payload: Partial) => { + const updateDraftIssue = async (payload: Partial) => { if (!user) return; await issuesService @@ -203,6 +230,11 @@ export const CreateUpdateDraftIssueModal: React.FC = ({ mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); } + if (!payload.is_draft) { + if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle); + if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module); + } + if (!createMore) onClose(); setToastAlert({ @@ -220,7 +252,93 @@ export const CreateUpdateDraftIssueModal: React.FC = ({ }); }; - const handleFormSubmit = async (formData: Partial) => { + const addIssueToCycle = async (issueId: string, cycleId: string) => { + if (!workspaceSlug || !activeProject) return; + + await issuesService + .addIssueToCycle( + workspaceSlug as string, + activeProject ?? "", + cycleId, + { + issues: [issueId], + }, + user + ) + .then(() => { + if (cycleId) { + mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId, params)); + mutate(CYCLE_DETAILS(cycleId as string)); + } + }); + }; + + const addIssueToModule = async (issueId: string, moduleId: string) => { + if (!workspaceSlug || !activeProject) return; + + await modulesService + .addIssuesToModule( + workspaceSlug as string, + activeProject ?? "", + moduleId as string, + { + issues: [issueId], + }, + user + ) + .then(() => { + if (moduleId) { + mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params)); + mutate(MODULE_DETAILS(moduleId as string)); + } + }); + }; + + const createIssue = async (payload: Partial) => { + if (!workspaceSlug || !activeProject) return; + + await issuesService + .createIssues(workspaceSlug as string, activeProject ?? "", payload, user) + .then(async (res) => { + mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); + if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res.id, payload.cycle); + if (payload.module && payload.module !== "") await addIssueToModule(res.id, payload.module); + + if (displayFilters.layout === "calendar") mutate(calendarFetchKey); + if (displayFilters.layout === "gantt_chart") + mutate(ganttFetchKey, { + start_target_date: true, + order_by: "sort_order", + }); + if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey); + if (groupedIssues) mutateMyIssues(); + + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue created successfully.", + }); + + if (!createMore) onClose(); + + if (payload.assignees_list?.some((assignee) => assignee === user?.id)) + mutate(USER_ISSUE(workspaceSlug as string)); + + if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent)); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Issue could not be created. Please try again.", + }); + }); + }; + + const handleFormSubmit = async ( + formData: Partial, + action: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" = "createDraft" + ) => { if (!workspaceSlug || !activeProject) return; const payload: Partial = { @@ -231,8 +349,10 @@ export const CreateUpdateDraftIssueModal: React.FC = ({ description_html: formData.description_html ?? "

", }; - if (!data) await createIssue(payload); - else await updateIssue(payload); + if (action === "createDraft") await createDraftIssue(payload); + else if (action === "updateDraft" || action === "convertToNewIssue") + await updateDraftIssue(payload); + else if (action === "createNewIssue") await createIssue(payload); clearDraftIssueLocalStorage(); diff --git a/web/components/issues/form.tsx b/web/components/issues/form.tsx index 2e53f38666e..c92c3d332ed 100644 --- a/web/components/issues/form.tsx +++ b/web/components/issues/form.tsx @@ -139,6 +139,8 @@ export const IssueForm: FC = (props) => { target_date: getValues("target_date"), project: getValues("project"), parent: getValues("parent"), + cycle: getValues("cycle"), + module: getValues("module"), }; useEffect(() => { diff --git a/web/components/issues/modal.tsx b/web/components/issues/modal.tsx index 65580c94ae1..608cf4fd1eb 100644 --- a/web/components/issues/modal.tsx +++ b/web/components/issues/modal.tsx @@ -69,7 +69,7 @@ export const CreateUpdateIssueModal: React.FC = ({ handleClose, isOpen, isUpdatingSingleIssue = false, - prePopulateData, + prePopulateData: prePopulateDataProps, fieldsToShow = ["all"], onSubmit, }) => { @@ -78,6 +78,7 @@ export const CreateUpdateIssueModal: React.FC = ({ const [formDirtyState, setFormDirtyState] = useState(null); const [showConfirmDiscard, setShowConfirmDiscard] = useState(false); const [activeProject, setActiveProject] = useState(null); + const [prePopulateData, setPreloadedData] = useState>({}); const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId, viewId, inboxId } = router.query; @@ -98,11 +99,40 @@ export const CreateUpdateIssueModal: React.FC = ({ const { setToastAlert } = useToast(); - if (router.asPath.includes("my-issues") || router.asPath.includes("assigned")) - prePopulateData = { - ...prePopulateData, - assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""], - }; + useEffect(() => { + setPreloadedData(prePopulateDataProps ?? {}); + + if (cycleId && !prePopulateDataProps?.cycle) { + setPreloadedData((prevData) => ({ + ...(prevData ?? {}), + ...prePopulateDataProps, + cycle: cycleId.toString(), + })); + } + if (moduleId && !prePopulateDataProps?.module) { + setPreloadedData((prevData) => ({ + ...(prevData ?? {}), + ...prePopulateDataProps, + module: moduleId.toString(), + })); + } + if ( + (router.asPath.includes("my-issues") || router.asPath.includes("assigned")) && + !prePopulateDataProps?.assignees + ) { + setPreloadedData((prevData) => ({ + ...(prevData ?? {}), + ...prePopulateDataProps, + assignees: prePopulateDataProps?.assignees ?? [user?.id ?? ""], + })); + } + }, [prePopulateDataProps, cycleId, moduleId, router.asPath, user?.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(); @@ -111,6 +141,22 @@ export const CreateUpdateIssueModal: React.FC = ({ 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) { setShowConfirmDiscard(true); @@ -295,7 +341,7 @@ export const CreateUpdateIssueModal: React.FC = ({ }); }); - if (!createMore) onDiscardClose(); + if (!createMore) onFormSubmitClose(); }; const createDraftIssue = async () => { @@ -354,7 +400,7 @@ export const CreateUpdateIssueModal: React.FC = ({ if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle); if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module); - if (!createMore) onDiscardClose(); + if (!createMore) onFormSubmitClose(); setToastAlert({ type: "success", diff --git a/web/components/workspace/sidebar-quick-action.tsx b/web/components/workspace/sidebar-quick-action.tsx index 4c7dda3b99b..8923abc141e 100644 --- a/web/components/workspace/sidebar-quick-action.tsx +++ b/web/components/workspace/sidebar-quick-action.tsx @@ -3,8 +3,6 @@ import React, { useState } from "react"; // ui import { Icon } from "components/ui"; import { ChevronDown, PenSquare } from "lucide-react"; -// headless ui -import { Menu, Transition } from "@headlessui/react"; // hooks import useLocalStorage from "hooks/use-local-storage"; // components @@ -17,10 +15,7 @@ export const WorkspaceSidebarQuickAction = () => { const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false); - const { storedValue, clearValue } = useLocalStorage( - "draftedIssue", - JSON.stringify(undefined) - ); + const { storedValue, clearValue } = useLocalStorage("draftedIssue", JSON.stringify({})); return ( <> @@ -31,18 +26,17 @@ export const WorkspaceSidebarQuickAction = () => { onSubmit={() => { localStorage.removeItem("draftedIssue"); clearValue(); - setIsDraftIssueModalOpen(false); }} fieldsToShow={["all"]} />
{ > - {storedValue &&
} + {storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && ( + <> +
+ + - {storedValue && ( -
- - {({ open }) => ( - <> -
- - - -
- - -
- - - -
-
-
- - )} -
-
+
+
+ +
+
+ )}