diff --git a/apps/app/components/command-palette/change-issue-state.tsx b/apps/app/components/command-palette/change-issue-state.tsx index 2eef56193..30ab68b63 100644 --- a/apps/app/components/command-palette/change-issue-state.tsx +++ b/apps/app/components/command-palette/change-issue-state.tsx @@ -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 = ({ 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 diff --git a/apps/app/components/core/filter-list.tsx b/apps/app/components/core/filter-list.tsx index c42b7501b..2ede6a367 100644 --- a/apps/app/components/core/filter-list.tsx +++ b/apps/app/components/core/filter-list.tsx @@ -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 = ({ filters, setFilters }) => { @@ -37,7 +37,7 @@ export const FilterList: React.FC = ({ 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 diff --git a/apps/app/components/core/issues-view.tsx b/apps/app/components/core/issues-view.tsx index 240b7fbd0..240368d7f 100644 --- a/apps/app/components/core/issues-view.tsx +++ b/apps/app/components/core/issues-view.tsx @@ -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 = ({ } = 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 diff --git a/apps/app/components/issues/select/state.tsx b/apps/app/components/issues/select/state.tsx index 1be797e0d..5a9204d39 100644 --- a/apps/app/components/issues/select/state.tsx +++ b/apps/app/components/issues/select/state.tsx @@ -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>; @@ -29,7 +29,7 @@ export const IssueStateSelect: React.FC = ({ 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 diff --git a/apps/app/components/issues/sidebar-select/state.tsx b/apps/app/components/issues/sidebar-select/state.tsx index 434b4837d..8abb362db 100644 --- a/apps/app/components/issues/sidebar-select/state.tsx +++ b/apps/app/components/issues/sidebar-select/state.tsx @@ -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 = ({ 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 diff --git a/apps/app/components/issues/view-select/state.tsx b/apps/app/components/issues/view-select/state.tsx index 4a6482cde..222b8085d 100644 --- a/apps/app/components/issues/view-select/state.tsx +++ b/apps/app/components/issues/view-select/state.tsx @@ -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 = ({ 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 diff --git a/apps/app/components/states/create-state-modal.tsx b/apps/app/components/states/create-state-modal.tsx index 769ca6821..ed62bb9ac 100644 --- a/apps/app/components/states/create-state-modal.tsx +++ b/apps/app/components/states/create-state-modal.tsx @@ -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 = { name: "", description: "", - color: "#000000", + color: "#858e96", group: "backlog", }; @@ -70,8 +70,19 @@ export const CreateStateModal: React.FC = ({ isOpen, projectId, handleClo await stateService .createState(workspaceSlug as string, projectId, payload) - .then(() => { - mutate(STATE_LIST(projectId)); + .then((res) => { + mutate( + STATES_LIST(projectId.toString()), + (prevData) => { + if (!prevData) return prevData; + + return { + ...prevData, + [res.group]: [...prevData[res.group], res], + }; + }, + false + ); onClose(); }) .catch((err) => { diff --git a/apps/app/components/states/create-update-state-inline.tsx b/apps/app/components/states/create-update-state-inline.tsx index 3a13faf25..36f780bd1 100644 --- a/apps/app/components/states/create-update-state-inline.tsx +++ b/apps/app/components/states/create-update-state-inline.tsx @@ -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 = { name: "", - color: "#000000", + color: "#858e96", group: "backlog", }; @@ -80,11 +80,23 @@ export const CreateUpdateStateInline: React.FC = ({ 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( + 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 = ({ 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({ diff --git a/apps/app/components/states/delete-state-modal.tsx b/apps/app/components/states/delete-state-modal.tsx index 362ff9353..a49ed561e 100644 --- a/apps/app/components/states/delete-state-modal.tsx +++ b/apps/app/components/states/delete-state-modal.tsx @@ -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 = ({ 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( + 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 ( @@ -127,24 +130,12 @@ export const DeleteStateModal: React.FC = ({ isOpen, onClose, data }) => the state will be permanently removed. This action cannot be undone.

-
- {issuesWithThisStateExist && ( -

- There are issues with this state. Please move them to another state - before deleting this state. -

- )} -
-
+
Cancel - + {isDeleteLoading ? "Deleting..." : "Delete"}
diff --git a/apps/app/components/states/single-state.tsx b/apps/app/components/states/single-state.tsx index 2a65a1674..a295d36ca 100644 --- a/apps/app/components/states/single-state.tsx +++ b/apps/app/components/states/single-state.tsx @@ -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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ }) .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 = ({
{getStateGroupIcon(state.group, "20", "20", state.color)}
-
{addSpaceIfCamelCase(state.name)}
+
{addSpaceIfCamelCase(state.name)}

{state.description}

diff --git a/apps/app/components/views/select-filters.tsx b/apps/app/components/views/select-filters.tsx index 2d25983c7..1b6986346 100644 --- a/apps/app/components/views/select-filters.tsx +++ b/apps/app/components/views/select-filters.tsx @@ -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 = ({ 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 diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index 51491ab1e..409f9a4d5 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -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"; diff --git a/apps/app/helpers/state.helper.ts b/apps/app/helpers/state.helper.ts index 7229957d0..512da4f19 100644 --- a/apps/app/helpers/state.helper.ts +++ b/apps/app/helpers/state.helper.ts @@ -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 diff --git a/apps/app/hooks/use-issues-view.tsx b/apps/app/hooks/use-issues-view.tsx index cb3ddb43e..57b584db4 100644 --- a/apps/app/hooks/use-issues-view.tsx +++ b/apps/app/hooks/use-issues-view.tsx @@ -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 diff --git a/apps/app/hooks/use-my-issues-filter.tsx b/apps/app/hooks/use-my-issues-filter.tsx index 9d26a7a60..bd97427f5 100644 --- a/apps/app/hooks/use-my-issues-filter.tsx +++ b/apps/app/hooks/use-my-issues-filter.tsx @@ -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 diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx index a91c1c6cb..8b513d640 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx @@ -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(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 diff --git a/apps/app/services/state.service.ts b/apps/app/services/state.service.ts index 69f30a3dc..a9d0e4cdb 100644 --- a/apps/app/services/state.service.ts +++ b/apps/app/services/state.service.ts @@ -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 { + async getStates(workspaceSlug: string, projectId: string): Promise { 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; }); } } diff --git a/apps/app/types/state.d.ts b/apps/app/types/state.d.ts index c0d9514c2..c3d69e90b 100644 --- a/apps/app/types/state.d.ts +++ b/apps/app/types/state.d.ts @@ -19,6 +19,6 @@ export interface IState { workspace_detail: IWorkspaceLite; } -export interface StateResponse { +export interface IStateResponse { [key: string]: IState[]; }