From f8ab0aa72bb63e1b2ded50c1672433045e572add Mon Sep 17 00:00:00 2001 From: Dakshesh Jain Date: Wed, 16 Aug 2023 16:53:39 +0530 Subject: [PATCH] dev: label store implementation --- apps/app/components/issues/select/label.tsx | 344 +++++++++--------- .../components/labels/create-label-modal.tsx | 322 ++++++++-------- .../labels/create-update-label-inline.tsx | 64 ++-- .../components/labels/delete-label-modal.tsx | 38 +- .../components/labels/labels-list-modal.tsx | 263 +++++++------ .../components/labels/single-label-group.tsx | 267 +++++++------- apps/app/components/labels/single-label.tsx | 8 +- .../projects/[projectId]/settings/labels.tsx | 45 +-- apps/app/store/label.ts | 158 ++++++++ apps/app/store/root.ts | 3 + apps/app/types/issues.d.ts | 14 + 11 files changed, 820 insertions(+), 706 deletions(-) create mode 100644 apps/app/store/label.ts diff --git a/apps/app/components/issues/select/label.tsx b/apps/app/components/issues/select/label.tsx index 6d7e2f391..5a333e69b 100644 --- a/apps/app/components/issues/select/label.tsx +++ b/apps/app/components/issues/select/label.tsx @@ -1,13 +1,12 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { useRouter } from "next/router"; -import useSWR from "swr"; +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; // headless ui import { Combobox, Transition } from "@headlessui/react"; -// services -import issuesServices from "services/issues.service"; // ui import { IssueLabelsList } from "components/ui"; // icons @@ -19,9 +18,7 @@ import { TagIcon, } from "@heroicons/react/24/outline"; // types -import type { IIssueLabels } from "types"; -// fetch-keys -import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; +import type { LabelLite } from "types"; type Props = { setIsOpen: React.Dispatch>; @@ -30,179 +27,180 @@ type Props = { projectId: string; }; -export const IssueLabelSelect: React.FC = ({ setIsOpen, value, onChange, projectId }) => { - // states - const [query, setQuery] = useState(""); +export const IssueLabelSelect: React.FC = observer( + ({ setIsOpen, value, onChange, projectId }) => { + // states + const [query, setQuery] = useState(""); - const router = useRouter(); - const { workspaceSlug } = router.query; + const router = useRouter(); + const { workspaceSlug } = router.query; - const { data: issueLabels } = useSWR( - projectId ? PROJECT_ISSUE_LABELS(projectId) : null, - workspaceSlug && projectId - ? () => issuesServices.getIssueLabels(workspaceSlug as string, projectId) - : null - ); + const { label: labelStore } = useMobxStore(); + const { isLabelsLoading: isLoading, labels, loadLabels, getLabelChildren } = labelStore; - const filteredOptions = - query === "" - ? issueLabels - : issueLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase())); + useEffect(() => { + if (workspaceSlug && projectId) loadLabels(workspaceSlug.toString(), projectId); + }, [workspaceSlug, projectId, loadLabels]); - return ( - onChange(val)} - className="relative flex-shrink-0" - multiple - > - {({ open }: any) => ( - <> - - {value && value.length > 0 ? ( - - issueLabels?.find((l) => l.id === v)?.color) ?? []} - length={3} - showLength={true} - /> - - ) : ( - - - Label - - )} - + const filteredOptions: LabelLite[] = labels?.filter((l) => + l.name.toLowerCase().includes(query.toLowerCase()) + ); - - onChange(val)} + className="relative flex-shrink-0" + multiple + > + {({ open }: any) => ( + <> + + {value && value.length > 0 ? ( + + labels?.find((l) => l.id === v)?.color) ?? []} + length={3} + showLength={true} + /> + + ) : ( + + + Label + + )} + + + -
- - setQuery(event.target.value)} - placeholder="Search for label..." - displayValue={(assigned: any) => assigned?.name} - /> -
-
- {issueLabels && filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((label) => { - const children = issueLabels?.filter((l) => l.parent === label.id); + +
+ + setQuery(event.target.value)} + placeholder="Search for label..." + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {!isLoading && filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((label) => { + const children = getLabelChildren(label.id); - if (children.length === 0) { - if (!label.parent) - return ( - - `${ - active ? "bg-custom-background-80" : "" - } group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-custom-text-200` - } - value={label.id} - > - {({ selected }) => ( -
-
- - {label.name} -
-
- -
-
- )} -
- ); - } else - return ( -
-
- {label.name} -
-
- {children.map((child) => ( - - `${ - active ? "bg-custom-background-80" : "" - } group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-custom-text-200` - } - value={child.id} - > - {({ selected }) => ( -
-
- - {child.name} -
-
- -
+ if (children.length === 0) { + if (!label.parent) + return ( + + `${ + active ? "bg-custom-background-80" : "" + } group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-custom-text-200` + } + value={label.id} + > + {({ selected }) => ( +
+
+ + {label.name}
- )} - - ))} +
+ +
+
+ )} +
+ ); + } else + return ( +
+
+ {label.name} +
+
+ {children.map((child) => ( + + `${ + active ? "bg-custom-background-80" : "" + } group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-custom-text-200` + } + value={child.id} + > + {({ selected }) => ( +
+
+ + {child.name} +
+
+ +
+
+ )} +
+ ))} +
-
- ); - }) + ); + }) + ) : ( +

No labels found

+ ) ) : ( -

No labels found

- ) - ) : ( -

Loading...

- )} - -
- - - - )} - - ); -}; +

