From a872b5a7a995188ce49f81d17c4a9d2ac75a19ba Mon Sep 17 00:00:00 2001 From: dakshesh14 Date: Fri, 15 Sep 2023 22:12:37 +0530 Subject: [PATCH 1/4] fix: hydration error and draft issue workflow --- .../command-palette/command-pallette.tsx | 9 +- web/components/core/views/issues-view.tsx | 11 +- web/components/issues/draft-issue-modal.tsx | 10 +- web/components/issues/form.tsx | 11 +- web/components/issues/modal.tsx | 21 +- web/components/ui/buttons/type.d.ts | 2 +- .../workspace/sidebar-quick-action.tsx | 20 +- web/services/issues.service.ts | 7 +- web/store/draft-issue.ts | 189 ++++++++++++++++++ web/store/root.ts | 3 + 10 files changed, 238 insertions(+), 45 deletions(-) create mode 100644 web/store/draft-issue.ts diff --git a/web/components/command-palette/command-pallette.tsx b/web/components/command-palette/command-pallette.tsx index 507d8a49cab..f183de9c6c1 100644 --- a/web/components/command-palette/command-pallette.tsx +++ b/web/components/command-palette/command-pallette.tsx @@ -41,7 +41,7 @@ export const CommandPalette: React.FC = observer(() => { const [isCreateUpdatePageModalOpen, setIsCreateUpdatePageModalOpen] = useState(false); const router = useRouter(); - const { workspaceSlug, projectId, issueId, inboxId } = router.query; + const { workspaceSlug, projectId, issueId, inboxId, cycleId, moduleId } = router.query; const { user } = useUser(); @@ -183,6 +183,13 @@ export const CommandPalette: React.FC = observer(() => { isOpen={isIssueModalOpen} handleClose={() => setIsIssueModalOpen(false)} fieldsToShow={inboxId ? ["name", "description", "priority"] : ["all"]} + prePopulateData={ + cycleId + ? { cycle: cycleId.toString() } + : moduleId + ? { module: moduleId.toString() } + : undefined + } /> = ({ } : null } - fieldsToShow={[ - "name", - "description", - "label", - "assignee", - "priority", - "dueDate", - "priority", - ]} + fieldsToShow={["all"]} /> = ({ const { user } = useUser(); const { projects } = useProjects(); + const { clearValue: clearDraftIssueLocalStorage } = useLocalStorage("draftedIssue", {}); + const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString()); const { setToastAlert } = useToast(); @@ -111,11 +114,14 @@ export const CreateUpdateDraftIssueModal: React.FC = ({ return; } + if (prePopulateData && prePopulateData.project) + return setActiveProject(prePopulateData.project); + // if data is not present, set active project to the project // in the url. This has the least priority. if (projects && projects.length > 0 && !activeProject) setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null); - }, [activeProject, data, projectId, projects, isOpen]); + }, [activeProject, data, projectId, projects, isOpen, prePopulateData]); const calendarFetchKey = cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams) @@ -228,6 +234,8 @@ export const CreateUpdateDraftIssueModal: React.FC = ({ if (!data) await createIssue(payload); else await updateIssue(payload); + clearDraftIssueLocalStorage(); + if (onSubmit) await onSubmit(payload); }; diff --git a/web/components/issues/form.tsx b/web/components/issues/form.tsx index e6b4f9e0897..2e53f38666e 100644 --- a/web/components/issues/form.tsx +++ b/web/components/issues/form.tsx @@ -8,7 +8,6 @@ import { Controller, useForm } from "react-hook-form"; import aiService from "services/ai.service"; // hooks import useToast from "hooks/use-toast"; -import useLocalStorage from "hooks/use-local-storage"; // components import { GptAssistantModal } from "components/core"; import { ParentIssuesListModal } from "components/issues"; @@ -62,11 +61,9 @@ export interface IssueFormProps { setActiveProject: React.Dispatch>; createMore: boolean; setCreateMore: React.Dispatch>; - handleClose: () => void; handleDiscardClose: () => void; status: boolean; user: ICurrentUserResponse | undefined; - setIsConfirmDiscardOpen: React.Dispatch>; handleFormDirty: (payload: Partial | null) => void; fieldsToShow: ( | "project" @@ -107,8 +104,6 @@ export const IssueForm: FC = (props) => { const [gptAssistantModal, setGptAssistantModal] = useState(false); const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); - const { setValue: setValueInLocalStorage } = useLocalStorage("draftedIssue", null); - const editorRef = useRef(null); const router = useRouter(); @@ -139,9 +134,11 @@ export const IssueForm: FC = (props) => { state: getValues("state"), priority: getValues("priority"), assignees: getValues("assignees"), - target_date: getValues("target_date"), labels: getValues("labels"), + start_date: getValues("start_date"), + target_date: getValues("target_date"), project: getValues("project"), + parent: getValues("parent"), }; useEffect(() => { @@ -571,8 +568,6 @@ export const IssueForm: FC = (props) => {
{ - const data = JSON.stringify(getValues()); - setValueInLocalStorage(data); handleDiscardClose(); }} > diff --git a/web/components/issues/modal.tsx b/web/components/issues/modal.tsx index d6ab434915d..65580c94ae1 100644 --- a/web/components/issues/modal.tsx +++ b/web/components/issues/modal.tsx @@ -19,6 +19,7 @@ import useInboxView from "hooks/use-inbox-view"; import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; import useProjects from "hooks/use-projects"; import useMyIssues from "hooks/my-issues/use-my-issues"; +import useLocalStorage from "hooks/use-local-storage"; // components import { IssueForm, ConfirmIssueDiscard } from "components/issues"; // types @@ -92,10 +93,11 @@ export const CreateUpdateIssueModal: React.FC = ({ const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString()); + const { setValue: setValueInLocalStorage, clearValue: clearLocalStorageValue } = + useLocalStorage("draftedIssue", {}); + 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, @@ -103,6 +105,13 @@ export const CreateUpdateIssueModal: React.FC = ({ }; const onClose = () => { + if (!showConfirmDiscard) handleClose(); + if (formDirtyState === null) return setActiveProject(null); + const data = JSON.stringify(formDirtyState); + setValueInLocalStorage(data); + }; + + const onDiscardClose = () => { if (formDirtyState !== null) { setShowConfirmDiscard(true); } else { @@ -111,11 +120,6 @@ export const CreateUpdateIssueModal: React.FC = ({ } }; - const onDiscardClose = () => { - handleClose(); - setActiveProject(null); - }; - const handleFormDirty = (data: any) => { setFormDirtyState(data); }; @@ -397,6 +401,7 @@ export const CreateUpdateIssueModal: React.FC = ({ setActiveProject(null); setFormDirtyState(null); setShowConfirmDiscard(false); + clearLocalStorageValue(); }} /> @@ -431,9 +436,7 @@ export const CreateUpdateIssueModal: React.FC = ({ initialData={data ?? prePopulateData} createMore={createMore} setCreateMore={setCreateMore} - handleClose={onClose} handleDiscardClose={onDiscardClose} - setIsConfirmDiscardOpen={setShowConfirmDiscard} projectId={activeProject ?? ""} setActiveProject={setActiveProject} status={data ? true : false} diff --git a/web/components/ui/buttons/type.d.ts b/web/components/ui/buttons/type.d.ts index 1391e0771d2..b227887ef6e 100644 --- a/web/components/ui/buttons/type.d.ts +++ b/web/components/ui/buttons/type.d.ts @@ -1,7 +1,7 @@ export type ButtonProps = { children: React.ReactNode; className?: string; - onClick?: () => void; + onClick?: (e: any) => void; type?: "button" | "submit" | "reset"; disabled?: boolean; loading?: boolean; diff --git a/web/components/workspace/sidebar-quick-action.tsx b/web/components/workspace/sidebar-quick-action.tsx index c44bf6d8a14..4c7dda3b99b 100644 --- a/web/components/workspace/sidebar-quick-action.tsx +++ b/web/components/workspace/sidebar-quick-action.tsx @@ -17,7 +17,10 @@ export const WorkspaceSidebarQuickAction = () => { const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false); - const { storedValue, clearValue } = useLocalStorage("draftedIssue", null); + const { storedValue, clearValue } = useLocalStorage( + "draftedIssue", + JSON.stringify(undefined) + ); return ( <> @@ -30,18 +33,7 @@ export const WorkspaceSidebarQuickAction = () => { clearValue(); setIsDraftIssueModalOpen(false); }} - fieldsToShow={[ - "name", - "description", - "label", - "assignee", - "priority", - "dueDate", - "priority", - "state", - "startDate", - "project", - ]} + fieldsToShow={["all"]} />
{ }`} >
{ + async deleteDraftIssue( + workspaceSlug: string, + projectId: string, + issueId: string, + user: ICurrentUserResponse + ): Promise { return this.delete( `/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/` ) diff --git a/web/store/draft-issue.ts b/web/store/draft-issue.ts new file mode 100644 index 00000000000..f4247c04d52 --- /dev/null +++ b/web/store/draft-issue.ts @@ -0,0 +1,189 @@ +// mobx +import { action, observable, runInAction, makeAutoObservable } from "mobx"; +// services +import issueService from "services/issues.service"; +// types +import type { ICurrentUserResponse, IIssue } from "types"; + +class DraftIssuesStore { + issues: { [key: string]: IIssue } = {}; + isIssuesLoading: boolean = false; + rootStore: any | null = null; + + constructor(_rootStore: any | null = null) { + makeAutoObservable(this, { + issues: observable.ref, + isIssuesLoading: observable.ref, + rootStore: observable.ref, + loadDraftIssues: action, + getIssueById: action, + createDraftIssue: action, + updateDraftIssue: action, + deleteDraftIssue: action, + }); + + this.rootStore = _rootStore; + } + + /** + * @description Fetch all draft issues of a project and hydrate issues field + */ + + loadDraftIssues = async (workspaceSlug: string, projectId: string, params?: any) => { + this.isIssuesLoading = true; + try { + const issuesResponse = await issueService.getDraftIssues(workspaceSlug, projectId, params); + + const issues = Array.isArray(issuesResponse) ? { allIssues: issuesResponse } : issuesResponse; + + runInAction(() => { + this.issues = issues; + this.isIssuesLoading = false; + }); + } catch (error) { + this.isIssuesLoading = false; + console.error("Fetching issues error", error); + } + }; + + /** + * @description Fetch a single draft issue by id and hydrate issues field + * @param workspaceSlug + * @param projectId + * @param issueId + * @returns {IIssue} + */ + + getIssueById = async ( + workspaceSlug: string, + projectId: string, + issueId: string + ): Promise => { + if (this.issues[issueId]) return this.issues[issueId]; + + try { + const issueResponse: IIssue = await issueService.getDraftIssueById( + workspaceSlug, + projectId, + issueId + ); + + const issues = { + ...this.issues, + [issueId]: { ...issueResponse }, + }; + + runInAction(() => { + this.issues = issues; + }); + + return issueResponse; + } catch (error) { + throw error; + } + }; + + /** + * @description Create a new draft issue and hydrate issues field + * @param workspaceSlug + * @param projectId + * @param issueForm + * @param user + * @returns {IIssue} + */ + + createDraftIssue = async ( + workspaceSlug: string, + projectId: string, + issueForm: IIssue, + user: ICurrentUserResponse + ): Promise => { + try { + const issueResponse = await issueService.createDraftIssue( + workspaceSlug, + projectId, + issueForm, + user + ); + + const issues = { + ...this.issues, + [issueResponse.id]: { ...issueResponse }, + }; + + runInAction(() => { + this.issues = issues; + }); + return issueResponse; + } catch (error) { + console.error("Creating issue error", error); + throw error; + } + }; + + updateDraftIssue = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + issueForm: Partial, + user: ICurrentUserResponse + ) => { + // keep a copy of the issue in the store + const originalIssue = { ...this.issues[issueId] }; + + // immediately update the issue in the store + const updatedIssue = { ...this.issues[issueId], ...issueForm }; + if (updatedIssue.assignees_list) updatedIssue.assignees = updatedIssue.assignees_list; + + try { + runInAction(() => { + this.issues[issueId] = { ...updatedIssue }; + }); + + // make a patch request to update the issue + const issueResponse: IIssue = await issueService.updateDraftIssue( + workspaceSlug, + projectId, + issueId, + issueForm, + user + ); + + const updatedIssues = { ...this.issues }; + updatedIssues[issueId] = { ...issueResponse }; + + runInAction(() => { + this.issues = updatedIssues; + }); + } catch (error) { + // if there is an error, revert the changes + runInAction(() => { + this.issues[issueId] = originalIssue; + }); + + return error; + } + }; + + deleteDraftIssue = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + user: ICurrentUserResponse + ) => { + const issues = { ...this.issues }; + delete issues[issueId]; + + try { + runInAction(() => { + this.issues = issues; + }); + + issueService.deleteDraftIssue(workspaceSlug, projectId, issueId, user); + } catch (error) { + console.error("Deleting issue error", error); + } + }; +} + +export default DraftIssuesStore; diff --git a/web/store/root.ts b/web/store/root.ts index ce0bdfad5b0..cdb4c83566c 100644 --- a/web/store/root.ts +++ b/web/store/root.ts @@ -6,6 +6,7 @@ import ThemeStore from "./theme"; import ProjectStore, { IProjectStore } from "./project"; import ProjectPublishStore, { IProjectPublishStore } from "./project-publish"; import IssuesStore from "./issues"; +import DraftIssuesStore from "./draft-issue"; enableStaticRendering(typeof window === "undefined"); @@ -15,6 +16,7 @@ export class RootStore { project: IProjectStore; projectPublish: IProjectPublishStore; issues: IssuesStore; + draftIssuesStore: DraftIssuesStore; constructor() { this.user = new UserStore(this); @@ -22,5 +24,6 @@ export class RootStore { this.project = new ProjectStore(this); this.projectPublish = new ProjectPublishStore(this); this.issues = new IssuesStore(this); + this.draftIssuesStore = new DraftIssuesStore(this); } } From 84e4a3d718dd1b827be51329c217e2434bb9cdc4 Mon Sep 17 00:00:00 2001 From: dakshesh14 Date: Fri, 15 Sep 2023 22:27:25 +0530 Subject: [PATCH 2/4] fix: build error --- web/components/issues/delete-draft-issue-modal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/issues/delete-draft-issue-modal.tsx b/web/components/issues/delete-draft-issue-modal.tsx index ddbe2a26975..6fc4f42189c 100644 --- a/web/components/issues/delete-draft-issue-modal.tsx +++ b/web/components/issues/delete-draft-issue-modal.tsx @@ -50,12 +50,12 @@ export const DeleteDraftIssueModal: React.FC = (props) => { }; const handleDeletion = async () => { - if (!workspaceSlug || !data) return; + if (!workspaceSlug || !data || !user) return; setIsDeleteLoading(true); await issueServices - .deleteDraftIssue(workspaceSlug as string, data.project, data.id) + .deleteDraftIssue(workspaceSlug as string, data.project, data.id, user) .then(() => { setIsDeleteLoading(false); handleClose(); From f0c14811343bc6593cd1ef7efa70c62d533969e1 Mon Sep 17 00:00:00 2001 From: dakshesh14 Date: Tue, 19 Sep 2023 18:42:53 +0530 Subject: [PATCH 3/4] fix: properties getting de-selected after create, module & cycle not getting auto-select on the form --- .../command-palette/command-pallette.tsx | 9 +- web/components/issues/draft-issue-form.tsx | 29 +++-- web/components/issues/draft-issue-modal.tsx | 105 ++++++++++++++++-- web/components/issues/modal.tsx | 62 +++++++++-- .../workspace/sidebar-quick-action.tsx | 90 ++++++--------- 5 files changed, 199 insertions(+), 96 deletions(-) diff --git a/web/components/command-palette/command-pallette.tsx b/web/components/command-palette/command-pallette.tsx index f183de9c6c1..507d8a49cab 100644 --- a/web/components/command-palette/command-pallette.tsx +++ b/web/components/command-palette/command-pallette.tsx @@ -41,7 +41,7 @@ export const CommandPalette: React.FC = observer(() => { const [isCreateUpdatePageModalOpen, setIsCreateUpdatePageModalOpen] = useState(false); const router = useRouter(); - const { workspaceSlug, projectId, issueId, inboxId, cycleId, moduleId } = router.query; + const { workspaceSlug, projectId, issueId, inboxId } = router.query; const { user } = useUser(); @@ -183,13 +183,6 @@ export const CommandPalette: React.FC = observer(() => { isOpen={isIssueModalOpen} handleClose={() => setIsIssueModalOpen(false)} fieldsToShow={inboxId ? ["name", "description", "priority"] : ["all"]} - prePopulateData={ - cycleId - ? { cycle: cycleId.toString() } - : moduleId - ? { module: moduleId.toString() } - : undefined - } /> = { }; interface IssueFormProps { - handleFormSubmit: (formData: Partial) => Promise; + handleFormSubmit: ( + formData: Partial, + action?: "createDraft" | "createToNewIssue" | "updateDraft" + ) => 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" | "createToNewIssue" | "updateDraft" = "createDraft" ) => { - await handleFormSubmit({ - ...formData, - is_draft: action === "saveDraft", - }); + await handleFormSubmit( + { + ...(data ?? {}), + ...formData, + is_draft: action === "createDraft", + }, + action + ); setGptAssistantModal(false); @@ -563,15 +570,13 @@ export const DraftIssueForm: FC = (props) => { Discard handleCreateUpdateIssue(formData, "saveDraft"))} + onClick={handleSubmit((formData) => handleCreateUpdateIssue(formData, "createDraft"))} > {isSubmitting ? "Saving..." : "Save Draft"} - {data && ( - - {isSubmitting ? "Saving..." : "Add Issue"} - - )} + + {isSubmitting ? "Saving..." : "Add Issue"} +
diff --git a/web/components/issues/draft-issue-modal.tsx b/web/components/issues/draft-issue-modal.tsx index 3b0664cb804..011516b349b 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; @@ -109,12 +112,9 @@ 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) + if (prePopulateData && prePopulateData.project && !activeProject) return setActiveProject(prePopulateData.project); // if data is not present, set active project to the project @@ -147,7 +147,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 +187,7 @@ export const CreateUpdateDraftIssueModal: React.FC = ({ if (!createMore) onClose(); }; - const updateIssue = async (payload: Partial) => { + const updateDraftIssue = async (payload: Partial) => { if (!user) return; await issuesService @@ -220,7 +220,91 @@ 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 (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" | "createToNewIssue" | "updateDraft" = "createDraft" + ) => { if (!workspaceSlug || !activeProject) return; const payload: Partial = { @@ -231,8 +315,9 @@ 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") await updateDraftIssue(payload); + else if (action === "createToNewIssue") await createIssue(payload); clearDraftIssueLocalStorage(); diff --git a/web/components/issues/modal.tsx b/web/components/issues/modal.tsx index 65580c94ae1..33728966863 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..4c07c2a91e8 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 ( <> @@ -37,12 +32,12 @@ export const WorkspaceSidebarQuickAction = () => { />
{ > - {storedValue &&
} + {storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && ( + <> +
+ + - {storedValue && ( -
- - {({ open }) => ( - <> -
- - - -
- - -
- - - -
-
-
- - )} -
-
+
+
+ +
+
+ )}
From 4522a2d13c3de38abbf8c4660538ccd908fa8c56 Mon Sep 17 00:00:00 2001 From: dakshesh14 Date: Wed, 20 Sep 2023 12:55:09 +0530 Subject: [PATCH 4/4] fix: display layout, props being updated directly --- web/components/core/views/issues-view.tsx | 25 ++++++- .../issues/confirm-issue-discard.tsx | 2 +- web/components/issues/draft-issue-form.tsx | 21 ++++-- web/components/issues/draft-issue-modal.tsx | 74 +++++++++++++------ web/components/issues/form.tsx | 2 + web/components/issues/modal.tsx | 2 +- .../workspace/sidebar-quick-action.tsx | 5 +- 7 files changed, 95 insertions(+), 36 deletions(-) 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, - action?: "createDraft" | "createToNewIssue" | "updateDraft" + action?: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" ) => Promise; data?: Partial | null; prePopulatedData?: Partial | null; @@ -137,13 +137,13 @@ export const DraftIssueForm: FC = (props) => { const handleCreateUpdateIssue = async ( formData: Partial, - action: "createDraft" | "createToNewIssue" | "updateDraft" = "createDraft" + action: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" = "createDraft" ) => { await handleFormSubmit( { ...(data ?? {}), ...formData, - is_draft: action === "createDraft", + is_draft: action === "createDraft" || action === "updateDraft", }, action ); @@ -270,7 +270,9 @@ export const DraftIssueForm: FC = (props) => { )}
handleCreateUpdateIssue(formData, "createToNewIssue"))} + onSubmit={handleSubmit((formData) => + handleCreateUpdateIssue(formData, "convertToNewIssue") + )} >
@@ -570,11 +572,18 @@ export const DraftIssueForm: FC = (props) => { Discard handleCreateUpdateIssue(formData, "createDraft"))} + onClick={handleSubmit((formData) => + handleCreateUpdateIssue(formData, data?.id ? "updateDraft" : "createDraft") + )} > {isSubmitting ? "Saving..." : "Save Draft"} - + + 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 3cfd38e04ae..c060c72f6c3 100644 --- a/web/components/issues/draft-issue-modal.tsx +++ b/web/components/issues/draft-issue-modal.tsx @@ -59,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; @@ -89,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 @@ -206,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({ @@ -290,6 +319,8 @@ export const CreateUpdateDraftIssueModal: React.FC = ({ message: "Issue created successfully.", }); + if (!createMore) onClose(); + if (payload.assignees_list?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE(workspaceSlug as string)); @@ -306,7 +337,7 @@ export const CreateUpdateDraftIssueModal: React.FC = ({ const handleFormSubmit = async ( formData: Partial, - action: "createDraft" | "createToNewIssue" | "updateDraft" = "createDraft" + action: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" = "createDraft" ) => { if (!workspaceSlug || !activeProject) return; @@ -319,10 +350,9 @@ export const CreateUpdateDraftIssueModal: React.FC = ({ }; if (action === "createDraft") await createDraftIssue(payload); - else if (action === "updateDraft") await updateDraftIssue(payload); - else if (action === "createToNewIssue") await createIssue(payload); - - clearDraftIssueLocalStorage(); + 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 33728966863..608cf4fd1eb 100644 --- a/web/components/issues/modal.tsx +++ b/web/components/issues/modal.tsx @@ -123,7 +123,7 @@ export const CreateUpdateIssueModal: React.FC = ({ setPreloadedData((prevData) => ({ ...(prevData ?? {}), ...prePopulateDataProps, - assignees: [...(prePopulateDataProps?.assignees ?? []), user?.id ?? ""], + assignees: prePopulateDataProps?.assignees ?? [user?.id ?? ""], })); } }, [prePopulateDataProps, cycleId, moduleId, router.asPath, user?.id]); diff --git a/web/components/workspace/sidebar-quick-action.tsx b/web/components/workspace/sidebar-quick-action.tsx index 4c07c2a91e8..8923abc141e 100644 --- a/web/components/workspace/sidebar-quick-action.tsx +++ b/web/components/workspace/sidebar-quick-action.tsx @@ -26,7 +26,6 @@ export const WorkspaceSidebarQuickAction = () => { onSubmit={() => { localStorage.removeItem("draftedIssue"); clearValue(); - setIsDraftIssueModalOpen(false); }} fieldsToShow={["all"]} /> @@ -66,11 +65,11 @@ export const WorkspaceSidebarQuickAction = () => {