From 79bf7d4c0c42e945fa5beb6f9a4b126ae4a4ecdc Mon Sep 17 00:00:00 2001 From: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Date: Tue, 19 Sep 2023 12:56:32 +0530 Subject: [PATCH 1/7] fix: hydration error and draft issue workflow (#2199) * fix: hydration error and draft issue workflow * fix: build error --- .../command-palette/command-pallette.tsx | 9 +- web/components/core/views/issues-view.tsx | 11 +- .../issues/delete-draft-issue-modal.tsx | 4 +- 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 + 11 files changed, 240 insertions(+), 47 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 507d8a49c..f183de9c6 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"]} /> = (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(); diff --git a/web/components/issues/draft-issue-modal.tsx b/web/components/issues/draft-issue-modal.tsx index 489a09d18..3b0664cb8 100644 --- a/web/components/issues/draft-issue-modal.tsx +++ b/web/components/issues/draft-issue-modal.tsx @@ -14,6 +14,7 @@ import useIssuesView from "hooks/use-issues-view"; import useCalendarIssuesView from "hooks/use-calendar-issues-view"; import useToast from "hooks/use-toast"; import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; +import useLocalStorage from "hooks/use-local-storage"; import useProjects from "hooks/use-projects"; import useMyIssues from "hooks/my-issues/use-my-issues"; // components @@ -79,6 +80,8 @@ export const CreateUpdateDraftIssueModal: React.FC = ({ 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 e6b4f9e08..2e53f3866 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 d6ab43491..65580c94a 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 1391e0771..b227887ef 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 c44bf6d8a..4c7dda3b9 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 000000000..f4247c04d --- /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 ce0bdfad5..cdb4c8356 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 f6b92fc953544f1461ba672dc2ae9b4150c0a342 Mon Sep 17 00:00:00 2001 From: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Date: Tue, 19 Sep 2023 12:58:00 +0530 Subject: [PATCH 2/7] fix: activity not coming for blocking/blocked, 'related to' and duplicate (#2189) * fix: activity not coming for duplicate, relatesd to and for blocked/blocking refactor: mutation logic to use relation id instead of issue id * fix: mutation logic and changed keys to be aligned with api * fix: build error --- web/components/core/activity.tsx | 43 ++++++++++++++++++- .../issues/sidebar-select/blocked.tsx | 8 ++-- .../issues/sidebar-select/blocker.tsx | 9 ++-- .../issues/sidebar-select/duplicate.tsx | 38 ++++++++-------- .../issues/sidebar-select/relates-to.tsx | 38 ++++++++-------- web/components/issues/sidebar.tsx | 38 +++++++--------- web/components/web-view/activity-message.tsx | 35 ++++++++++++++- .../web-view/issue-properties-detail.tsx | 10 ++--- web/services/issues.service.ts | 1 + web/types/issues.d.ts | 20 +++------ 10 files changed, 142 insertions(+), 98 deletions(-) diff --git a/web/components/core/activity.tsx b/web/components/core/activity.tsx index 4a20c15e8..7c2798e7a 100644 --- a/web/components/core/activity.tsx +++ b/web/components/core/activity.tsx @@ -2,8 +2,9 @@ import { useRouter } from "next/router"; // icons import { Icon, Tooltip } from "components/ui"; +import { CopyPlus } from "lucide-react"; import { Squares2X2Icon } from "@heroicons/react/24/outline"; -import { BlockedIcon, BlockerIcon } from "components/icons"; +import { BlockedIcon, BlockerIcon, RelatedIcon } from "components/icons"; // helpers import { renderShortDateWithYearFormat } from "helpers/date-time.helper"; import { capitalizeFirstLetter } from "helpers/string.helper"; @@ -157,7 +158,7 @@ const activityDetails: { }, icon: , }, - blocks: { + blocked_by: { message: (activity) => { if (activity.old_value === "") return ( @@ -176,6 +177,44 @@ const activityDetails: { }, icon: , }, + duplicate: { + message: (activity) => { + if (activity.old_value === "") + return ( + <> + marked this issue as duplicate of{" "} + {activity.new_value}. + + ); + else + return ( + <> + removed this issue as a duplicate of{" "} + {activity.old_value}. + + ); + }, + icon: , + }, + relates_to: { + message: (activity) => { + if (activity.old_value === "") + return ( + <> + marked that this issue relates to{" "} + {activity.new_value}. + + ); + else + return ( + <> + removed the relation from{" "} + {activity.old_value}. + + ); + }, + icon: , + }, cycles: { message: (activity, showIssue, workspaceSlug) => { if (activity.verb === "created") diff --git a/web/components/issues/sidebar-select/blocked.tsx b/web/components/issues/sidebar-select/blocked.tsx index 9554a83ba..d7e448377 100644 --- a/web/components/issues/sidebar-select/blocked.tsx +++ b/web/components/issues/sidebar-select/blocked.tsx @@ -73,7 +73,7 @@ export const SidebarBlockedSelect: React.FC = ({ ...selectedIssues.map((issue) => ({ issue: issueId as string, relation_type: "blocked_by" as const, - related_issue_detail: issue.blocked_issue_detail, + issue_detail: issue.blocked_issue_detail, related_issue: issue.blocked_issue_detail.id, })), ], @@ -111,17 +111,17 @@ export const SidebarBlockedSelect: React.FC = ({ {blockedByIssue && blockedByIssue.length > 0 ? blockedByIssue.map((relation) => (
- {`${relation.related_issue_detail?.project_detail.identifier}-${relation.related_issue_detail?.sequence_id}`} + {`${relation.issue_detail?.project_detail.identifier}-${relation.issue_detail?.sequence_id}`}