fix: fields not getting selected in the create issue form (#2212)

* fix: hydration error and draft issue workflow

* fix: build error

* fix: properties getting de-selected after create, module & cycle not getting auto-select on the form

* fix: display layout, props being updated directly
This commit is contained in:
Dakshesh Jain 2023-09-20 13:06:51 +05:30 committed by GitHub
parent e01a0d20fe
commit cdfff12f4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 284 additions and 110 deletions

View File

@ -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<Props> = ({
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<Props> = ({
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);

View File

@ -57,7 +57,7 @@ export const ConfirmIssueDiscard: React.FC<Props> = (props) => {
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-[40rem]">
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<div className="mt-3 text-center sm:mt-0 sm:text-left">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-custom-text-100"

View File

@ -55,7 +55,10 @@ const defaultValues: Partial<IIssue> = {
};
interface IssueFormProps {
handleFormSubmit: (formData: Partial<IIssue>) => Promise<void>;
handleFormSubmit: (
formData: Partial<IIssue>,
action?: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue"
) => Promise<void>;
data?: Partial<IIssue> | null;
prePopulatedData?: Partial<IIssue> | null;
projectId: string;
@ -134,12 +137,16 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
const handleCreateUpdateIssue = async (
formData: Partial<IIssue>,
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<IssueFormProps> = (props) => {
</>
)}
<form
onSubmit={handleSubmit((formData) => handleCreateUpdateIssue(formData, "createToNewIssue"))}
onSubmit={handleSubmit((formData) =>
handleCreateUpdateIssue(formData, "convertToNewIssue")
)}
>
<div className="space-y-5">
<div className="flex items-center gap-x-2">
@ -563,15 +572,20 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
<SecondaryButton onClick={onClose}>Discard</SecondaryButton>
<SecondaryButton
loading={isSubmitting}
onClick={handleSubmit((formData) => handleCreateUpdateIssue(formData, "saveDraft"))}
onClick={handleSubmit((formData) =>
handleCreateUpdateIssue(formData, data?.id ? "updateDraft" : "createDraft")
)}
>
{isSubmitting ? "Saving..." : "Save Draft"}
</SecondaryButton>
{data && (
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Add Issue"}
</PrimaryButton>
)}
<PrimaryButton
loading={isSubmitting}
onClick={handleSubmit((formData) =>
handleCreateUpdateIssue(formData, data ? "convertToNewIssue" : "createNewIssue")
)}
>
{isSubmitting ? "Saving..." : "Add Issue"}
</PrimaryButton>
</div>
</div>
</form>

View File

@ -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<IIssue>) => Promise<void> | void;
}
export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
data,
handleClose,
isOpen,
isUpdatingSingleIssue = false,
prePopulateData,
fieldsToShow = ["all"],
onSubmit,
}) => {
export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = (props) => {
const {
data,
handleClose,
isOpen,
isUpdatingSingleIssue = false,
prePopulateData: prePopulateDataProps,
fieldsToShow = ["all"],
onSubmit,
} = props;
// states
const [createMore, setCreateMore] = useState(false);
const [activeProject, setActiveProject] = useState<string | null>(null);
const [prePopulateData, setPreloadedData] = useState<Partial<IIssue> | undefined>(undefined);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
@ -86,19 +92,40 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
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<IssuesModalProps> = ({
// 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<IssuesModalProps> = ({
? VIEW_ISSUES(viewId.toString(), viewGanttParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "");
const createIssue = async (payload: Partial<IIssue>) => {
const createDraftIssue = async (payload: Partial<IIssue>) => {
if (!workspaceSlug || !activeProject || !user) return;
await issuesService
@ -187,7 +214,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
if (!createMore) onClose();
};
const updateIssue = async (payload: Partial<IIssue>) => {
const updateDraftIssue = async (payload: Partial<IIssue>) => {
if (!user) return;
await issuesService
@ -203,6 +230,11 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
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<IssuesModalProps> = ({
});
};
const handleFormSubmit = async (formData: Partial<IIssue>) => {
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<IIssue>) => {
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<IIssue>,
action: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" = "createDraft"
) => {
if (!workspaceSlug || !activeProject) return;
const payload: Partial<IIssue> = {
@ -231,8 +349,10 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
description_html: formData.description_html ?? "<p></p>",
};
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();

View File

@ -139,6 +139,8 @@ export const IssueForm: FC<IssueFormProps> = (props) => {
target_date: getValues("target_date"),
project: getValues("project"),
parent: getValues("parent"),
cycle: getValues("cycle"),
module: getValues("module"),
};
useEffect(() => {

View File

@ -69,7 +69,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
handleClose,
isOpen,
isUpdatingSingleIssue = false,
prePopulateData,
prePopulateData: prePopulateDataProps,
fieldsToShow = ["all"],
onSubmit,
}) => {
@ -78,6 +78,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
const [formDirtyState, setFormDirtyState] = useState<any>(null);
const [showConfirmDiscard, setShowConfirmDiscard] = useState(false);
const [activeProject, setActiveProject] = useState<string | null>(null);
const [prePopulateData, setPreloadedData] = useState<Partial<IIssue>>({});
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId, inboxId } = router.query;
@ -98,11 +99,40 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
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<IssuesModalProps> = ({
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<IssuesModalProps> = ({
});
});
if (!createMore) onDiscardClose();
if (!createMore) onFormSubmitClose();
};
const createDraftIssue = async () => {
@ -354,7 +400,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
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",

View File

@ -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<any>(
"draftedIssue",
JSON.stringify(undefined)
);
const { storedValue, clearValue } = useLocalStorage<any>("draftedIssue", JSON.stringify({}));
return (
<>
@ -31,18 +26,17 @@ export const WorkspaceSidebarQuickAction = () => {
onSubmit={() => {
localStorage.removeItem("draftedIssue");
clearValue();
setIsDraftIssueModalOpen(false);
}}
fieldsToShow={["all"]}
/>
<div
className={`relative flex items-center justify-between w-full cursor-pointer px-4 mt-4 ${
className={`flex items-center justify-between w-full cursor-pointer px-4 mt-4 ${
store?.theme?.sidebarCollapsed ? "flex-col gap-1" : "gap-2"
}`}
>
<div
className={`flex items-center justify-between w-full rounded cursor-pointer px-2 gap-1 group ${
className={`relative 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"
@ -50,7 +44,7 @@ export const WorkspaceSidebarQuickAction = () => {
>
<button
type="button"
className="flex items-center gap-2 flex-grow rounded flex-shrink-0 py-1.5"
className="relative flex items-center gap-2 flex-grow rounded flex-shrink-0 py-1.5"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
@ -65,56 +59,35 @@ export const WorkspaceSidebarQuickAction = () => {
)}
</button>
{storedValue && <div className="h-8 w-0.5 bg-custom-sidebar-background-80" />}
{storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && (
<>
<div className="h-8 w-0.5 bg-custom-sidebar-background-80" />
{storedValue && (
<div className="relative">
<Menu as={React.Fragment}>
{({ open }) => (
<>
<div>
<Menu.Button
type="button"
className={`flex items-center justify-center rounded flex-shrink-0 p-1.5 ${
open ? "rotate-180 pl-0" : "rotate-0 pr-0"
}`}
>
<ChevronDown
size={16}
className="!text-custom-sidebar-text-300 transform transition-transform duration-300"
/>
</Menu.Button>
</div>
<Transition
as={React.Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute -right-4 mt-1 w-52 bg-custom-background-300">
<div className="px-1 py-1 ">
<Menu.Item>
<button
onClick={() => setIsDraftIssueModalOpen(true)}
className="w-full flex text-sm items-center rounded flex-shrink-0 py-[10px] px-3 bg-custom-background-100 shadow border-[0.5px] border-custom-border-300 text-custom-text-300"
>
<PenSquare
size={16}
className="!text-lg !leading-4 text-custom-sidebar-text-300 mr-2"
/>
Last Drafted Issue
</button>
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</>
)}
</Menu>
</div>
<button
type="button"
className="flex items-center justify-center rounded flex-shrink-0 py-1.5 ml-1.5"
>
<ChevronDown
size={16}
className="!text-custom-sidebar-text-300 transform transition-transform duration-300 group-hover:rotate-180 rotate-0"
/>
</button>
<div className="absolute w-full h-10 pt-2 top-full left-0 opacity-0 group-hover:opacity-100 mt-0 pointer-events-none group-hover:pointer-events-auto">
<div className="w-full h-full">
<button
onClick={() => setIsDraftIssueModalOpen(true)}
className="w-full flex text-sm items-center rounded flex-shrink-0 py-[10px] px-3 bg-custom-background-100 shadow border-[0.5px] border-custom-border-300 text-custom-text-300"
>
<PenSquare
size={16}
className="!text-lg !leading-4 text-custom-sidebar-text-300 mr-2"
/>
Last Drafted Issue
</button>
</div>
</div>
</>
)}
</div>