mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
dev: label store implementation
This commit is contained in:
parent
550473bb02
commit
f8ab0aa72b
@ -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<React.SetStateAction<boolean>>;
|
||||
@ -30,24 +27,24 @@ type Props = {
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange, projectId }) => {
|
||||
export const IssueLabelSelect: React.FC<Props> = observer(
|
||||
({ setIsOpen, value, onChange, projectId }) => {
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
||||
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]);
|
||||
|
||||
const filteredOptions: LabelLite[] = labels?.filter((l) =>
|
||||
l.name.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
@ -63,7 +60,7 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
|
||||
{value && value.length > 0 ? (
|
||||
<span className="flex items-center justify-center gap-2 px-3 py-1 text-xs">
|
||||
<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}
|
||||
showLength={true}
|
||||
/>
|
||||
@ -100,10 +97,10 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
|
||||
/>
|
||||
</div>
|
||||
<div className="py-1.5">
|
||||
{issueLabels && filteredOptions ? (
|
||||
{!isLoading && filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((label) => {
|
||||
const children = issueLabels?.filter((l) => l.parent === label.id);
|
||||
const children = getLabelChildren(label.id);
|
||||
|
||||
if (children.length === 0) {
|
||||
if (!label.parent)
|
||||
@ -205,4 +202,5 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@ -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,21 +30,19 @@ type Props = {
|
||||
user: ICurrentUserResponse | undefined;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<IState> = {
|
||||
const defaultValues: Partial<LabelForm> = {
|
||||
name: "",
|
||||
color: "rgb(var(--color-text-200))",
|
||||
};
|
||||
|
||||
export const CreateLabelModal: React.FC<Props> = ({
|
||||
isOpen,
|
||||
projectId,
|
||||
handleClose,
|
||||
user,
|
||||
onSuccess,
|
||||
}) => {
|
||||
export const CreateLabelModal: React.FC<Props> = observer(
|
||||
({ isOpen, projectId, handleClose, user, onSuccess }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { label: labelStore } = useMobxStore();
|
||||
const { createLabel } = labelStore;
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
@ -67,19 +64,13 @@ export const CreateLabelModal: React.FC<Props> = ({
|
||||
reset(defaultValues);
|
||||
};
|
||||
|
||||
const onSubmit = async (formData: IIssueLabels) => {
|
||||
if (!workspaceSlug) return;
|
||||
const onSubmit = async (formData: LabelForm) => {
|
||||
if (!workspaceSlug || !user) return;
|
||||
|
||||
await issuesService
|
||||
.createIssueLabel(workspaceSlug as string, projectId as string, formData, user)
|
||||
.then((res) => {
|
||||
mutate<IIssueLabels[]>(
|
||||
PROJECT_ISSUE_LABELS(projectId),
|
||||
(prevData) => [res, ...(prevData ?? [])],
|
||||
false
|
||||
);
|
||||
await createLabel(workspaceSlug.toString(), projectId as string, formData, user)
|
||||
.then((response: any) => {
|
||||
onClose();
|
||||
if (onSuccess) onSuccess(res);
|
||||
if (onSuccess) onSuccess(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
@ -206,4 +197,5 @@ export const CreateLabelModal: React.FC<Props> = ({
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@ -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<React.SetStateAction<boolean>>;
|
||||
isUpdating: boolean;
|
||||
labelToUpdate: IIssueLabels | null;
|
||||
labelToUpdate: LabelLite | null;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
@ -37,13 +36,16 @@ const defaultValues: Partial<IIssueLabels> = {
|
||||
color: "rgb(var(--color-text-200))",
|
||||
};
|
||||
|
||||
export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
|
||||
function CreateUpdateLabelInline(props, ref) {
|
||||
export const CreateUpdateLabelInline = observer(
|
||||
forwardRef<HTMLDivElement, Props>(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,39 +67,25 @@ export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
|
||||
};
|
||||
|
||||
const handleLabelCreate: SubmitHandler<IIssueLabels> = 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<IIssueLabels[]>(
|
||||
PROJECT_ISSUE_LABELS(projectId as string),
|
||||
(prevData) => [res, ...(prevData ?? [])],
|
||||
false
|
||||
);
|
||||
await createLabel(workspaceSlug.toString(), projectId.toString(), formData, user).finally(
|
||||
() => {
|
||||
handleClose();
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleLabelUpdate: SubmitHandler<IIssueLabels> = async (formData) => {
|
||||
if (!workspaceSlug || !projectId || isSubmitting) return;
|
||||
if (!workspaceSlug || !projectId || isSubmitting || !user) return;
|
||||
|
||||
await issuesService
|
||||
.patchIssueLabel(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
await updateLabel(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
labelToUpdate?.id ?? "",
|
||||
formData,
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
reset(defaultValues);
|
||||
mutate<IIssueLabels[]>(
|
||||
PROJECT_ISSUE_LABELS(projectId as string),
|
||||
(prevData) =>
|
||||
prevData?.map((p) => (p.id === labelToUpdate?.id ? { ...p, ...formData } : p)),
|
||||
false
|
||||
);
|
||||
).finally(() => {
|
||||
handleClose();
|
||||
});
|
||||
};
|
||||
@ -212,5 +200,5 @@ export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -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<Props> = ({ isOpen, onClose, data, user }) => {
|
||||
export const DeleteLabelModal: React.FC<Props> = 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<Props> = ({ isOpen, onClose, data, user
|
||||
};
|
||||
|
||||
const handleDeletion = async () => {
|
||||
if (!workspaceSlug || !projectId || !data) return;
|
||||
if (!workspaceSlug || !projectId || !data || !user) return;
|
||||
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
mutate<IIssueLabels[]>(
|
||||
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<Props> = ({ isOpen, onClose, data, user
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -2,73 +2,53 @@ 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<Props> = ({ isOpen, handleClose, parent, user }) => {
|
||||
export const LabelsListModal: React.FC<Props> = observer(
|
||||
({ isOpen, handleClose, parent, user }) => {
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: issueLabels, mutate } = useSWR<IIssueLabels[]>(
|
||||
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 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 (
|
||||
@ -120,7 +100,7 @@ export const LabelsListModal: React.FC<Props> = ({ isOpen, handleClose, parent,
|
||||
)}
|
||||
<ul className="text-sm text-gray-700">
|
||||
{filteredLabels.map((label) => {
|
||||
const children = issueLabels?.filter((l) => l.parent === label.id);
|
||||
const children = getLabelChildren(label.id);
|
||||
|
||||
if (
|
||||
(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>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@ -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,57 +20,37 @@ 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<Props> = ({
|
||||
label,
|
||||
labelChildren,
|
||||
addLabelToGroup,
|
||||
editLabel,
|
||||
handleLabelDelete,
|
||||
user,
|
||||
}) => {
|
||||
export const SingleLabelGroup: React.FC<Props> = 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<IIssueLabels[]>(
|
||||
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 (
|
||||
@ -102,7 +82,7 @@ export const SingleLabelGroup: React.FC<Props> = ({
|
||||
<span>Edit label</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleLabelDelete}>
|
||||
<CustomMenu.MenuItem onClick={() => handleLabelDelete(label)}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span>Delete label</span>
|
||||
@ -112,7 +92,9 @@ export const SingleLabelGroup: React.FC<Props> = ({
|
||||
<Disclosure.Button>
|
||||
<span>
|
||||
<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>
|
||||
</Disclosure.Button>
|
||||
@ -158,7 +140,7 @@ export const SingleLabelGroup: React.FC<Props> = ({
|
||||
<span>Edit label</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleLabelDelete}>
|
||||
<CustomMenu.MenuItem onClick={() => handleLabelDelete(child)}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span>Delete label</span>
|
||||
@ -175,4 +157,5 @@ export const SingleLabelGroup: React.FC<Props> = ({
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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<IIssueLabels | null>(null);
|
||||
const [labelToUpdate, setLabelToUpdate] = useState<LabelLite | null>(null);
|
||||
|
||||
// labels list modal
|
||||
const [labelsListModal, setLabelsListModal] = useState(false);
|
||||
const [parentLabel, setParentLabel] = useState<IIssueLabels | undefined>(undefined);
|
||||
const [parentLabel, setParentLabel] = useState<LabelLite | undefined>(undefined);
|
||||
|
||||
// delete label
|
||||
const [selectDeleteLabel, setSelectDeleteLabel] = useState<IIssueLabels | null>(null);
|
||||
const [selectDeleteLabel, setSelectDeleteLabel] = useState<LabelLite | null>(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<HTMLDivElement>(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);
|
||||
|
158
apps/app/store/label.ts
Normal file
158
apps/app/store/label.ts
Normal 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;
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
14
apps/app/types/issues.d.ts
vendored
14
apps/app/types/issues.d.ts
vendored
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user