chore: state delete validation (#930)

This commit is contained in:
Aaryan Khandelwal 2023-04-22 18:19:35 +05:30 committed by GitHub
parent 48e77ea81b
commit 99dd1b9f0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 113 additions and 103 deletions

View File

@ -14,7 +14,7 @@ import stateService from "services/state.service";
// types
import { IIssue } from "types";
// fetch keys
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATE_LIST } from "constants/fetch-keys";
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATES_LIST } from "constants/fetch-keys";
// icons
import { CheckIcon, getStateGroupIcon } from "components/icons";
@ -28,7 +28,7 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue }) =
const { workspaceSlug, projectId, issueId } = router.query;
const { data: stateGroups, mutate: mutateIssueDetails } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null

View File

@ -15,7 +15,7 @@ import issuesService from "services/issues.service";
import projectService from "services/project.service";
import stateService from "services/state.service";
// types
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATE_LIST } from "constants/fetch-keys";
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
import { IIssueFilterOptions } from "types";
export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
@ -37,7 +37,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
);
const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null

View File

@ -46,7 +46,7 @@ import {
MODULE_DETAILS,
MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS,
STATE_LIST,
STATES_LIST,
} from "constants/fetch-keys";
// image
@ -103,7 +103,7 @@ export const IssuesView: React.FC<Props> = ({
} = useIssuesView();
const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null

View File

@ -14,7 +14,7 @@ import { getStateGroupIcon } from "components/icons";
// helpers
import { getStatesList } from "helpers/state.helper";
// fetch keys
import { STATE_LIST } from "constants/fetch-keys";
import { STATES_LIST } from "constants/fetch-keys";
type Props = {
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
@ -29,7 +29,7 @@ export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
const { workspaceSlug } = router.query;
const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId) : null,
workspaceSlug && projectId ? STATES_LIST(projectId) : null,
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId)
: null

View File

