From 166520dfda68d6855839a2d7888548709fd84780 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 8 Feb 2023 18:51:03 +0530 Subject: [PATCH] feat: label grouping, fix: new states response (#254) --- .../components/core/board-view/all-boards.tsx | 1 - apps/app/components/core/issues-view.tsx | 16 +- apps/app/components/issues/select/state.tsx | 5 +- .../issues/sidebar-select/state.tsx | 8 +- .../issues/sub-issues-list-modal.tsx | 4 +- .../components/issues/view-select/state.tsx | 12 +- apps/app/components/labels/index.ts | 2 + .../components/labels/labels-list-modal.tsx | 180 ++++++++++++++++++ apps/app/components/labels/single-label.tsx | 171 +++++++++++++++++ .../project/settings/single-label.tsx | 151 --------------- apps/app/helpers/state.helper.ts | 21 ++ apps/app/hooks/use-issue-view.tsx | 4 +- apps/app/hooks/use-my-issues-filter.tsx | 4 +- .../projects/[projectId]/settings/labels.tsx | 101 +++++----- .../projects/[projectId]/settings/states.tsx | 154 +++++++-------- apps/app/services/state.service.ts | 4 +- apps/app/types/state.d.ts | 4 + 17 files changed, 539 insertions(+), 303 deletions(-) create mode 100644 apps/app/components/labels/index.ts create mode 100644 apps/app/components/labels/labels-list-modal.tsx create mode 100644 apps/app/components/labels/single-label.tsx delete mode 100644 apps/app/components/project/settings/single-label.tsx create mode 100644 apps/app/helpers/state.helper.ts diff --git a/apps/app/components/core/board-view/all-boards.tsx b/apps/app/components/core/board-view/all-boards.tsx index 77c36e548..9aa29e7c4 100644 --- a/apps/app/components/core/board-view/all-boards.tsx +++ b/apps/app/components/core/board-view/all-boards.tsx @@ -3,7 +3,6 @@ import { DragDropContext, DropResult } from "react-beautiful-dnd"; // hooks import useIssueView from "hooks/use-issue-view"; // components -import StrictModeDroppable from "components/dnd/StrictModeDroppable"; import { SingleBoard } from "components/core/board-view/single-board"; // types import { IIssue, IProjectMember, IState, UserAuth } from "types"; diff --git a/apps/app/components/core/issues-view.tsx b/apps/app/components/core/issues-view.tsx index 5f1d2c289..ec4c5b688 100644 --- a/apps/app/components/core/issues-view.tsx +++ b/apps/app/components/core/issues-view.tsx @@ -14,17 +14,12 @@ import modulesService from "services/modules.service"; // hooks import useIssueView from "hooks/use-issue-view"; // components -import { AllLists, AllBoards, ExistingIssuesListModal } from "components/core"; +import { AllLists, AllBoards } from "components/core"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +// helpers +import { getStatesList } from "helpers/state.helper"; // types -import { - CycleIssueResponse, - IIssue, - IssueResponse, - IState, - ModuleIssueResponse, - UserAuth, -} from "types"; +import { CycleIssueResponse, IIssue, IssueResponse, ModuleIssueResponse, UserAuth } from "types"; // fetch-keys import { CYCLE_ISSUES, @@ -68,12 +63,13 @@ export const IssuesView: React.FC = ({ const { issueView, groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues); - const { data: states } = useSWR( + const { data: stateGroups } = useSWR( workspaceSlug && projectId ? STATE_LIST(projectId as string) : null, workspaceSlug ? () => stateService.getStates(workspaceSlug as string, projectId as string) : null ); + const states = getStatesList(stateGroups ?? {}); const { data: members } = useSWR( projectId ? PROJECT_MEMBERS(projectId as string) : null, diff --git a/apps/app/components/issues/select/state.tsx b/apps/app/components/issues/select/state.tsx index b1bfc2cf6..269b0af73 100644 --- a/apps/app/components/issues/select/state.tsx +++ b/apps/app/components/issues/select/state.tsx @@ -10,6 +10,8 @@ import stateService from "services/state.service"; import { Squares2X2Icon, PlusIcon } from "@heroicons/react/24/outline"; // icons import { Combobox, Transition } from "@headlessui/react"; +// helpers +import { getStatesList } from "helpers/state.helper"; // fetch keys import { STATE_LIST } from "constants/fetch-keys"; @@ -27,12 +29,13 @@ export const IssueStateSelect: React.FC = ({ setIsOpen, value, onChange, const router = useRouter(); const { workspaceSlug } = router.query; - const { data: states } = useSWR( + const { data: stateGroups } = useSWR( workspaceSlug && projectId ? STATE_LIST(projectId) : null, workspaceSlug && projectId ? () => stateService.getStates(workspaceSlug as string, projectId) : null ); + const states = getStatesList(stateGroups ?? {}); const options = states?.map((state) => ({ value: state.id, diff --git a/apps/app/components/issues/sidebar-select/state.tsx b/apps/app/components/issues/sidebar-select/state.tsx index bbe57cc7a..0ed3a04bb 100644 --- a/apps/app/components/issues/sidebar-select/state.tsx +++ b/apps/app/components/issues/sidebar-select/state.tsx @@ -4,13 +4,16 @@ import { useRouter } from "next/router"; import useSWR from "swr"; +// react-hook-form import { Control, Controller } from "react-hook-form"; // services -import { Squares2X2Icon } from "@heroicons/react/24/outline"; import stateService from "services/state.service"; // ui import { Spinner, CustomSelect } from "components/ui"; // icons +import { Squares2X2Icon } from "@heroicons/react/24/outline"; +// helpers +import { getStatesList } from "helpers/state.helper"; // types import { IIssue, UserAuth } from "types"; // constants @@ -26,12 +29,13 @@ export const SidebarStateSelect: React.FC = ({ control, submitChanges, us const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { data: states } = useSWR( + const { data: stateGroups } = useSWR( workspaceSlug && projectId ? STATE_LIST(projectId as string) : null, workspaceSlug && projectId ? () => stateService.getStates(workspaceSlug as string, projectId as string) : null ); + const states = getStatesList(stateGroups ?? {}); const isNotAllowed = userAuth.isGuest || userAuth.isViewer; diff --git a/apps/app/components/issues/sub-issues-list-modal.tsx b/apps/app/components/issues/sub-issues-list-modal.tsx index 897a9d097..ac3bc7597 100644 --- a/apps/app/components/issues/sub-issues-list-modal.tsx +++ b/apps/app/components/issues/sub-issues-list-modal.tsx @@ -44,7 +44,7 @@ export const SubIssuesListModal: React.FC = ({ isOpen, handleClose, paren : issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ?? []; - const handleCommandPaletteClose = () => { + const handleModalClose = () => { handleClose(); setQuery(""); }; @@ -93,7 +93,7 @@ export const SubIssuesListModal: React.FC = ({ isOpen, handleClose, paren return ( setQuery("")} appear> - + = ({ const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { data: states } = useSWR( - workspaceSlug && projectId ? STATE_LIST(projectId as string) : null, - workspaceSlug - ? () => stateService.getStates(workspaceSlug as string, projectId as string) - : null + const { data: stateGroups } = useSWR( + workspaceSlug && projectId ? STATE_LIST(issue.project) : null, + workspaceSlug ? () => stateService.getStates(workspaceSlug as string, issue.project) : null ); + const states = getStatesList(stateGroups ?? {}); return ( void; + parent: IIssueLabels | undefined; +}; + +export const LabelsListModal: React.FC = ({ isOpen, handleClose, parent }) => { + const [query, setQuery] = useState(""); + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { data: issueLabels, mutate } = useSWR( + workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, + workspaceSlug && projectId + ? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string) + : null + ); + + const filteredLabels: IIssueLabels[] = + query === "" + ? issueLabels ?? [] + : issueLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase())) ?? []; + + const handleModalClose = () => { + handleClose(); + setQuery(""); + }; + + const addChildLabel = async (label: IIssueLabels) => { + if (!workspaceSlug || !projectId) return; + + mutate( + (prevData) => + prevData?.map((l) => { + if (l.id === label.id) return { ...l, parent: parent?.id ?? "" }; + + return l; + }), + false + ); + + await issuesService + .patchIssueLabel(workspaceSlug as string, projectId as string, label.id, { + parent: parent?.id ?? "", + }) + .then((res) => { + console.log(res); + mutate(); + }); + }; + + return ( + setQuery("")} appear> + + +
+ + +
+ + + +
+
+ + + {filteredLabels.length > 0 && ( + <> +
  • + {query === "" && ( +

    + Labels +

    + )} +
      + {filteredLabels.map((label) => { + const children = issueLabels?.filter((l) => l.parent === label.id); + + if ( + (label.parent === "" || label.parent === null) && // issue does not have any other parent + label.id !== parent?.id && // issue is not itself + children?.length === 0 // issue doesn't have any othe children + ) + return ( + + `flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 ${ + active ? "bg-gray-900 bg-opacity-5 text-gray-900" : "" + }` + } + onClick={() => { + addChildLabel(label); + handleClose(); + }} + > + + {label.name} + + ); + })} +
    +
  • + + )} +
    + + {query !== "" && filteredLabels.length === 0 && ( +
    +
    + )} +
    +
    +
    +
    +
    +
    + ); +}; diff --git a/apps/app/components/labels/single-label.tsx b/apps/app/components/labels/single-label.tsx new file mode 100644 index 000000000..eb721a8ac --- /dev/null +++ b/apps/app/components/labels/single-label.tsx @@ -0,0 +1,171 @@ +import React, { useState } from "react"; + +import { useRouter } from "next/router"; + +import { mutate } from "swr"; + +// headless ui +import { Disclosure, Transition } from "@headlessui/react"; +// services +import issuesService from "services/issues.service"; +// components +import { LabelsListModal } from "components/labels"; +// ui +import { CustomMenu } from "components/ui"; +// icons +import { ChevronDownIcon } from "@heroicons/react/24/outline"; +// types +import { IIssueLabels } from "types"; +// fetch-keys +import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; + +type Props = { + label: IIssueLabels; + issueLabels: IIssueLabels[]; + editLabel: (label: IIssueLabels) => void; + handleLabelDelete: (labelId: string) => void; +}; + +export const SingleLabel: React.FC = ({ + label, + issueLabels, + editLabel, + handleLabelDelete, +}) => { + const [labelsListModal, setLabelsListModal] = useState(false); + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const children = issueLabels?.filter((l) => l.parent === label.id); + + const removeFromGroup = (label: IIssueLabels) => { + if (!workspaceSlug || !projectId) return; + + mutate( + PROJECT_ISSUE_LABELS(projectId as string), + (prevData) => + prevData?.map((l) => { + if (l.id === label.id) return { ...l, parent: null }; + + return l; + }), + false + ); + + issuesService + .patchIssueLabel(workspaceSlug as string, projectId as string, label.id, { + parent: null, + }) + .then((res) => { + mutate(PROJECT_ISSUE_LABELS(projectId as string)); + }); + }; + + return ( + <> + setLabelsListModal(false)} + parent={label} + /> + {children && children.length === 0 ? ( + label.parent === "" || !label.parent ? ( +
    +
    +
    + +
    {label.name}
    +
    + + setLabelsListModal(true)}> + Convert to group + + editLabel(label)}>Edit + handleLabelDelete(label.id)}> + Delete + + +
    +
    + ) : null + ) : ( + + {({ open }) => ( + <> +
    + +
    + + + +
    {label.name}
    +
    +
    + + setLabelsListModal(true)}> + Add more labels + + editLabel(label)}>Edit + handleLabelDelete(label.id)}> + Delete + + +
    + + +
    + {children.map((child) => ( +
    +
    + + {child.name} +
    +
    + + removeFromGroup(child)}> + Remove from group + + editLabel(child)}> + Edit + + handleLabelDelete(child.id)}> + Delete + + +
    +
    + ))} +
    +
    +
    + + )} +
    + )} + + ); +}; diff --git a/apps/app/components/project/settings/single-label.tsx b/apps/app/components/project/settings/single-label.tsx deleted file mode 100644 index c58fb608f..000000000 --- a/apps/app/components/project/settings/single-label.tsx +++ /dev/null @@ -1,151 +0,0 @@ -// react -import React, { useState } from "react"; -// react-hook-form -import { Controller, useForm } from "react-hook-form"; -// headless ui -import { Popover, Transition } from "@headlessui/react"; -// ui -import { PencilIcon, RectangleGroupIcon } from "@heroicons/react/24/outline"; -import { TwitterPicker } from "react-color"; -import { Button, CustomMenu, Input } from "components/ui"; -// icons -// types -import { IIssueLabels } from "types"; - -type Props = { - label: IIssueLabels; - issueLabels: IIssueLabels[]; - editLabel: (label: IIssueLabels) => void; - handleLabelDelete: (labelId: string) => void; -}; - -const defaultValues: Partial = { - name: "", - color: "#ff0000", -}; - -const SingleLabel: React.FC = ({ label, issueLabels, editLabel, handleLabelDelete }) => { - const [newLabelForm, setNewLabelForm] = useState(false); - - const { - register, - formState: { errors, isSubmitting }, - watch, - control, - } = useForm({ defaultValues }); - - const children = issueLabels?.filter((l) => l.parent === label.id); - - return ( - <> - {children && children.length === 0 ? ( -
    -
    -
    - -
    {label.name}
    -
    - - {/* Convert to group */} - editLabel(label)}>Edit - handleLabelDelete(label.id)}> - Delete - - -
    -
    -
    - - {({ open }) => ( - <> - - {watch("color") && watch("color") !== "" && ( - - )} - - - - - ( - onChange(value.hex)} - /> - )} - /> - - - - )} - -
    -
    - -
    - - -
    -
    - ) : ( -
    -

    - - This is the label group title -

    -
    -
    -
    -
    - This is the label title -
    - -
    -
    -
    - )} - - ); -}; - -export default SingleLabel; diff --git a/apps/app/helpers/state.helper.ts b/apps/app/helpers/state.helper.ts new file mode 100644 index 000000000..a852157a9 --- /dev/null +++ b/apps/app/helpers/state.helper.ts @@ -0,0 +1,21 @@ +// types +import { IState, StateResponse } from "types"; + +export const orderStateGroups = (unorderedStateGroups: StateResponse) => + Object.assign( + { backlog: [], unstarted: [], started: [], completed: [], cancelled: [] }, + unorderedStateGroups + ); + +export const getStatesList = (stateGroups: any): IState[] => { + // order the unordered state groups first + const orderedStateGroups = Object.assign( + { backlog: [], unstarted: [], started: [], completed: [], cancelled: [] }, + stateGroups + ); + + // extract states from the groups and return them + return Object.keys(orderedStateGroups) + .map((group) => [...orderedStateGroups[group].map((state: IState) => state)]) + .flat(); +}; diff --git a/apps/app/hooks/use-issue-view.tsx b/apps/app/hooks/use-issue-view.tsx index 671ca36ca..4256869fb 100644 --- a/apps/app/hooks/use-issue-view.tsx +++ b/apps/app/hooks/use-issue-view.tsx @@ -10,6 +10,7 @@ import stateService from "services/state.service"; import { issueViewContext } from "contexts/issue-view.context"; // helpers import { groupBy, orderArrayBy } from "helpers/array.helper"; +import { getStatesList } from "helpers/state.helper"; // types import { IIssue, IState } from "types"; // fetch-keys @@ -35,12 +36,13 @@ const useIssueView = (projectIssues: IIssue[]) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { data: states } = useSWR( + const { data: stateGroups } = useSWR( workspaceSlug && projectId ? STATE_LIST(projectId as string) : null, workspaceSlug && projectId ? () => stateService.getStates(workspaceSlug as string, projectId as string) : null ); + const states = getStatesList(stateGroups ?? {}); let groupedByIssues: { [key: string]: IIssue[]; diff --git a/apps/app/hooks/use-my-issues-filter.tsx b/apps/app/hooks/use-my-issues-filter.tsx index 435e78bec..80613d5e3 100644 --- a/apps/app/hooks/use-my-issues-filter.tsx +++ b/apps/app/hooks/use-my-issues-filter.tsx @@ -8,6 +8,7 @@ import userService from "services/user.service"; import useUser from "hooks/use-user"; // helpers import { groupBy } from "helpers/array.helper"; +import { getStatesList } from "helpers/state.helper"; // types import { Properties, NestedKeyOf, IIssue } from "types"; // fetch-keys @@ -36,12 +37,13 @@ const useMyIssuesProperties = (issues?: IIssue[]) => { const { user } = useUser(); - const { data: states } = useSWR( + const { data: stateGroups } = useSWR( workspaceSlug && projectId ? STATE_LIST(projectId as string) : null, workspaceSlug && projectId ? () => stateService.getStates(workspaceSlug as string, projectId as string) : null ); + const states = getStatesList(stateGroups ?? {}); useEffect(() => { if (!user) return; diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx index b82f76d9e..41ca87343 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx @@ -19,14 +19,14 @@ import { requiredAdmin } from "lib/auth"; // layouts import AppLayout from "layouts/app-layout"; // components -import SingleLabel from "components/project/settings/single-label"; +import { SingleLabel } from "components/labels"; // ui import { Button, Input, Loader } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons import { PlusIcon } from "@heroicons/react/24/outline"; // types -import { IIssueLabels } from "types"; +import { IIssueLabels, UserAuth } from "types"; import type { NextPageContext, NextPage } from "next"; // fetch-keys import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS, WORKSPACE_DETAILS } from "constants/fetch-keys"; @@ -36,28 +36,15 @@ const defaultValues: Partial = { color: "#ff0000", }; -type TLabelSettingsProps = { - isMember: boolean; - isOwner: boolean; - isViewer: boolean; - isGuest: boolean; -}; - -const LabelsSettings: NextPage = (props) => { +const LabelsSettings: NextPage = (props) => { const { isMember, isOwner, isViewer, isGuest } = props; - const [newLabelForm, setNewLabelForm] = useState(false); + const [labelForm, setLabelForm] = useState(false); const [isUpdating, setIsUpdating] = useState(false); const [labelIdForUpdate, setLabelIdForUpdate] = useState(null); - const { - query: { workspaceSlug, projectId }, - } = useRouter(); - - const { data: activeWorkspace } = useSWR( - workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null, - () => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null) - ); + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; const { data: projectDetails } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, @@ -66,6 +53,13 @@ const LabelsSettings: NextPage = (props) => { : null ); + const { data: issueLabels, mutate } = useSWR( + workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, + workspaceSlug && projectId + ? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string) + : null + ); + const { register, handleSubmit, @@ -76,37 +70,37 @@ const LabelsSettings: NextPage = (props) => { watch, } = useForm({ defaultValues }); - const { data: issueLabels, mutate } = useSWR( - workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, - workspaceSlug && projectId - ? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string) - : null - ); - - const handleNewLabel: SubmitHandler = async (formData) => { - if (!activeWorkspace || !projectDetails || isSubmitting) return; - await issuesService - .createIssueLabel(activeWorkspace.slug, projectDetails.id, formData) - .then((res) => { - reset(defaultValues); - mutate((prevData) => [...(prevData ?? []), res], false); - setNewLabelForm(false); - }); + const newLabel = () => { + reset(); + setIsUpdating(false); + setLabelForm(true); }; const editLabel = (label: IIssueLabels) => { - setNewLabelForm(true); + setLabelForm(true); setValue("color", label.color); setValue("name", label.name); setIsUpdating(true); setLabelIdForUpdate(label.id); }; - const handleLabelUpdate: SubmitHandler = async (formData) => { - if (!activeWorkspace || !projectDetails || isSubmitting) return; + const handleLabelCreate: SubmitHandler = async (formData) => { + if (!workspaceSlug || !projectDetails || isSubmitting) return; await issuesService - .patchIssueLabel(activeWorkspace.slug, projectDetails.id, labelIdForUpdate ?? "", formData) + .createIssueLabel(workspaceSlug as string, projectDetails.id, formData) + .then((res) => { + mutate((prevData) => [res, ...(prevData ?? [])], false); + reset(defaultValues); + setLabelForm(false); + }); + }; + + const handleLabelUpdate: SubmitHandler = async (formData) => { + if (!workspaceSlug || !projectDetails || isSubmitting) return; + + await issuesService + .patchIssueLabel(workspaceSlug as string, projectDetails.id, labelIdForUpdate ?? "", formData) .then((res) => { console.log(res); reset(defaultValues); @@ -115,15 +109,15 @@ const LabelsSettings: NextPage = (props) => { prevData?.map((p) => (p.id === labelIdForUpdate ? { ...p, ...formData } : p)), false ); - setNewLabelForm(false); + setLabelForm(false); }); }; const handleLabelDelete = (labelId: string) => { - if (activeWorkspace && projectDetails) { + if (workspaceSlug && projectDetails) { mutate((prevData) => prevData?.filter((p) => p.id !== labelId), false); issuesService - .deleteIssueLabel(activeWorkspace.slug, projectDetails.id, labelId) + .deleteIssueLabel(workspaceSlug as string, projectDetails.id, labelId) .then((res) => { console.log(res); }) @@ -154,11 +148,7 @@ const LabelsSettings: NextPage = (props) => {

    Manage labels

    - @@ -166,7 +156,7 @@ const LabelsSettings: NextPage = (props) => {
    @@ -227,7 +217,14 @@ const LabelsSettings: NextPage = (props) => { error={errors.name} />
    - {isUpdating ? ( @@ -239,7 +236,11 @@ const LabelsSettings: NextPage = (props) => { {isSubmitting ? "Updating" : "Update"} ) : ( - )} diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx index 52a095b2a..880e44967 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx @@ -1,10 +1,9 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -import useSWR from "swr"; -import { PencilSquareIcon, PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; -import { IState } from "types"; +import useSWR from "swr"; + // services import stateService from "services/state.service"; import projectService from "services/project.service"; @@ -17,22 +16,18 @@ import { CreateUpdateStateInline, DeleteStateModal, StateGroup } from "component // ui import { Loader } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; +// icons +import { PencilSquareIcon, PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; // helpers import { addSpaceIfCamelCase } from "helpers/string.helper"; -import { groupBy } from "helpers/array.helper"; +import { getStatesList, orderStateGroups } from "helpers/state.helper"; // types +import { UserAuth } from "types"; import type { NextPage, NextPageContext } from "next"; // fetch-keys import { PROJECT_DETAILS, STATE_LIST } from "constants/fetch-keys"; -type TStateSettingsProps = { - isMember: boolean; - isOwner: boolean; - isViewer: boolean; - isGuest: boolean; -}; - -const StatesSettings: NextPage = (props) => { +const StatesSettings: NextPage = (props) => { const { isMember, isOwner, isViewer, isGuest } = props; const [activeGroup, setActiveGroup] = useState(null); @@ -56,16 +51,15 @@ const StatesSettings: NextPage = (props) => { ? () => stateService.getStates(workspaceSlug as string, projectId as string) : null ); + const orderedStateGroups = orderStateGroups(states ?? {}); - const groupedStates: { - [key: string]: IState[]; - } = groupBy(states ?? [], "group"); + const statesList = getStatesList(orderStateGroups ?? {}); return ( <> state.id === selectDeleteState) ?? null} + data={statesList?.find((state) => state.id === selectDeleteState) ?? null} onClose={() => setSelectDeleteState(null)} /> = (props) => {
    {states && projectDetails ? ( - Object.keys(groupedStates).map((key) => ( -
    -
    -

    {key} states

    - -
    -
    - {key === activeGroup && ( - { - setActiveGroup(null); - setSelectedState(null); - }} - workspaceSlug={workspaceSlug as string} - data={null} - selectedGroup={key as keyof StateGroup} - /> - )} - {groupedStates[key]?.map((state) => - state.id !== selectedState ? ( -
    { + if (orderedStateGroups[key].length !== 0) + return ( +
    +
    +

    {key} states

    + - -
    -
    - ) : ( -
    + + Add + +
    +
    + {key === activeGroup && ( { @@ -149,15 +106,60 @@ const StatesSettings: NextPage = (props) => { setSelectedState(null); }} workspaceSlug={workspaceSlug as string} - data={states?.find((state) => state.id === selectedState) ?? null} + data={null} selectedGroup={key as keyof StateGroup} /> -
    - ) - )} -
    -
    - )) + )} + {orderedStateGroups[key].map((state) => + state.id !== selectedState ? ( +
    +
    +
    +
    {addSpaceIfCamelCase(state.name)}
    +
    +
    + + +
    +
    + ) : ( +
    + { + setActiveGroup(null); + setSelectedState(null); + }} + workspaceSlug={workspaceSlug as string} + data={ + statesList?.find((state) => state.id === selectedState) ?? null + } + selectedGroup={key as keyof StateGroup} + /> +
    + ) + )} +
    +
    + ); + }) ) : ( diff --git a/apps/app/services/state.service.ts b/apps/app/services/state.service.ts index 33571f95e..2940dfd1c 100644 --- a/apps/app/services/state.service.ts +++ b/apps/app/services/state.service.ts @@ -4,7 +4,7 @@ import APIService from "services/api.service"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; // types -import type { IState } from "types"; +import type { IState, StateResponse } from "types"; class ProjectStateServices extends APIService { constructor() { @@ -19,7 +19,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) => { diff --git a/apps/app/types/state.d.ts b/apps/app/types/state.d.ts index c6da87925..5f69ae49f 100644 --- a/apps/app/types/state.d.ts +++ b/apps/app/types/state.d.ts @@ -13,3 +13,7 @@ export interface IState { sequence: number; group: "backlog" | "unstarted" | "started" | "completed" | "cancelled"; } + +export interface StateResponse { + [key: string]: IState[]; +}