dev: label store implementation

This commit is contained in:
Dakshesh Jain 2023-08-16 16:53:39 +05:30
parent 550473bb02
commit f8ab0aa72b
11 changed files with 820 additions and 706 deletions

View File

@ -1,13 +1,12 @@
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// headless ui // headless ui
import { Combobox, Transition } from "@headlessui/react"; import { Combobox, Transition } from "@headlessui/react";
// services
import issuesServices from "services/issues.service";
// ui // ui
import { IssueLabelsList } from "components/ui"; import { IssueLabelsList } from "components/ui";
// icons // icons
@ -19,9 +18,7 @@ import {
TagIcon, TagIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
// types // types
import type { IIssueLabels } from "types"; import type { LabelLite } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
type Props = { type Props = {
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>; setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
@ -30,24 +27,24 @@ type Props = {
projectId: string; projectId: string;
}; };
export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange, projectId }) => { export const IssueLabelSelect: React.FC<Props> = observer(
({ setIsOpen, value, onChange, projectId }) => {
// states // states
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { data: issueLabels } = useSWR<IIssueLabels[]>( const { label: labelStore } = useMobxStore();
projectId ? PROJECT_ISSUE_LABELS(projectId) : null, const { isLabelsLoading: isLoading, labels, loadLabels, getLabelChildren } = labelStore;
workspaceSlug && projectId
? () => issuesServices.getIssueLabels(workspaceSlug as string, projectId)
: null
);
const filteredOptions = useEffect(() => {
query === "" if (workspaceSlug && projectId) loadLabels(workspaceSlug.toString(), projectId);
? issueLabels }, [workspaceSlug, projectId, loadLabels]);
: issueLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase()));
const filteredOptions: LabelLite[] = labels?.filter((l) =>
l.name.toLowerCase().includes(query.toLowerCase())
);
return ( return (
<Combobox <Combobox
@ -63,7 +60,7 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
{value && value.length > 0 ? ( {value && value.length > 0 ? (
<span className="flex items-center justify-center gap-2 px-3 py-1 text-xs"> <span className="flex items-center justify-center gap-2 px-3 py-1 text-xs">
<IssueLabelsList <IssueLabelsList
labels={value.map((v) => issueLabels?.find((l) => l.id === v)?.color) ?? []} labels={value.map((v) => labels?.find((l) => l.id === v)?.color) ?? []}
length={3} length={3}
showLength={true} showLength={true}
/> />
@ -100,10 +97,10 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
/> />
</div> </div>
<div className="py-1.5"> <div className="py-1.5">
{issueLabels && filteredOptions ? ( {!isLoading && filteredOptions ? (
filteredOptions.length > 0 ? ( filteredOptions.length > 0 ? (
filteredOptions.map((label) => { filteredOptions.map((label) => {
const children = issueLabels?.filter((l) => l.parent === label.id); const children = getLabelChildren(label.id);
if (children.length === 0) { if (children.length === 0) {
if (!label.parent) if (!label.parent)
@ -205,4 +202,5 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
)} )}
</Combobox> </Combobox>
); );
}; }
);

View File

@ -2,7 +2,9 @@ import React, { useEffect } from "react";
import { useRouter } from "next/router"; 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 // react-hook-form
import { Controller, useForm } from "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"; import { TwitterPicker } from "react-color";
// headless ui // headless ui
import { Dialog, Popover, Transition } from "@headlessui/react"; import { Dialog, Popover, Transition } from "@headlessui/react";
// services
import issuesService from "services/issues.service";
// ui // ui
import { Input, PrimaryButton, SecondaryButton } from "components/ui"; import { Input, PrimaryButton, SecondaryButton } from "components/ui";
// icons // icons
import { ChevronDownIcon } from "@heroicons/react/24/outline"; import { ChevronDownIcon } from "@heroicons/react/24/outline";
// types // types
import type { ICurrentUserResponse, IIssueLabels, IState } from "types"; import type { ICurrentUserResponse, IIssueLabels, LabelForm } from "types";
// constants // constants
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label"; import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label";
// types // types
@ -31,21 +30,19 @@ type Props = {
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
}; };
const defaultValues: Partial<IState> = { const defaultValues: Partial<LabelForm> = {
name: "", name: "",
color: "rgb(var(--color-text-200))", color: "rgb(var(--color-text-200))",
}; };
export const CreateLabelModal: React.FC<Props> = ({ export const CreateLabelModal: React.FC<Props> = observer(
isOpen, ({ isOpen, projectId, handleClose, user, onSuccess }) => {
projectId,
handleClose,
user,
onSuccess,
}) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { label: labelStore } = useMobxStore();
const { createLabel } = labelStore;
const { const {
register, register,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
@ -67,19 +64,13 @@ export const CreateLabelModal: React.FC<Props> = ({
reset(defaultValues); reset(defaultValues);
}; };
const onSubmit = async (formData: IIssueLabels) => { const onSubmit = async (formData: LabelForm) => {
if (!workspaceSlug) return; if (!workspaceSlug || !user) return;
await issuesService await createLabel(workspaceSlug.toString(), projectId as string, formData, user)
.createIssueLabel(workspaceSlug as string, projectId as string, formData, user) .then((response: any) => {
.then((res) => {
mutate<IIssueLabels[]>(
PROJECT_ISSUE_LABELS(projectId),
(prevData) => [res, ...(prevData ?? [])],
false
);
onClose(); onClose();
if (onSuccess) onSuccess(res); if (onSuccess) onSuccess(response);
}) })
.catch((error) => { .catch((error) => {
console.log(error); console.log(error);
@ -206,4 +197,5 @@ export const CreateLabelModal: React.FC<Props> = ({
</Dialog> </Dialog>
</Transition.Root> </Transition.Root>
); );
}; }
);

View File

@ -2,7 +2,9 @@ import React, { forwardRef, useEffect } from "react";
import { useRouter } from "next/router"; 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 // react-hook-form
import { Controller, SubmitHandler, useForm } from "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"; import { TwitterPicker } from "react-color";
// headless ui // headless ui
import { Popover, Transition } from "@headlessui/react"; import { Popover, Transition } from "@headlessui/react";
// services
import issuesService from "services/issues.service";
// ui // ui
import { Input, PrimaryButton, SecondaryButton } from "components/ui"; import { Input, PrimaryButton, SecondaryButton } from "components/ui";
// icons // icons
import { ChevronDownIcon } from "@heroicons/react/24/outline"; import { ChevronDownIcon } from "@heroicons/react/24/outline";
// types // types
import { IIssueLabels } from "types"; import { IIssueLabels, LabelLite } from "types";
// fetch-keys // fetch-keys
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label"; import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label";
type Props = { type Props = {
labelForm: boolean; labelForm: boolean;
setLabelForm: React.Dispatch<React.SetStateAction<boolean>>; setLabelForm: React.Dispatch<React.SetStateAction<boolean>>;
isUpdating: boolean; isUpdating: boolean;
labelToUpdate: IIssueLabels | null; labelToUpdate: LabelLite | null;
onClose?: () => void; onClose?: () => void;
}; };
@ -37,13 +36,16 @@ const defaultValues: Partial<IIssueLabels> = {
color: "rgb(var(--color-text-200))", color: "rgb(var(--color-text-200))",
}; };
export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>( export const CreateUpdateLabelInline = observer(
function CreateUpdateLabelInline(props, ref) { forwardRef<HTMLDivElement, Props>(function CreateUpdateLabelInline(props, ref) {
const { labelForm, setLabelForm, isUpdating, labelToUpdate, onClose } = props; const { labelForm, setLabelForm, isUpdating, labelToUpdate, onClose } = props;
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { label: labelStore } = useMobxStore();
const { createLabel, updateLabel } = labelStore;
const { user } = useUserAuth(); const { user } = useUserAuth();
const { const {
@ -65,39 +67,25 @@ export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
}; };
const handleLabelCreate: SubmitHandler<IIssueLabels> = async (formData) => { const handleLabelCreate: SubmitHandler<IIssueLabels> = async (formData) => {
if (!workspaceSlug || !projectId || isSubmitting) return; if (!workspaceSlug || !projectId || isSubmitting || !user) return;
await issuesService await createLabel(workspaceSlug.toString(), projectId.toString(), formData, user).finally(
.createIssueLabel(workspaceSlug as string, projectId as string, formData, user) () => {
.then((res) => {
mutate<IIssueLabels[]>(
PROJECT_ISSUE_LABELS(projectId as string),
(prevData) => [res, ...(prevData ?? [])],
false
);
handleClose(); handleClose();
}); }
);
}; };
const handleLabelUpdate: SubmitHandler<IIssueLabels> = async (formData) => { const handleLabelUpdate: SubmitHandler<IIssueLabels> = async (formData) => {
if (!workspaceSlug || !projectId || isSubmitting) return; if (!workspaceSlug || !projectId || isSubmitting || !user) return;
await issuesService await updateLabel(
.patchIssueLabel( workspaceSlug.toString(),
workspaceSlug as string, projectId.toString(),
projectId as string,
labelToUpdate?.id ?? "", labelToUpdate?.id ?? "",
formData, formData,
user user
) ).finally(() => {
.then(() => {
reset(defaultValues);
mutate<IIssueLabels[]>(
PROJECT_ISSUE_LABELS(projectId as string),
(prevData) =>
prevData?.map((p) => (p.id === labelToUpdate?.id ? { ...p, ...formData } : p)),
false
);
handleClose(); handleClose();
}); });
}; };
@ -212,5 +200,5 @@ export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
)} )}
</div> </div>
); );
} })
); );

View File

@ -2,36 +2,37 @@ import React, { useState } from "react";
import { useRouter } from "next/router"; 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 // headless ui
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// icons // icons
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// services
import issuesService from "services/issues.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// ui // ui
import { DangerButton, SecondaryButton } from "components/ui"; import { DangerButton, SecondaryButton } from "components/ui";
// types // types
import type { ICurrentUserResponse, IIssueLabels } from "types"; import type { ICurrentUserResponse, LabelLite } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
data: IIssueLabels | null; data: LabelLite | null;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
}; };
export const DeleteLabelModal: React.FC<Props> = ({ isOpen, onClose, data, user }) => { export const DeleteLabelModal: React.FC<Props> = observer(({ isOpen, onClose, data, user }) => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { label: labelStore } = useMobxStore();
const { deleteLabel } = labelStore;
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const handleClose = () => { const handleClose = () => {
@ -40,28 +41,21 @@ export const DeleteLabelModal: React.FC<Props> = ({ isOpen, onClose, data, user
}; };
const handleDeletion = async () => { const handleDeletion = async () => {
if (!workspaceSlug || !projectId || !data) return; if (!workspaceSlug || !projectId || !data || !user) return;
setIsDeleteLoading(true); setIsDeleteLoading(true);
mutate<IIssueLabels[]>( await deleteLabel(workspaceSlug.toString(), projectId.toString(), data.id, user)
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())
.catch(() => { .catch(() => {
setIsDeleteLoading(false);
mutate(PROJECT_ISSUE_LABELS(projectId.toString()));
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: "Label could not be deleted. Please try again.", message: "Label could not be deleted. Please try again.",
}); });
})
.finally(() => {
handleClose();
setIsDeleteLoading(false);
}); });
}; };
@ -130,4 +124,4 @@ export const DeleteLabelModal: React.FC<Props> = ({ isOpen, onClose, data, user
</Dialog> </Dialog>
</Transition.Root> </Transition.Root>
); );
}; });

View File

@ -2,73 +2,53 @@ import React, { useState } from "react";
import { useRouter } from "next/router"; 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 // headless ui
import { Combobox, Dialog, Transition } from "@headlessui/react"; import { Combobox, Dialog, Transition } from "@headlessui/react";
// icons // icons
import { RectangleStackIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { RectangleStackIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
// services
import issuesService from "services/issues.service";
// types // types
import { ICurrentUserResponse, IIssueLabels } from "types"; import { ICurrentUserResponse, LabelLite } from "types";
// constants
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
handleClose: () => void; handleClose: () => void;
parent: IIssueLabels | undefined; parent: LabelLite | undefined;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
}; };
export const LabelsListModal: React.FC<Props> = ({ isOpen, handleClose, parent, user }) => { export const LabelsListModal: React.FC<Props> = observer(
({ isOpen, handleClose, parent, user }) => {
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { data: issueLabels, mutate } = useSWR<IIssueLabels[]>( const { label: labelStore } = useMobxStore();
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, const { updateLabel, getLabelChildren, getFilteredLabels } = labelStore;
workspaceSlug && projectId
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
: null
);
const filteredLabels: IIssueLabels[] = const filteredLabels = getFilteredLabels(query);
query === ""
? issueLabels ?? []
: issueLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase())) ?? [];
const handleModalClose = () => { const handleModalClose = () => {
handleClose(); handleClose();
setQuery(""); setQuery("");
}; };
const addChildLabel = async (label: IIssueLabels) => { const addChildLabel = async (label: LabelLite) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId || !user) return;
mutate( updateLabel(
(prevData: any) => workspaceSlug.toString(),
prevData?.map((l: any) => { projectId.toString(),
if (l.id === label.id) return { ...l, parent: parent?.id ?? "" };
return l;
}),
false
);
await issuesService
.patchIssueLabel(
workspaceSlug as string,
projectId as string,
label.id, label.id,
{ {
parent: parent?.id ?? "", parent: parent?.id ?? "",
}, },
user user
) );
.then(() => mutate());
}; };
return ( return (
@ -120,7 +100,7 @@ export const LabelsListModal: React.FC<Props> = ({ isOpen, handleClose, parent,
)} )}
<ul className="text-sm text-gray-700"> <ul className="text-sm text-gray-700">
{filteredLabels.map((label) => { {filteredLabels.map((label) => {
const children = issueLabels?.filter((l) => l.parent === label.id); const children = getLabelChildren(label.id);
if ( if (
(label.parent === "" || label.parent === null) && // issue does not have any other parent (label.parent === "" || label.parent === null) && // issue does not have any other parent
@ -176,4 +156,5 @@ export const LabelsListModal: React.FC<Props> = ({ isOpen, handleClose, parent,
</Dialog> </Dialog>
</Transition.Root> </Transition.Root>
); );
}; }
);

View File

@ -2,12 +2,12 @@ import React from "react";
import { useRouter } from "next/router"; 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 // headless ui
import { Disclosure, Transition } from "@headlessui/react"; import { Disclosure, Transition } from "@headlessui/react";
// services
import issuesService from "services/issues.service";
// ui // ui
import { CustomMenu } from "components/ui"; import { CustomMenu } from "components/ui";
// icons // icons
@ -20,57 +20,37 @@ import {
TrashIcon, TrashIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
// types // types
import { ICurrentUserResponse, IIssueLabels } from "types"; import { ICurrentUserResponse, LabelLite } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
type Props = { type Props = {
label: IIssueLabels; label: LabelLite;
labelChildren: IIssueLabels[]; labelChildren: LabelLite[];
addLabelToGroup: (parentLabel: IIssueLabels) => void; addLabelToGroup: (parentLabel: LabelLite) => void;
editLabel: (label: IIssueLabels) => void; editLabel: (label: LabelLite) => void;
handleLabelDelete: () => void; handleLabelDelete: (label: LabelLite) => void;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
}; };
export const SingleLabelGroup: React.FC<Props> = ({ export const SingleLabelGroup: React.FC<Props> = observer(
label, ({ label, labelChildren, addLabelToGroup, editLabel, handleLabelDelete, user }) => {
labelChildren,
addLabelToGroup,
editLabel,
handleLabelDelete,
user,
}) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const removeFromGroup = (label: IIssueLabels) => { const { label: labelStore } = useMobxStore();
if (!workspaceSlug || !projectId) return; const { updateLabel } = labelStore;
mutate<IIssueLabels[]>( const removeFromGroup = (label: LabelLite) => {
PROJECT_ISSUE_LABELS(projectId as string), if (!workspaceSlug || !projectId || !user) return;
(prevData) =>
prevData?.map((l) => {
if (l.id === label.id) return { ...l, parent: null };
return l; updateLabel(
}), workspaceSlug.toString(),
false projectId.toString(),
);
issuesService
.patchIssueLabel(
workspaceSlug as string,
projectId as string,
label.id, label.id,
{ {
parent: null, parent: null,
}, },
user user
) );
.then(() => {
mutate(PROJECT_ISSUE_LABELS(projectId as string));
});
}; };
return ( return (
@ -102,7 +82,7 @@ export const SingleLabelGroup: React.FC<Props> = ({
<span>Edit label</span> <span>Edit label</span>
</span> </span>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleLabelDelete}> <CustomMenu.MenuItem onClick={() => handleLabelDelete(label)}>
<span className="flex items-center justify-start gap-2"> <span className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" /> <TrashIcon className="h-4 w-4" />
<span>Delete label</span> <span>Delete label</span>
@ -112,7 +92,9 @@ export const SingleLabelGroup: React.FC<Props> = ({
<Disclosure.Button> <Disclosure.Button>
<span> <span>
<ChevronDownIcon <ChevronDownIcon
className={`h-4 w-4 text-custom-text-100 ${!open ? "rotate-90 transform" : ""}`} className={`h-4 w-4 text-custom-text-100 ${
!open ? "rotate-90 transform" : ""
}`}
/> />
</span> </span>
</Disclosure.Button> </Disclosure.Button>
@ -158,7 +140,7 @@ export const SingleLabelGroup: React.FC<Props> = ({
<span>Edit label</span> <span>Edit label</span>
</span> </span>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleLabelDelete}> <CustomMenu.MenuItem onClick={() => handleLabelDelete(child)}>
<span className="flex items-center justify-start gap-2"> <span className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" /> <TrashIcon className="h-4 w-4" />
<span>Delete label</span> <span>Delete label</span>
@ -175,4 +157,5 @@ export const SingleLabelGroup: React.FC<Props> = ({
)} )}
</Disclosure> </Disclosure>
); );
}; }
);

View File

@ -3,14 +3,14 @@ import React from "react";
// ui // ui
import { CustomMenu } from "components/ui"; import { CustomMenu } from "components/ui";
// types // types
import { IIssueLabels } from "types"; import { LabelLite } from "types";
//icons //icons
import { RectangleGroupIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; import { RectangleGroupIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
type Props = { type Props = {
label: IIssueLabels; label: LabelLite;
addLabelToGroup: (parentLabel: IIssueLabels) => void; addLabelToGroup: (parentLabel: LabelLite) => void;
editLabel: (label: IIssueLabels) => void; editLabel: (label: LabelLite) => void;
handleLabelDelete: () => void; handleLabelDelete: () => void;
}; };

View File

@ -1,14 +1,17 @@
import React, { useState, useRef } from "react"; import React, { useState, useRef, useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
import useUserAuth from "hooks/use-user-auth"; import useUserAuth from "hooks/use-user-auth";
// services // services
import projectService from "services/project.service"; import projectService from "services/project.service";
import issuesService from "services/issues.service";
// layouts // layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// components // components
@ -28,10 +31,10 @@ import { PlusIcon } from "@heroicons/react/24/outline";
// images // images
import emptyLabel from "public/empty-state/label.svg"; import emptyLabel from "public/empty-state/label.svg";
// types // types
import { IIssueLabels } from "types"; import { LabelLite } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; import { PROJECT_DETAILS } from "constants/fetch-keys";
// helper // helper
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
@ -41,18 +44,21 @@ const LabelsSettings: NextPage = () => {
// edit label // edit label
const [isUpdating, setIsUpdating] = useState(false); const [isUpdating, setIsUpdating] = useState(false);
const [labelToUpdate, setLabelToUpdate] = useState<IIssueLabels | null>(null); const [labelToUpdate, setLabelToUpdate] = useState<LabelLite | null>(null);
// labels list modal // labels list modal
const [labelsListModal, setLabelsListModal] = useState(false); const [labelsListModal, setLabelsListModal] = useState(false);
const [parentLabel, setParentLabel] = useState<IIssueLabels | undefined>(undefined); const [parentLabel, setParentLabel] = useState<LabelLite | undefined>(undefined);
// delete label // delete label
const [selectDeleteLabel, setSelectDeleteLabel] = useState<IIssueLabels | null>(null); const [selectDeleteLabel, setSelectDeleteLabel] = useState<LabelLite | null>(null);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { label } = useMobxStore();
const { labels, isLabelsLoading: isLoading, getLabelChildren, loadLabels } = label;
const { user } = useUserAuth(); const { user } = useUserAuth();
const scrollToRef = useRef<HTMLDivElement>(null); const scrollToRef = useRef<HTMLDivElement>(null);
@ -64,24 +70,21 @@ const LabelsSettings: NextPage = () => {
: null : null
); );
const { data: issueLabels } = useSWR( useEffect(() => {
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, if (workspaceSlug && projectId) loadLabels(workspaceSlug.toString(), projectId.toString());
workspaceSlug && projectId }, [loadLabels, projectId, workspaceSlug]);
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
: null
);
const newLabel = () => { const newLabel = () => {
setIsUpdating(false); setIsUpdating(false);
setLabelForm(true); setLabelForm(true);
}; };
const addLabelToGroup = (parentLabel: IIssueLabels) => { const addLabelToGroup = (parentLabel: LabelLite) => {
setLabelsListModal(true); setLabelsListModal(true);
setParentLabel(parentLabel); setParentLabel(parentLabel);
}; };
const editLabel = (label: IIssueLabels) => { const editLabel = (label: LabelLite) => {
setLabelForm(true); setLabelForm(true);
setIsUpdating(true); setIsUpdating(true);
setLabelToUpdate(label); setLabelToUpdate(label);
@ -142,10 +145,10 @@ const LabelsSettings: NextPage = () => {
/> />
)} )}
<> <>
{issueLabels ? ( {!isLoading ? (
issueLabels.length > 0 ? ( labels.length > 0 ? (
issueLabels.map((label) => { labels.map((label) => {
const children = issueLabels?.filter((l) => l.parent === label.id); const children = getLabelChildren(label.id);
if (children && children.length === 0) { if (children && children.length === 0) {
if (!label.parent) if (!label.parent)
@ -176,7 +179,7 @@ const LabelsSettings: NextPage = () => {
behavior: "smooth", behavior: "smooth",
}); });
}} }}
handleLabelDelete={() => setSelectDeleteLabel(label)} handleLabelDelete={setSelectDeleteLabel}
user={user} user={user}
/> />
); );
@ -210,4 +213,4 @@ const LabelsSettings: NextPage = () => {
); );
}; };
export default LabelsSettings; export default observer(LabelsSettings);

158
apps/app/store/label.ts Normal file
View File

@ -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<LabelForm>,
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;

View File

@ -3,6 +3,7 @@ import { enableStaticRendering } from "mobx-react-lite";
// store imports // store imports
import UserStore from "./user"; import UserStore from "./user";
import ThemeStore from "./theme"; import ThemeStore from "./theme";
import LabelStore from "./label";
import ProjectPublishStore, { IProjectPublishStore } from "./project-publish"; import ProjectPublishStore, { IProjectPublishStore } from "./project-publish";
enableStaticRendering(typeof window === "undefined"); enableStaticRendering(typeof window === "undefined");
@ -10,11 +11,13 @@ enableStaticRendering(typeof window === "undefined");
export class RootStore { export class RootStore {
user; user;
theme; theme;
label: LabelStore;
projectPublish: IProjectPublishStore; projectPublish: IProjectPublishStore;
constructor() { constructor() {
this.user = new UserStore(this); this.user = new UserStore(this);
this.theme = new ThemeStore(this); this.theme = new ThemeStore(this);
this.label = new LabelStore(this);
this.projectPublish = new ProjectPublishStore(this); this.projectPublish = new ProjectPublishStore(this);
} }
} }

View File

@ -164,6 +164,20 @@ export interface IIssueLabels {
parent: string | null; 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 { export interface IIssueActivity {
actor: string; actor: string;
actor_detail: IUserLite; actor_detail: IUserLite;