@ -17,7 +17,7 @@ import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { UserAuth } from "types";
// constants
import { STATE_LIST } from "constants/fetch-keys";
import { STATES_LIST } from "constants/fetch-keys";
type Props = {
value: string;
@ -30,7 +30,7 @@ export const SidebarStateSelect: React.FC<Props> = ({ value, onChange, userAuth
const { workspaceSlug, projectId } = router.query;
const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null

View File

@ -15,7 +15,7 @@ import { getStatesList } from "helpers/state.helper";
// types
import { IIssue } from "types";
// fetch-keys
import { STATE_LIST } from "constants/fetch-keys";
import { STATES_LIST } from "constants/fetch-keys";
type Props = {
issue: IIssue;
@ -36,7 +36,7 @@ export const ViewStateSelect: React.FC<Props> = ({
const { workspaceSlug } = router.query;
const { data: stateGroups } = useSWR(
workspaceSlug && issue ? STATE_LIST(issue.project) : null,
workspaceSlug && issue ? STATES_LIST(issue.project) : null,
workspaceSlug && issue
? () => stateService.getStates(workspaceSlug as string, issue.project)
: null

View File

@ -19,9 +19,9 @@ import { CustomSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "c
// icons
import { ChevronDownIcon } from "@heroicons/react/24/outline";
// types
import type { IState } from "types";
import type { IState, IStateResponse } from "types";
// fetch keys
import { STATE_LIST } from "constants/fetch-keys";
import { STATES_LIST } from "constants/fetch-keys";
// constants
import { GROUP_CHOICES } from "constants/project";
@ -35,7 +35,7 @@ type Props = {
const defaultValues: Partial<IState> = {
name: "",
description: "",
color: "#000000",
color: "#858e96",
group: "backlog",
};
@ -70,8 +70,19 @@ export const CreateStateModal: React.FC<Props> = ({ isOpen, projectId, handleClo
await stateService
.createState(workspaceSlug as string, projectId, payload)
.then(() => {
mutate(STATE_LIST(projectId));
.then((res) => {
mutate<IStateResponse>(
STATES_LIST(projectId.toString()),
(prevData) => {
if (!prevData) return prevData;
return {
...prevData,
[res.group]: [...prevData[res.group], res],
};
},
false
);
onClose();
})
.catch((err) => {

View File

@ -17,9 +17,9 @@ import useToast from "hooks/use-toast";
// ui
import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/ui";
// types
import type { IState } from "types";
import type { IState, IStateResponse } from "types";
// fetch-keys
import { STATE_LIST } from "constants/fetch-keys";
import { STATES_LIST } from "constants/fetch-keys";
// constants
import { GROUP_CHOICES } from "constants/project";
@ -33,7 +33,7 @@ export type StateGroup = "backlog" | "unstarted" | "started" | "completed" | "ca
const defaultValues: Partial<IState> = {
name: "",
color: "#000000",
color: "#858e96",
group: "backlog",
};
@ -80,11 +80,23 @@ export const CreateUpdateStateInline: React.FC<Props> = ({ data, onClose, select
const payload: IState = {
...formData,
};
if (!data) {
await stateService
.createState(workspaceSlug as string, projectId as string, { ...payload })
.createState(workspaceSlug.toString(), projectId.toString(), { ...payload })
.then((res) => {
mutate(STATE_LIST(projectId as string));
mutate<IStateResponse>(
STATES_LIST(projectId.toString()),
(prevData) => {
if (!prevData) return prevData;
return {
...prevData,
[res.group]: [...prevData[res.group], res],
};
},
false
);
handleClose();
setToastAlert({
@ -109,11 +121,11 @@ export const CreateUpdateStateInline: React.FC<Props> = ({ data, onClose, select
});
} else {
await stateService
.updateState(workspaceSlug as string, projectId as string, data.id, {
.updateState(workspaceSlug.toString(), projectId.toString(), data.id, {
...payload,
})
.then(() => {
mutate(STATE_LIST(projectId as string));
mutate(STATES_LIST(projectId.toString()));
handleClose();
setToastAlert({

View File

@ -1,8 +1,8 @@
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
import { mutate } from "swr";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
@ -10,17 +10,14 @@ import { Dialog, Transition } from "@headlessui/react";
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// services
import stateServices from "services/state.service";
import issuesServices from "services/issues.service";
// hooks
import useToast from "hooks/use-toast";
// ui
import { DangerButton, SecondaryButton } from "components/ui";
// helpers
import { groupBy } from "helpers/array.helper";
// types
import type { IState } from "types";
import type { IState, IStateResponse } from "types";
// fetch-keys
import { STATE_LIST, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
import { STATES_LIST } from "constants/fetch-keys";
type Props = {
isOpen: boolean;
@ -30,54 +27,60 @@ type Props = {
export const DeleteStateModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [issuesWithThisStateExist, setIssuesWithThisStateExist] = useState(true);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { workspaceSlug } = router.query;
const { setToastAlert } = useToast();
const { data: issues } = useSWR(
workspaceSlug && projectId
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
: null,
workspaceSlug && projectId
? () => issuesServices.getIssues(workspaceSlug as string, projectId as string)
: null
);
const handleClose = () => {
onClose();
setIsDeleteLoading(false);
};
const handleDeletion = async () => {
if (!workspaceSlug || !data) return;
setIsDeleteLoading(true);
if (!data || !workspaceSlug || issuesWithThisStateExist) return;
await stateServices
.deleteState(workspaceSlug as string, data.project, data.id)
.then(() => {
mutate(STATE_LIST(data.project));
handleClose();
mutate<IStateResponse>(
STATES_LIST(data.project),
(prevData) => {
if (!prevData) return prevData;
setToastAlert({
title: "Success",
type: "success",
message: "State deleted successfully",
});
const stateGroup = [...prevData[data.group]].filter((s) => s.id !== data.id);
return {
...prevData,
[data.group]: stateGroup,
};
},
false
);
handleClose();
})
.catch((error) => {
console.log(error);
.catch((err) => {
setIsDeleteLoading(false);
if (err.status === 400)
setToastAlert({
type: "error",
title: "Error!",
message:
"This state contains some issues within it, please move them to some other state to delete this state.",
});
else
setToastAlert({
type: "error",
title: "Error!",
message: "State could not be deleted. Please try again.",
});
});
};
const groupedIssues = groupBy(issues ?? [], "state");
useEffect(() => {
if (data) setIssuesWithThisStateExist(!!groupedIssues[data.id]);
}, [groupedIssues, data]);
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
@ -127,24 +130,12 @@ export const DeleteStateModal: React.FC<Props> = ({ isOpen, onClose, data }) =>
the state will be permanently removed. This action cannot be undone.
</p>
</div>
<div className="mt-2">
{issuesWithThisStateExist && (
<p className="text-sm text-red-500">
There are issues with this state. Please move them to another state
before deleting this state.
</p>
)}
</div>
</div>
</div>
</div>
<div className="flex justify-end gap-2 bg-gray-50 p-4 sm:px-6">
<div className="flex justify-end gap-2 p-4 sm:px-6">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<DangerButton
onClick={handleDeletion}
disabled={issuesWithThisStateExist}
loading={isDeleteLoading}
>
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
{isDeleteLoading ? "Deleting..." : "Delete"}
</DangerButton>
</div>

View File

@ -15,6 +15,7 @@ import {
PencilSquareIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { getStateGroupIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
import { groupBy, orderArrayBy } from "helpers/array.helper";
@ -22,8 +23,7 @@ import { orderStateGroups } from "helpers/state.helper";
// types
import { IState } from "types";
// fetch-keys
import { STATE_LIST } from "constants/fetch-keys";
import { getStateGroupIcon } from "components/icons";
import { STATES_LIST } from "constants/fetch-keys";
type Props = {
index: number;
@ -60,7 +60,7 @@ export const SingleState: React.FC<Props> = ({
newStatesList = orderArrayBy(newStatesList, "sequence", "ascending");
mutate(
STATE_LIST(projectId as string),
STATES_LIST(projectId as string),
orderStateGroups(groupBy(newStatesList, "group")),
false
);
@ -76,7 +76,7 @@ export const SingleState: React.FC<Props> = ({
default: true,
})
.then(() => {
mutate(STATE_LIST(projectId as string));
mutate(STATES_LIST(projectId as string));
setIsSubmitting(false);
})
.catch(() => {
@ -89,7 +89,7 @@ export const SingleState: React.FC<Props> = ({
default: true,
})
.then(() => {
mutate(STATE_LIST(projectId as string));
mutate(STATES_LIST(projectId as string));
setIsSubmitting(false);
})
.catch(() => {
@ -115,7 +115,7 @@ export const SingleState: React.FC<Props> = ({
newStatesList = orderArrayBy(newStatesList, "sequence", "ascending");
mutate(
STATE_LIST(projectId as string),
STATES_LIST(projectId as string),
orderStateGroups(groupBy(newStatesList, "group")),
false
);
@ -126,7 +126,7 @@ export const SingleState: React.FC<Props> = ({
})
.then((res) => {
console.log(res);
mutate(STATE_LIST(projectId as string));
mutate(STATES_LIST(projectId as string));
})
.catch((err) => {
console.error(err);
@ -140,7 +140,7 @@ export const SingleState: React.FC<Props> = ({
<div className="flex items-center gap-3">
{getStateGroupIcon(state.group, "20", "20", state.color)}
<div>
<h6 className="font-medium text-brand-muted-1">{addSpaceIfCamelCase(state.name)}</h6>
<h6 className="text-brand-muted-1 font-medium">{addSpaceIfCamelCase(state.name)}</h6>
<p className="text-xs text-gray-400">{state.description}</p>
</div>
</div>

View File

@ -15,7 +15,7 @@ import { getStatesList } from "helpers/state.helper";
// types
import { IIssueFilterOptions, IQuery } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATE_LIST } from "constants/fetch-keys";
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
// constants
import { PRIORITIES } from "constants/project";
@ -36,7 +36,7 @@ export const SelectFilters: React.FC<Props> = ({
const { workspaceSlug, projectId } = router.query;
const { data: states } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null

View File

@ -89,8 +89,8 @@ export const CYCLE_DRAFT_LIST = (projectId: string) =>
export const CYCLE_COMPLETE_LIST = (projectId: string) =>
`CYCLE_COMPLETE_LIST_${projectId.toUpperCase()}`;
export const STATE_LIST = (projectId: string) => `STATE_LIST_${projectId.toUpperCase()}`;
export const STATE_DETAIL = "STATE_DETAILS";
export const STATES_LIST = (projectId: string) => `STATES_LIST_${projectId.toUpperCase()}`;
export const STATE_DETAILS = "STATE_DETAILS";
export const USER_ISSUE = (workspaceSlug: string) => `USER_ISSUE_${workspaceSlug.toUpperCase()}`;
export const USER_ACTIVITY = "USER_ACTIVITY";

View File

@ -1,7 +1,7 @@
// types
import { IState, StateResponse } from "types";
import { IState, IStateResponse } from "types";
export const orderStateGroups = (unorderedStateGroups: StateResponse) =>
export const orderStateGroups = (unorderedStateGroups: IStateResponse) =>
Object.assign(
{ backlog: [], unstarted: [], started: [], completed: [], cancelled: [] },
unorderedStateGroups

View File

@ -20,7 +20,7 @@ import {
CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS,
STATE_LIST,
STATES_LIST,
} from "constants/fetch-keys";
const useIssuesView = () => {
@ -100,7 +100,7 @@ const useIssuesView = () => {
);
const { data: states } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null

View File

@ -12,7 +12,7 @@ import { getStatesList } from "helpers/state.helper";
// types
import { Properties, NestedKeyOf, IIssue } from "types";
// fetch-keys
import { STATE_LIST } from "constants/fetch-keys";
import { STATES_LIST } from "constants/fetch-keys";
// constants
import { PRIORITIES } from "constants/project";
@ -41,7 +41,7 @@ const useMyIssuesProperties = (issues?: IIssue[]) => {
const { user } = useUser();
const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null

View File

@ -6,7 +6,8 @@ import useSWR from "swr";
// services
import stateService from "services/state.service";
import projectService from "services/project.service";
// hooks
import useProjectDetails from "hooks/use-project-details";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// components
@ -26,7 +27,7 @@ import { getStatesList, orderStateGroups } from "helpers/state.helper";
// types
import type { NextPage } from "next";
// fetch-keys
import { PROJECT_DETAILS, STATE_LIST } from "constants/fetch-keys";
import { STATES_LIST } from "constants/fetch-keys";
const StatesSettings: NextPage = () => {
const [activeGroup, setActiveGroup] = useState<StateGroup>(null);
@ -36,15 +37,10 @@ const StatesSettings: NextPage = () => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: projectDetails } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
: null
);
const { projectDetails } = useProjectDetails();
const { data: states } = useSWR(
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null

View File

@ -8,7 +8,7 @@ const trackEvent =
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
// types
import type { IState, StateResponse } from "types";
import type { IState, IStateResponse } from "types";
class ProjectStateServices extends APIService {
constructor() {
@ -26,7 +26,7 @@ class ProjectStateServices extends APIService {
});
}
async getStates(workspaceSlug: string, projectId: string): Promise<StateResponse> {
async getStates(workspaceSlug: string, projectId: string): Promise<IStateResponse> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/`)
.then((response) => response?.data)
.catch((error) => {
@ -96,7 +96,7 @@ class ProjectStateServices extends APIService {
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
throw error?.response;
});
}
}

View File

@ -19,6 +19,6 @@ export interface IState {
workspace_detail: IWorkspaceLite;
}
export interface StateResponse {
export interface IStateResponse {
[key: string]: IState[];
}