Loading...

+ )} + +
+ + + + )} + + ); + } +); diff --git a/apps/app/components/labels/create-label-modal.tsx b/apps/app/components/labels/create-label-modal.tsx index 190b9e832..f72db7876 100644 --- a/apps/app/components/labels/create-label-modal.tsx +++ b/apps/app/components/labels/create-label-modal.tsx @@ -2,7 +2,9 @@ import React, { useEffect } from "react"; import { useRouter } from "next/router"; -import { mutate } from "swr"; +// mobx +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; // react-hook-form import { Controller, useForm } from "react-hook-form"; @@ -10,16 +12,13 @@ import { Controller, useForm } from "react-hook-form"; import { TwitterPicker } from "react-color"; // headless ui import { Dialog, Popover, Transition } from "@headlessui/react"; -// services -import issuesService from "services/issues.service"; // ui import { Input, PrimaryButton, SecondaryButton } from "components/ui"; // icons import { ChevronDownIcon } from "@heroicons/react/24/outline"; // types -import type { ICurrentUserResponse, IIssueLabels, IState } from "types"; +import type { ICurrentUserResponse, IIssueLabels, LabelForm } from "types"; // constants -import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label"; // types @@ -31,179 +30,172 @@ type Props = { user: ICurrentUserResponse | undefined; }; -const defaultValues: Partial = { +const defaultValues: Partial = { name: "", color: "rgb(var(--color-text-200))", }; -export const CreateLabelModal: React.FC = ({ - isOpen, - projectId, - handleClose, - user, - onSuccess, -}) => { - const router = useRouter(); - const { workspaceSlug } = router.query; +export const CreateLabelModal: React.FC = observer( + ({ isOpen, projectId, handleClose, user, onSuccess }) => { + const router = useRouter(); + const { workspaceSlug } = router.query; - const { - register, - formState: { errors, isSubmitting }, - handleSubmit, - watch, - control, - reset, - setValue, - } = useForm({ - defaultValues, - }); + const { label: labelStore } = useMobxStore(); + const { createLabel } = labelStore; - useEffect(() => { - if (isOpen) setValue("color", getRandomLabelColor()); - }, [setValue, isOpen]); + const { + register, + formState: { errors, isSubmitting }, + handleSubmit, + watch, + control, + reset, + setValue, + } = useForm({ + defaultValues, + }); - const onClose = () => { - handleClose(); - reset(defaultValues); - }; + useEffect(() => { + if (isOpen) setValue("color", getRandomLabelColor()); + }, [setValue, isOpen]); - const onSubmit = async (formData: IIssueLabels) => { - if (!workspaceSlug) return; + const onClose = () => { + handleClose(); + reset(defaultValues); + }; - await issuesService - .createIssueLabel(workspaceSlug as string, projectId as string, formData, user) - .then((res) => { - mutate( - PROJECT_ISSUE_LABELS(projectId), - (prevData) => [res, ...(prevData ?? [])], - false - ); - onClose(); - if (onSuccess) onSuccess(res); - }) - .catch((error) => { - console.log(error); - }); - }; + const onSubmit = async (formData: LabelForm) => { + if (!workspaceSlug || !user) return; - return ( - - - -
- + await createLabel(workspaceSlug.toString(), projectId as string, formData, user) + .then((response: any) => { + onClose(); + if (onSuccess) onSuccess(response); + }) + .catch((error) => { + console.log(error); + }); + }; -
-
- - -
-
- - Create Label - -
- - {({ open, close }) => ( - <> - - {watch("color") && watch("color") !== "" && ( - - )} - + + +
+ + +
+
+ + + +
+ + Create Label + +
+ + {({ open, close }) => ( + <> + - - - - ( - { - onChange(value.hex); - close(); - }} - /> - )} + > + {watch("color") && watch("color") !== "" && ( + + )} + - - - )} - -
- + + + + + ( + { + onChange(value.hex); + close(); + }} + /> + )} + /> + + + + )} + +
+ +
-
-
- Cancel - - {isSubmitting ? "Creating Label..." : "Create Label"} - -
- -
-
+
+ Cancel + + {isSubmitting ? "Creating Label..." : "Create Label"} + +
+ + + +
-
-
- - ); -}; +
+
+ ); + } +); diff --git a/apps/app/components/labels/create-update-label-inline.tsx b/apps/app/components/labels/create-update-label-inline.tsx index 6306d14ca..6bc61bc0b 100644 --- a/apps/app/components/labels/create-update-label-inline.tsx +++ b/apps/app/components/labels/create-update-label-inline.tsx @@ -2,7 +2,9 @@ import React, { forwardRef, useEffect } from "react"; import { useRouter } from "next/router"; -import { mutate } from "swr"; +// mobx +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; // react-hook-form import { Controller, SubmitHandler, useForm } from "react-hook-form"; @@ -12,23 +14,20 @@ import useUserAuth from "hooks/use-user-auth"; import { TwitterPicker } from "react-color"; // headless ui import { Popover, Transition } from "@headlessui/react"; -// services -import issuesService from "services/issues.service"; // ui import { Input, PrimaryButton, SecondaryButton } from "components/ui"; // icons import { ChevronDownIcon } from "@heroicons/react/24/outline"; // types -import { IIssueLabels } from "types"; +import { IIssueLabels, LabelLite } from "types"; // fetch-keys -import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label"; type Props = { labelForm: boolean; setLabelForm: React.Dispatch>; isUpdating: boolean; - labelToUpdate: IIssueLabels | null; + labelToUpdate: LabelLite | null; onClose?: () => void; }; @@ -37,13 +36,16 @@ const defaultValues: Partial = { color: "rgb(var(--color-text-200))", }; -export const CreateUpdateLabelInline = forwardRef( - function CreateUpdateLabelInline(props, ref) { +export const CreateUpdateLabelInline = observer( + forwardRef(function CreateUpdateLabelInline(props, ref) { const { labelForm, setLabelForm, isUpdating, labelToUpdate, onClose } = props; const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { label: labelStore } = useMobxStore(); + const { createLabel, updateLabel } = labelStore; + const { user } = useUserAuth(); const { @@ -65,41 +67,27 @@ export const CreateUpdateLabelInline = forwardRef( }; const handleLabelCreate: SubmitHandler = async (formData) => { - if (!workspaceSlug || !projectId || isSubmitting) return; + if (!workspaceSlug || !projectId || isSubmitting || !user) return; - await issuesService - .createIssueLabel(workspaceSlug as string, projectId as string, formData, user) - .then((res) => { - mutate( - PROJECT_ISSUE_LABELS(projectId as string), - (prevData) => [res, ...(prevData ?? [])], - false - ); + await createLabel(workspaceSlug.toString(), projectId.toString(), formData, user).finally( + () => { handleClose(); - }); + } + ); }; const handleLabelUpdate: SubmitHandler = async (formData) => { - if (!workspaceSlug || !projectId || isSubmitting) return; + if (!workspaceSlug || !projectId || isSubmitting || !user) return; - await issuesService - .patchIssueLabel( - workspaceSlug as string, - projectId as string, - labelToUpdate?.id ?? "", - formData, - user - ) - .then(() => { - reset(defaultValues); - mutate( - PROJECT_ISSUE_LABELS(projectId as string), - (prevData) => - prevData?.map((p) => (p.id === labelToUpdate?.id ? { ...p, ...formData } : p)), - false - ); - handleClose(); - }); + await updateLabel( + workspaceSlug.toString(), + projectId.toString(), + labelToUpdate?.id ?? "", + formData, + user + ).finally(() => { + handleClose(); + }); }; useEffect(() => { @@ -212,5 +200,5 @@ export const CreateUpdateLabelInline = forwardRef( )}
); - } + }) ); diff --git a/apps/app/components/labels/delete-label-modal.tsx b/apps/app/components/labels/delete-label-modal.tsx index 8535539f9..f6bc7861e 100644 --- a/apps/app/components/labels/delete-label-modal.tsx +++ b/apps/app/components/labels/delete-label-modal.tsx @@ -2,36 +2,37 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -import { mutate } from "swr"; +// mobx +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; // headless ui import { Dialog, Transition } from "@headlessui/react"; // icons import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; -// services -import issuesService from "services/issues.service"; // hooks import useToast from "hooks/use-toast"; // ui import { DangerButton, SecondaryButton } from "components/ui"; // types -import type { ICurrentUserResponse, IIssueLabels } from "types"; -// fetch-keys -import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; +import type { ICurrentUserResponse, LabelLite } from "types"; type Props = { isOpen: boolean; onClose: () => void; - data: IIssueLabels | null; + data: LabelLite | null; user: ICurrentUserResponse | undefined; }; -export const DeleteLabelModal: React.FC = ({ isOpen, onClose, data, user }) => { +export const DeleteLabelModal: React.FC = observer(({ isOpen, onClose, data, user }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { label: labelStore } = useMobxStore(); + const { deleteLabel } = labelStore; + const { setToastAlert } = useToast(); const handleClose = () => { @@ -40,28 +41,21 @@ export const DeleteLabelModal: React.FC = ({ isOpen, onClose, data, user }; const handleDeletion = async () => { - if (!workspaceSlug || !projectId || !data) return; + if (!workspaceSlug || !projectId || !data || !user) return; setIsDeleteLoading(true); - mutate( - PROJECT_ISSUE_LABELS(projectId.toString()), - (prevData) => (prevData ?? []).filter((p) => p.id !== data.id), - false - ); - - await issuesService - .deleteIssueLabel(workspaceSlug.toString(), projectId.toString(), data.id, user) - .then(() => handleClose()) + await deleteLabel(workspaceSlug.toString(), projectId.toString(), data.id, user) .catch(() => { - setIsDeleteLoading(false); - - mutate(PROJECT_ISSUE_LABELS(projectId.toString())); setToastAlert({ type: "error", title: "Error!", message: "Label could not be deleted. Please try again.", }); + }) + .finally(() => { + handleClose(); + setIsDeleteLoading(false); }); }; @@ -130,4 +124,4 @@ export const DeleteLabelModal: React.FC = ({ isOpen, onClose, data, user ); -}; +}); diff --git a/apps/app/components/labels/labels-list-modal.tsx b/apps/app/components/labels/labels-list-modal.tsx index 9042bde3a..e255d3e32 100644 --- a/apps/app/components/labels/labels-list-modal.tsx +++ b/apps/app/components/labels/labels-list-modal.tsx @@ -2,178 +2,159 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -import useSWR, { mutate } from "swr"; +// mobx +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; // headless ui import { Combobox, Dialog, Transition } from "@headlessui/react"; // icons import { RectangleStackIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; -// services -import issuesService from "services/issues.service"; // types -import { ICurrentUserResponse, IIssueLabels } from "types"; -// constants -import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; +import { ICurrentUserResponse, LabelLite } from "types"; type Props = { isOpen: boolean; handleClose: () => void; - parent: IIssueLabels | undefined; + parent: LabelLite | undefined; user: ICurrentUserResponse | undefined; }; -export const LabelsListModal: React.FC = ({ isOpen, handleClose, parent, user }) => { - const [query, setQuery] = useState(""); +export const LabelsListModal: React.FC = observer( + ({ isOpen, handleClose, parent, user }) => { + const [query, setQuery] = useState(""); - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + 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 { label: labelStore } = useMobxStore(); + const { updateLabel, getLabelChildren, getFilteredLabels } = labelStore; - const filteredLabels: IIssueLabels[] = - query === "" - ? issueLabels ?? [] - : issueLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase())) ?? []; + const filteredLabels = getFilteredLabels(query); - const handleModalClose = () => { - handleClose(); - setQuery(""); - }; + const handleModalClose = () => { + handleClose(); + setQuery(""); + }; - const addChildLabel = async (label: IIssueLabels) => { - if (!workspaceSlug || !projectId) return; + const addChildLabel = async (label: LabelLite) => { + if (!workspaceSlug || !projectId || !user) return; - mutate( - (prevData: any) => - prevData?.map((l: any) => { - if (l.id === label.id) return { ...l, parent: parent?.id ?? "" }; - - return l; - }), - false - ); - - await issuesService - .patchIssueLabel( - workspaceSlug as string, - projectId as string, + updateLabel( + workspaceSlug.toString(), + projectId.toString(), label.id, { parent: parent?.id ?? "", }, user - ) - .then(() => mutate()); - }; + ); + }; - return ( - setQuery("")} appear> - - -
- - -
+ 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 w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${ - active ? "bg-custom-background-80 text-custom-text-100" : "" - }` - } - onClick={() => { - addChildLabel(label); - handleClose(); - }} - > - - {label.name} - - ); - })} -
    -
  • - )} -
    - - {query !== "" && filteredLabels.length === 0 && ( -
    - + + + +
    +
    - )} -
    -
    -
    -
    -
    -
    - ); -}; + + + {filteredLabels.length > 0 && ( +
  • + {query === "" && ( +

    + Labels +

    + )} +
      + {filteredLabels.map((label) => { + const children = getLabelChildren(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 w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${ + active ? "bg-custom-background-80 text-custom-text-100" : "" + }` + } + onClick={() => { + addChildLabel(label); + handleClose(); + }} + > + + {label.name} + + ); + })} +
    +
  • + )} +
    + + {query !== "" && filteredLabels.length === 0 && ( +
    +
    + )} + + + +
    +
    +
    + ); + } +); diff --git a/apps/app/components/labels/single-label-group.tsx b/apps/app/components/labels/single-label-group.tsx index d489ce057..2d8eecd2b 100644 --- a/apps/app/components/labels/single-label-group.tsx +++ b/apps/app/components/labels/single-label-group.tsx @@ -2,12 +2,12 @@ import React from "react"; import { useRouter } from "next/router"; -import { mutate } from "swr"; +// mobx +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; // headless ui import { Disclosure, Transition } from "@headlessui/react"; -// services -import issuesService from "services/issues.service"; // ui import { CustomMenu } from "components/ui"; // icons @@ -20,159 +20,142 @@ import { TrashIcon, } from "@heroicons/react/24/outline"; // types -import { ICurrentUserResponse, IIssueLabels } from "types"; -// fetch-keys -import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; +import { ICurrentUserResponse, LabelLite } from "types"; type Props = { - label: IIssueLabels; - labelChildren: IIssueLabels[]; - addLabelToGroup: (parentLabel: IIssueLabels) => void; - editLabel: (label: IIssueLabels) => void; - handleLabelDelete: () => void; + label: LabelLite; + labelChildren: LabelLite[]; + addLabelToGroup: (parentLabel: LabelLite) => void; + editLabel: (label: LabelLite) => void; + handleLabelDelete: (label: LabelLite) => void; user: ICurrentUserResponse | undefined; }; -export const SingleLabelGroup: React.FC = ({ - label, - labelChildren, - addLabelToGroup, - editLabel, - handleLabelDelete, - user, -}) => { - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; +export const SingleLabelGroup: React.FC = observer( + ({ label, labelChildren, addLabelToGroup, editLabel, handleLabelDelete, user }) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; - const removeFromGroup = (label: IIssueLabels) => { - if (!workspaceSlug || !projectId) return; + const { label: labelStore } = useMobxStore(); + const { updateLabel } = labelStore; - mutate( - PROJECT_ISSUE_LABELS(projectId as string), - (prevData) => - prevData?.map((l) => { - if (l.id === label.id) return { ...l, parent: null }; + const removeFromGroup = (label: LabelLite) => { + if (!workspaceSlug || !projectId || !user) return; - return l; - }), - false - ); - - issuesService - .patchIssueLabel( - workspaceSlug as string, - projectId as string, + updateLabel( + workspaceSlug.toString(), + projectId.toString(), label.id, { parent: null, }, user - ) - .then(() => { - mutate(PROJECT_ISSUE_LABELS(projectId as string)); - }); - }; + ); + }; - return ( - - {({ open }) => ( - <> -
    -
    - - - -
    {label.name}
    -
    -
    - - addLabelToGroup(label)}> - - - Add more labels - - - editLabel(label)}> - - - Edit label - - - - - - Delete label - - - - + return ( + + {({ open }) => ( + <> +
    +
    - + - -
    -
    - - -
    - {labelChildren.map((child) => ( -
    -
    - - {child.name} -
    -
    - - removeFromGroup(child)}> - - - Remove from group - - - editLabel(child)}> - - - Edit label - - - - - - Delete label - - - -
    -
    - ))} +
    {label.name}
    -
    -
    - - )} -
    - ); -}; +
    + + addLabelToGroup(label)}> + + + Add more labels + + + editLabel(label)}> + + + Edit label + + + handleLabelDelete(label)}> + + + Delete label + + + + + + + + +
    +
    + + +
    + {labelChildren.map((child) => ( +
    +
    + + {child.name} +
    +
    + + removeFromGroup(child)}> + + + Remove from group + + + editLabel(child)}> + + + Edit label + + + handleLabelDelete(child)}> + + + Delete label + + + +
    +
    + ))} +
    +
    +
    + + )} + + ); + } +); diff --git a/apps/app/components/labels/single-label.tsx b/apps/app/components/labels/single-label.tsx index 15fcf896d..f376c29d7 100644 --- a/apps/app/components/labels/single-label.tsx +++ b/apps/app/components/labels/single-label.tsx @@ -3,14 +3,14 @@ import React from "react"; // ui import { CustomMenu } from "components/ui"; // types -import { IIssueLabels } from "types"; +import { LabelLite } from "types"; //icons import { RectangleGroupIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; type Props = { - label: IIssueLabels; - addLabelToGroup: (parentLabel: IIssueLabels) => void; - editLabel: (label: IIssueLabels) => void; + label: LabelLite; + addLabelToGroup: (parentLabel: LabelLite) => void; + editLabel: (label: LabelLite) => void; handleLabelDelete: () => void; }; diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx index dc845da68..f7ee6ee81 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx @@ -1,14 +1,17 @@ -import React, { useState, useRef } from "react"; +import React, { useState, useRef, useEffect } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; +// mobx +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; + // hooks import useUserAuth from "hooks/use-user-auth"; // services import projectService from "services/project.service"; -import issuesService from "services/issues.service"; // layouts import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; // components @@ -28,10 +31,10 @@ import { PlusIcon } from "@heroicons/react/24/outline"; // images import emptyLabel from "public/empty-state/label.svg"; // types -import { IIssueLabels } from "types"; +import { LabelLite } from "types"; import type { NextPage } from "next"; // fetch-keys -import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; +import { PROJECT_DETAILS } from "constants/fetch-keys"; // helper import { truncateText } from "helpers/string.helper"; @@ -41,18 +44,21 @@ const LabelsSettings: NextPage = () => { // edit label const [isUpdating, setIsUpdating] = useState(false); - const [labelToUpdate, setLabelToUpdate] = useState(null); + const [labelToUpdate, setLabelToUpdate] = useState(null); // labels list modal const [labelsListModal, setLabelsListModal] = useState(false); - const [parentLabel, setParentLabel] = useState(undefined); + const [parentLabel, setParentLabel] = useState(undefined); // delete label - const [selectDeleteLabel, setSelectDeleteLabel] = useState(null); + const [selectDeleteLabel, setSelectDeleteLabel] = useState(null); const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { label } = useMobxStore(); + const { labels, isLabelsLoading: isLoading, getLabelChildren, loadLabels } = label; + const { user } = useUserAuth(); const scrollToRef = useRef(null); @@ -64,24 +70,21 @@ const LabelsSettings: NextPage = () => { : null ); - const { data: issueLabels } = useSWR( - workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, - workspaceSlug && projectId - ? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string) - : null - ); + useEffect(() => { + if (workspaceSlug && projectId) loadLabels(workspaceSlug.toString(), projectId.toString()); + }, [loadLabels, projectId, workspaceSlug]); const newLabel = () => { setIsUpdating(false); setLabelForm(true); }; - const addLabelToGroup = (parentLabel: IIssueLabels) => { + const addLabelToGroup = (parentLabel: LabelLite) => { setLabelsListModal(true); setParentLabel(parentLabel); }; - const editLabel = (label: IIssueLabels) => { + const editLabel = (label: LabelLite) => { setLabelForm(true); setIsUpdating(true); setLabelToUpdate(label); @@ -142,10 +145,10 @@ const LabelsSettings: NextPage = () => { /> )} <> - {issueLabels ? ( - issueLabels.length > 0 ? ( - issueLabels.map((label) => { - const children = issueLabels?.filter((l) => l.parent === label.id); + {!isLoading ? ( + labels.length > 0 ? ( + labels.map((label) => { + const children = getLabelChildren(label.id); if (children && children.length === 0) { if (!label.parent) @@ -176,7 +179,7 @@ const LabelsSettings: NextPage = () => { behavior: "smooth", }); }} - handleLabelDelete={() => setSelectDeleteLabel(label)} + handleLabelDelete={setSelectDeleteLabel} user={user} /> ); @@ -210,4 +213,4 @@ const LabelsSettings: NextPage = () => { ); }; -export default LabelsSettings; +export default observer(LabelsSettings); diff --git a/apps/app/store/label.ts b/apps/app/store/label.ts new file mode 100644 index 000000000..d13882cd8 --- /dev/null +++ b/apps/app/store/label.ts @@ -0,0 +1,158 @@ +// mobx +import { action, observable, runInAction, makeAutoObservable } from "mobx"; + +// services +import issueService from "services/issues.service"; + +// types +import type { IIssueLabels, LabelLite, ICurrentUserResponse, LabelForm } from "types"; + +class LabelStore { + labels: LabelLite[] = []; + isLabelsLoading: boolean = false; + rootStore: any | null = null; + + constructor(_rootStore: any | null = null) { + makeAutoObservable(this, { + labels: observable.ref, + loadLabels: action, + isLabelsLoading: observable, + createLabel: action, + updateLabel: action, + deleteLabel: action, + }); + + this.rootStore = _rootStore; + } + + /** + * @description Fetch all labels of a project and hydrate labels field + */ + + loadLabels = async (workspaceSlug: string, projectId: string) => { + this.isLabelsLoading = true; + try { + const labelsResponse: IIssueLabels[] = await issueService.getIssueLabels( + workspaceSlug, + projectId + ); + runInAction(() => { + this.labels = labelsResponse.map((label) => ({ + id: label.id, + name: label.name, + description: label.description, + color: label.color, + parent: label.parent, + })); + this.isLabelsLoading = false; + }); + } catch (error) { + this.isLabelsLoading = false; + console.error("Fetching labels error", error); + } + }; + + getLabelById = (labelId: string) => this.labels.find((label) => label.id === labelId); + + getLabelChildren = (labelId: string) => this.labels.filter((label) => label.parent === labelId); + + /** + * For provided query, this function returns all labels that contain query in their name from the labels store. + * @param query - query string + * @returns {LabelLite[]} array of labels that contain query in their name + * @example + * getFilteredLabels("labe") // [{ id: "1", name: "label1", description: "", color: "", parent: null }] + */ + getFilteredLabels = (query: string): LabelLite[] => + this.labels.filter((label) => label.name.includes(query)); + + createLabel = async ( + workspaceSlug: string, + projectId: string, + labelForm: LabelForm, + user: ICurrentUserResponse + ) => { + try { + const labelResponse: IIssueLabels = await issueService.createIssueLabel( + workspaceSlug, + projectId, + labelForm, + user + ); + + runInAction(() => { + this.labels = [ + ...this.labels, + { + id: labelResponse.id, + name: labelResponse.name, + description: labelResponse.description, + color: labelResponse.color, + parent: labelResponse.parent, + }, + ]; + this.labels.sort((a, b) => a.name.localeCompare(b.name)); + }); + return labelResponse; + } catch (error) { + console.error("Creating label error", error); + return error; + } + }; + + updateLabel = async ( + workspaceSlug: string, + projectId: string, + labelId: string, + labelForm: Partial, + user: ICurrentUserResponse + ) => { + try { + const labelResponse: IIssueLabels = await issueService.patchIssueLabel( + workspaceSlug, + projectId, + labelId, + labelForm, + user + ); + + const _labels = [...this.labels].map((label) => { + if (label.id === labelId) { + return { + id: labelResponse.id, + name: labelResponse.name, + description: labelResponse.description, + color: labelResponse.color, + parent: labelResponse.parent, + }; + } + return label; + }); + + runInAction(() => { + this.labels = _labels; + }); + } catch (error) { + console.error("Updating label error", error); + return error; + } + }; + + deleteLabel = async ( + workspaceSlug: string, + projectId: string, + labelId: string, + user: ICurrentUserResponse + ) => { + try { + issueService.deleteIssueLabel(workspaceSlug, projectId, labelId, user); + runInAction(() => { + this.labels = this.labels.filter((label) => label.id !== labelId); + }); + } catch (error) { + console.error("Deleting label error", error); + } + }; +} + +export default LabelStore; diff --git a/apps/app/store/root.ts b/apps/app/store/root.ts index 5895637a8..4ff045ff9 100644 --- a/apps/app/store/root.ts +++ b/apps/app/store/root.ts @@ -3,6 +3,7 @@ import { enableStaticRendering } from "mobx-react-lite"; // store imports import UserStore from "./user"; import ThemeStore from "./theme"; +import LabelStore from "./label"; import ProjectPublishStore, { IProjectPublishStore } from "./project-publish"; enableStaticRendering(typeof window === "undefined"); @@ -10,11 +11,13 @@ enableStaticRendering(typeof window === "undefined"); export class RootStore { user; theme; + label: LabelStore; projectPublish: IProjectPublishStore; constructor() { this.user = new UserStore(this); this.theme = new ThemeStore(this); + this.label = new LabelStore(this); this.projectPublish = new ProjectPublishStore(this); } } diff --git a/apps/app/types/issues.d.ts b/apps/app/types/issues.d.ts index 683704f9f..4f89e67a6 100644 --- a/apps/app/types/issues.d.ts +++ b/apps/app/types/issues.d.ts @@ -164,6 +164,20 @@ export interface IIssueLabels { parent: string | null; } +export interface LabelForm { + name: string; + description: string; + color: string; + parent: string | null; +} + +/** + * @description Issue label's lite version + */ +export interface LabelLite extends LabelForm { + id: string; +} + export interface IIssueActivity { actor: string; actor_detail: IUserLite;