fix: hydration error and draft issue workflow (#2199)

* fix: hydration error and draft issue workflow

* fix: build error
This commit is contained in:
Dakshesh Jain 2023-09-19 12:56:32 +05:30 committed by GitHub
parent 5d331477ef
commit 79bf7d4c0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 240 additions and 47 deletions

View File

@ -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
}
/>
<BulkDeleteIssuesModal
isOpen={isBulkDeleteIssuesModalOpen}

View File

@ -23,7 +23,6 @@ import {
CreateUpdateIssueModal,
DeleteIssueModal,
DeleteDraftIssueModal,
IssuePeekOverview,
CreateUpdateDraftIssueModal,
} from "components/issues";
import { CreateUpdateViewModal } from "components/views";
@ -484,15 +483,7 @@ export const IssuesView: React.FC<Props> = ({
}
: null
}
fieldsToShow={[
"name",
"description",
"label",
"assignee",
"priority",
"dueDate",
"priority",
]}
fieldsToShow={["all"]}
/>
<CreateUpdateIssueModal
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}

View File

@ -50,12 +50,12 @@ export const DeleteDraftIssueModal: React.FC<Props> = (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();

View File

@ -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<IssuesModalProps> = ({
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<IssuesModalProps> = ({
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<IssuesModalProps> = ({
if (!data) await createIssue(payload);
else await updateIssue(payload);
clearDraftIssueLocalStorage();
if (onSubmit) await onSubmit(payload);
};

View File

@ -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<React.SetStateAction<string | null>>;
createMore: boolean;
setCreateMore: React.Dispatch<React.SetStateAction<boolean>>;
handleClose: () => void;
handleDiscardClose: () => void;
status: boolean;
user: ICurrentUserResponse | undefined;
setIsConfirmDiscardOpen: React.Dispatch<React.SetStateAction<boolean>>;
handleFormDirty: (payload: Partial<IIssue> | null) => void;
fieldsToShow: (
| "project"
@ -107,8 +104,6 @@ export const IssueForm: FC<IssueFormProps> = (props) => {
const [gptAssistantModal, setGptAssistantModal] = useState(false);
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
const { setValue: setValueInLocalStorage } = useLocalStorage<any>("draftedIssue", null);
const editorRef = useRef<any>(null);
const router = useRouter();
@ -139,9 +134,11 @@ export const IssueForm: FC<IssueFormProps> = (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<IssueFormProps> = (props) => {
<div className="flex items-center gap-2">
<SecondaryButton
onClick={() => {
const data = JSON.stringify(getValues());
setValueInLocalStorage(data);
handleDiscardClose();
}}
>

View File

@ -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<IssuesModalProps> = ({
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
const { setValue: setValueInLocalStorage, clearValue: clearLocalStorageValue } =
useLocalStorage<any>("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<IssuesModalProps> = ({
};
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<IssuesModalProps> = ({
}
};
const onDiscardClose = () => {
handleClose();
setActiveProject(null);
};
const handleFormDirty = (data: any) => {
setFormDirtyState(data);
};
@ -397,6 +401,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
setActiveProject(null);
setFormDirtyState(null);
setShowConfirmDiscard(false);
clearLocalStorageValue();
}}
/>
@ -431,9 +436,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
initialData={data ?? prePopulateData}
createMore={createMore}
setCreateMore={setCreateMore}
handleClose={onClose}
handleDiscardClose={onDiscardClose}
setIsConfirmDiscardOpen={setShowConfirmDiscard}
projectId={activeProject ?? ""}
setActiveProject={setActiveProject}
status={data ? true : false}

View File

@ -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;

View File

@ -17,7 +17,10 @@ export const WorkspaceSidebarQuickAction = () => {
const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false);
const { storedValue, clearValue } = useLocalStorage<any>("draftedIssue", null);
const { storedValue, clearValue } = useLocalStorage<any>(
"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"]}
/>
<div
@ -50,7 +42,7 @@ export const WorkspaceSidebarQuickAction = () => {
}`}
>
<div
className={`flex items-center justify-between w-full rounded cursor-pointer px-2 gap-1 ${
className={`flex items-center justify-between w-full rounded cursor-pointer px-2 gap-1 group ${
store?.theme?.sidebarCollapsed
? "px-2 hover:bg-custom-sidebar-background-80"
: "px-3 shadow border-[0.5px] border-custom-border-300"

View File

@ -658,7 +658,12 @@ class ProjectIssuesServices extends APIService {
});
}
async deleteDraftIssue(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
async deleteDraftIssue(
workspaceSlug: string,
projectId: string,
issueId: string,
user: ICurrentUserResponse
): Promise<any> {
return this.delete(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`
)

189
web/store/draft-issue.ts Normal file
View File

@ -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<IIssue> => {
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<IIssue> => {
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<IIssue>,
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;

View File

@ -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);
}
}