From 2d64caef90567c5dbee0901e109782f5488811e6 Mon Sep 17 00:00:00 2001 From: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Date: Wed, 1 Nov 2023 13:42:51 +0530 Subject: [PATCH] refactor: project settings (#2575) * refactor: project setting estimate * refactor: project setting label * refactor: project setting state * refactor: project setting integration * refactor: project settings member * fix: estimate not updating * fix: estimate not in observable * fix: build error --- .../create-update-estimate-modal.tsx | 283 +++--------- .../estimates/delete-estimate-modal.tsx | 50 +- ...le-estimate.tsx => estimate-list-item.tsx} | 74 ++- web/components/estimates/estimate-list.tsx | 139 ++++++ web/components/estimates/index.ts | 2 +- web/components/issues/draft-issue-form.tsx | 8 +- web/components/issues/form.tsx | 8 +- web/components/labels/create-label-modal.tsx | 25 +- .../labels/create-update-label-inline.tsx | 300 ++++++------ web/components/labels/delete-label-modal.tsx | 53 +-- web/components/labels/index.ts | 5 +- web/components/labels/labels-list-modal.tsx | 63 ++- ...up.tsx => project-setting-label-group.tsx} | 69 +-- ...sx => project-setting-label-list-item.tsx} | 9 +- .../labels/project-setting-label-list.tsx | 166 +++++++ .../project/confirm-project-member-remove.tsx | 6 +- web/components/project/form.tsx | 4 +- web/components/project/index.ts | 7 +- ...egration-card.tsx => integration-card.tsx} | 2 +- web/components/project/member-list-item.tsx | 203 ++++++++ web/components/project/member-list.tsx | 110 +++++ web/components/project/member-select.tsx | 28 +- .../project-settings-member-defaults.tsx | 149 ++++++ .../project/send-project-invitation-modal.tsx | 8 +- web/components/states/create-state-modal.tsx | 67 ++- .../states/create-update-state-inline.tsx | 176 ++++--- web/components/states/delete-state-modal.tsx | 50 +- web/components/states/index.ts | 3 +- ...sx => project-setting-state-list-item.tsx} | 140 +----- .../states/project-setting-state-list.tsx | 129 ++++++ web/components/states/state-select.tsx | 24 +- .../projects/[projectId]/pages/[pageId].tsx | 1 - .../[projectId]/settings/estimates.tsx | 180 +------- .../[projectId]/settings/integrations.tsx | 12 +- .../projects/[projectId]/settings/labels.tsx | 189 +------- .../projects/[projectId]/settings/members.tsx | 437 +----------------- .../projects/[projectId]/settings/states.tsx | 149 +----- web/store/project/index.ts | 3 + web/store/project/project.store.ts | 71 ++- web/store/project/project_estimates.store.ts | 141 ++++++ web/store/project/project_label_store.ts | 140 ++++++ web/store/project/project_state.store.ts | 279 +++++++++++ web/store/root.ts | 19 +- yarn.lock | 256 +++++----- 44 files changed, 2308 insertions(+), 1929 deletions(-) rename web/components/estimates/{single-estimate.tsx => estimate-list-item.tsx} (64%) create mode 100644 web/components/estimates/estimate-list.tsx rename web/components/labels/{single-label-group.tsx => project-setting-label-group.tsx} (82%) rename web/components/labels/{single-label.tsx => project-setting-label-list-item.tsx} (91%) create mode 100644 web/components/labels/project-setting-label-list.tsx rename web/components/project/{single-integration-card.tsx => integration-card.tsx} (98%) create mode 100644 web/components/project/member-list-item.tsx create mode 100644 web/components/project/member-list.tsx create mode 100644 web/components/project/project-settings-member-defaults.tsx rename web/components/states/{single-state.tsx => project-setting-state-list-item.tsx} (52%) create mode 100644 web/components/states/project-setting-state-list.tsx create mode 100644 web/store/project/project_estimates.store.ts create mode 100644 web/store/project/project_label_store.ts create mode 100644 web/store/project/project_state.store.ts diff --git a/web/components/estimates/create-update-estimate-modal.tsx b/web/components/estimates/create-update-estimate-modal.tsx index 46be6d5c9..6c331f950 100644 --- a/web/components/estimates/create-update-estimate-modal.tsx +++ b/web/components/estimates/create-update-estimate-modal.tsx @@ -1,15 +1,11 @@ import React, { useEffect } from "react"; - import { useRouter } from "next/router"; - -import { mutate } from "swr"; - -// react-hook-form import { Controller, useForm } from "react-hook-form"; -// headless ui import { Dialog, Transition } from "@headlessui/react"; -// services -import { ProjectEstimateService } from "services/project"; + +// store +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; // hooks import useToast from "hooks/use-toast"; // ui @@ -17,29 +13,15 @@ import { Button, Input, TextArea } from "@plane/ui"; // helpers import { checkDuplicates } from "helpers/array.helper"; // types -import { IUser, IEstimate, IEstimateFormData } from "types"; -// fetch-keys -import { ESTIMATES_LIST, ESTIMATE_DETAILS } from "constants/fetch-keys"; +import { IEstimate, IEstimateFormData } from "types"; type Props = { isOpen: boolean; handleClose: () => void; data?: IEstimate; - user: IUser | undefined; }; -type FormValues = { - name: string; - description: string; - value1: string; - value2: string; - value3: string; - value4: string; - value5: string; - value6: string; -}; - -const defaultValues: Partial = { +const defaultValues = { name: "", description: "", value1: "", @@ -50,10 +32,18 @@ const defaultValues: Partial = { value6: "", }; -// services -const projectEstimateService = new ProjectEstimateService(); +type FormValues = typeof defaultValues; + +export const CreateUpdateEstimateModal: React.FC = observer((props) => { + const { handleClose, data, isOpen } = props; + + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + // store + const { projectEstimates: projectEstimatesStore } = useMobxStore(); -export const CreateUpdateEstimateModal: React.FC = ({ handleClose, data, isOpen, user }) => { const { formState: { errors, isSubmitting }, handleSubmit, @@ -68,71 +58,47 @@ export const CreateUpdateEstimateModal: React.FC = ({ handleClose, data, reset(); }; - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - const { setToastAlert } = useToast(); const createEstimate = async (payload: IEstimateFormData) => { if (!workspaceSlug || !projectId) return; - await projectEstimateService - .createEstimate(workspaceSlug as string, projectId as string, payload, user) + await projectEstimatesStore + .createEstimate(workspaceSlug.toString(), projectId.toString(), payload) .then(() => { - mutate(ESTIMATES_LIST(projectId as string)); onClose(); }) .catch((err) => { - if (err.status === 400) - setToastAlert({ - type: "error", - title: "Error!", - message: "Estimate with that name already exists. Please try again with another name.", - }); - else - setToastAlert({ - type: "error", - title: "Error!", - message: "Estimate could not be created. Please try again.", - }); + const error = err?.error; + const errorString = Array.isArray(error) ? error[0] : error; + + setToastAlert({ + type: "error", + title: "Error!", + message: + errorString ?? err.status === 400 + ? "Estimate with that name already exists. Please try again with another name." + : "Estimate could not be created. Please try again.", + }); }); }; const updateEstimate = async (payload: IEstimateFormData) => { if (!workspaceSlug || !projectId || !data) return; - mutate( - ESTIMATES_LIST(projectId.toString()), - (prevData) => - prevData?.map((p) => { - if (p.id === data.id) - return { - ...p, - name: payload.estimate.name, - description: payload.estimate.description, - points: p.points.map((point, index) => ({ - ...point, - value: payload.estimate_points[index].value, - })), - }; - - return p; - }), - false - ); - - await projectEstimateService - .patchEstimate(workspaceSlug as string, projectId as string, data?.id as string, payload, user) + await projectEstimatesStore + .updateEstimate(workspaceSlug.toString(), projectId.toString(), data.id, payload) .then(() => { - mutate(ESTIMATES_LIST(projectId.toString())); - mutate(ESTIMATE_DETAILS(data.id)); handleClose(); }) - .catch(() => { + .catch((err) => { + const error = err?.error; + const errorString = Array.isArray(error) ? error[0] : error; + setToastAlert({ type: "error", title: "Error!", - message: "Estimate could not be updated. Please try again.", + message: errorString ?? "Estimate could not be updated. Please try again.", }); }); @@ -291,151 +257,38 @@ export const CreateUpdateEstimateModal: React.FC = ({ handleClose, data, )} /> + + {/* list of all the points */} + {/* since they are all the same, we can use a loop to render them */}
-
- - 1 - - ( - ( +
+ + {i + 1} + + ( + + )} /> - )} - /> - - -
-
- - 2 - - ( - - )} - /> - - -
-
- - 3 - - ( - - )} - /> - - -
-
- - 4 - - ( - - )} - /> - - -
-
- - 5 - - ( - - )} - /> - - -
-
- - 6 - - ( - - )} - /> - - -
+
+
+
+ ))}
@@ -461,4 +314,4 @@ export const CreateUpdateEstimateModal: React.FC = ({ handleClose, data, ); -}; +}); diff --git a/web/components/estimates/delete-estimate-modal.tsx b/web/components/estimates/delete-estimate-modal.tsx index 59f8e10be..0a96eda3d 100644 --- a/web/components/estimates/delete-estimate-modal.tsx +++ b/web/components/estimates/delete-estimate-modal.tsx @@ -1,10 +1,13 @@ import React, { useEffect, useState } from "react"; - -// headless ui +import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; +// store +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import useToast from "hooks/use-toast"; // types import { IEstimate } from "types"; - // icons import { AlertTriangle } from "lucide-react"; // ui @@ -12,14 +15,43 @@ import { Button } from "@plane/ui"; type Props = { isOpen: boolean; + data: IEstimate | null; handleClose: () => void; - data: IEstimate; - handleDelete: () => void; }; -export const DeleteEstimateModal: React.FC = ({ isOpen, handleClose, data, handleDelete }) => { +export const DeleteEstimateModal: React.FC = observer((props) => { + const { isOpen, handleClose, data } = props; + + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + // store + const { projectEstimates: projectEstimatesStore } = useMobxStore(); + + // states const [isDeleteLoading, setIsDeleteLoading] = useState(false); + // hooks + const { setToastAlert } = useToast(); + + const handleEstimateDelete = () => { + if (!workspaceSlug || !projectId) return; + + const estimateId = data?.id!; + + projectEstimatesStore.deleteEstimate(workspaceSlug.toString(), projectId.toString(), estimateId).catch((err) => { + const error = err?.error; + const errorString = Array.isArray(error) ? error[0] : error; + + setToastAlert({ + type: "error", + title: "Error!", + message: errorString ?? "Estimate could not be deleted. Please try again", + }); + }); + }; + useEffect(() => { setIsDeleteLoading(false); }, [isOpen]); @@ -68,7 +100,7 @@ export const DeleteEstimateModal: React.FC = ({ isOpen, handleClose, data

Are you sure you want to delete estimate-{" "} - {data.name} + {data?.name} {""}? All of the data related to the estiamte will be permanently removed. This action cannot be undone.

@@ -81,7 +113,7 @@ export const DeleteEstimateModal: React.FC = ({ isOpen, handleClose, data variant="danger" onClick={() => { setIsDeleteLoading(true); - handleDelete(); + handleEstimateDelete(); }} loading={isDeleteLoading} > @@ -96,4 +128,4 @@ export const DeleteEstimateModal: React.FC = ({ isOpen, handleClose, data ); -}; +}); diff --git a/web/components/estimates/single-estimate.tsx b/web/components/estimates/estimate-list-item.tsx similarity index 64% rename from web/components/estimates/single-estimate.tsx rename to web/components/estimates/estimate-list-item.tsx index a4e8fde28..78e069f4e 100644 --- a/web/components/estimates/single-estimate.tsx +++ b/web/components/estimates/estimate-list-item.tsx @@ -1,14 +1,12 @@ -import React, { useState } from "react"; +import React from "react"; import { useRouter } from "next/router"; -// services -import { ProjectService } from "services/project"; +// store +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; // hooks import useToast from "hooks/use-toast"; -import useProjectDetails from "hooks/use-project-details"; -// components -import { DeleteEstimateModal } from "components/estimates"; // ui import { Button, CustomMenu } from "@plane/ui"; //icons @@ -16,48 +14,46 @@ import { Pencil, Trash2 } from "lucide-react"; // helpers import { orderArrayBy } from "helpers/array.helper"; // types -import { IUser, IEstimate } from "types"; +import { IEstimate } from "types"; type Props = { - user: IUser | undefined; estimate: IEstimate; editEstimate: (estimate: IEstimate) => void; - handleEstimateDelete: (estimateId: string) => void; + deleteEstimate: (estimateId: string) => void; }; -// services -const projectService = new ProjectService(); - -export const SingleEstimate: React.FC = ({ user, estimate, editEstimate, handleEstimateDelete }) => { - const [isDeleteEstimateModalOpen, setIsDeleteEstimateModalOpen] = useState(false); +export const EstimateListItem: React.FC = observer((props) => { + const { estimate, editEstimate, deleteEstimate } = props; + // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; + // store + const { project: projectStore } = useMobxStore(); + const { setToastAlert } = useToast(); - const { projectDetails, mutateProjectDetails } = useProjectDetails(); + // derived values + const projectDetails = projectStore.project_details?.[projectId?.toString()!]; const handleUseEstimate = async () => { if (!workspaceSlug || !projectId) return; - const payload = { - estimate: estimate.id, - }; + await projectStore + .updateProject(workspaceSlug.toString(), projectId.toString(), { + estimate: estimate.id, + }) + .catch((err) => { + const error = err?.error; + const errorString = Array.isArray(error) ? error[0] : error; - mutateProjectDetails((prevData: any) => { - if (!prevData) return prevData; - - return { ...prevData, estimate: estimate.id }; - }, false); - - await projectService.updateProject(workspaceSlug as string, projectId as string, payload, user).catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Estimate points could not be used. Please try again.", + setToastAlert({ + type: "error", + title: "Error!", + message: errorString ?? "Estimate points could not be used. Please try again.", + }); }); - }); }; return ( @@ -76,7 +72,7 @@ export const SingleEstimate: React.FC = ({ user, estimate, editEstimate,

- {projectDetails?.estimate !== estimate.id && estimate.points.length > 0 && ( + {projectDetails?.estimate !== estimate?.id && estimate?.points?.length > 0 && ( @@ -95,7 +91,7 @@ export const SingleEstimate: React.FC = ({ user, estimate, editEstimate, {projectDetails?.estimate !== estimate.id && ( { - setIsDeleteEstimateModalOpen(true); + deleteEstimate(estimate.id); }} >
@@ -107,7 +103,7 @@ export const SingleEstimate: React.FC = ({ user, estimate, editEstimate,
- {estimate.points.length > 0 ? ( + {estimate?.points?.length > 0 ? (
Estimate points ( @@ -126,16 +122,6 @@ export const SingleEstimate: React.FC = ({ user, estimate, editEstimate,
)} - - setIsDeleteEstimateModalOpen(false)} - data={estimate} - handleDelete={() => { - handleEstimateDelete(estimate.id); - setIsDeleteEstimateModalOpen(false); - }} - /> ); -}; +}); diff --git a/web/components/estimates/estimate-list.tsx b/web/components/estimates/estimate-list.tsx new file mode 100644 index 000000000..a9fdb52f6 --- /dev/null +++ b/web/components/estimates/estimate-list.tsx @@ -0,0 +1,139 @@ +import React, { useState } from "react"; +import { useRouter } from "next/router"; + +// store +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; +// components +import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates"; +//hooks +import useToast from "hooks/use-toast"; +// ui +import { Button, Loader } from "@plane/ui"; +import { EmptyState } from "components/common"; +// icons +import { Plus } from "lucide-react"; +// images +import emptyEstimate from "public/empty-state/estimate.svg"; +// types +import { IEstimate } from "types"; + +export const EstimatesList: React.FC = observer(() => { + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + // store + const { project: projectStore } = useMobxStore(); + + // states + const [estimateFormOpen, setEstimateFormOpen] = useState(false); + const [estimateToDelete, setEstimateToDelete] = useState(null); + const [estimateToUpdate, setEstimateToUpdate] = useState(); + + // hooks + const { setToastAlert } = useToast(); + + // derived values + const estimatesList = projectStore.projectEstimates; + const projectDetails = projectStore.project_details?.[projectId?.toString()!]; + + const editEstimate = (estimate: IEstimate) => { + setEstimateFormOpen(true); + setEstimateToUpdate(estimate); + }; + + const disableEstimates = () => { + if (!workspaceSlug || !projectId) return; + + projectStore.updateProject(workspaceSlug.toString(), projectId.toString(), { estimate: null }).catch((err) => { + const error = err?.error; + const errorString = Array.isArray(error) ? error[0] : error; + + setToastAlert({ + type: "error", + title: "Error!", + message: errorString ?? "Estimate could not be disabled. Please try again", + }); + }); + }; + + return ( + <> + { + setEstimateFormOpen(false); + setEstimateToUpdate(undefined); + }} + /> + + setEstimateToDelete(null)} + data={projectStore.getProjectEstimateById(estimateToDelete!)} + /> + +
+

Estimates

+
+
+ + {projectDetails?.estimate && ( + + )} +
+
+
+ + {estimatesList ? ( + estimatesList.length > 0 ? ( +
+ {estimatesList.map((estimate) => ( + editEstimate(estimate)} + deleteEstimate={(estimateId) => setEstimateToDelete(estimateId)} + /> + ))} +
+ ) : ( +
+ , + text: "Add Estimate", + onClick: () => { + setEstimateFormOpen(true); + setEstimateToUpdate(undefined); + }, + }} + /> +
+ ) + ) : ( + + + + + + + )} + + ); +}); diff --git a/web/components/estimates/index.ts b/web/components/estimates/index.ts index b88ceaf03..e9a22a53d 100644 --- a/web/components/estimates/index.ts +++ b/web/components/estimates/index.ts @@ -1,4 +1,4 @@ export * from "./create-update-estimate-modal"; export * from "./delete-estimate-modal"; export * from "./estimate-select"; -export * from "./single-estimate"; +export * from "./estimate-list-item"; diff --git a/web/components/issues/draft-issue-form.tsx b/web/components/issues/draft-issue-form.tsx index 9deafb51a..6ba7ed329 100644 --- a/web/components/issues/draft-issue-form.tsx +++ b/web/components/issues/draft-issue-form.tsx @@ -297,17 +297,11 @@ export const DraftIssueForm: FC = (props) => { <> {projectId && ( <> - setStateModal(false)} - projectId={projectId} - user={user} - /> + setStateModal(false)} projectId={projectId} /> setLabelModal(false)} projectId={projectId} - user={user} onSuccess={(response) => setValue("labels", [...watch("labels"), response.id])} /> diff --git a/web/components/issues/form.tsx b/web/components/issues/form.tsx index 071a7661b..0d5f176e4 100644 --- a/web/components/issues/form.tsx +++ b/web/components/issues/form.tsx @@ -249,17 +249,11 @@ export const IssueForm: FC = observer((props) => { <> {projectId && ( <> - setStateModal(false)} - projectId={projectId} - user={user ?? undefined} - /> + setStateModal(false)} projectId={projectId} /> setLabelModal(false)} projectId={projectId} - user={user ?? undefined} onSuccess={(response) => setValue("labels", [...watch("labels"), response.id])} /> diff --git a/web/components/labels/create-label-modal.tsx b/web/components/labels/create-label-modal.tsx index d55e1233d..7f42f0095 100644 --- a/web/components/labels/create-label-modal.tsx +++ b/web/components/labels/create-label-modal.tsx @@ -1,19 +1,19 @@ import React, { useEffect } from "react"; import { useRouter } from "next/router"; -import { mutate } from "swr"; import { Controller, useForm } from "react-hook-form"; import { TwitterPicker } from "react-color"; import { Dialog, Popover, Transition } from "@headlessui/react"; -// services -import { IssueLabelService } from "services/issue"; + +// store +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; // ui import { Button, Input } from "@plane/ui"; // icons import { ChevronDown } from "lucide-react"; // types -import type { IUser, IIssueLabels, IState } from "types"; +import type { IIssueLabels, IState } from "types"; // constants -import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label"; // types @@ -22,7 +22,6 @@ type Props = { projectId: string; handleClose: () => void; onSuccess?: (response: IIssueLabels) => void; - user: IUser | undefined; }; const defaultValues: Partial = { @@ -30,12 +29,15 @@ const defaultValues: Partial = { color: "rgb(var(--color-text-200))", }; -const issueLabelService = new IssueLabelService(); +export const CreateLabelModal: React.FC = observer((props) => { + const { isOpen, projectId, handleClose, onSuccess } = props; -export const CreateLabelModal: React.FC = ({ isOpen, projectId, handleClose, user, onSuccess }) => { const router = useRouter(); const { workspaceSlug } = router.query; + // store + const { projectLabel: projectLabelStore } = useMobxStore(); + const { formState: { errors, isSubmitting }, handleSubmit, @@ -59,10 +61,9 @@ export const CreateLabelModal: React.FC = ({ isOpen, projectId, handleClo const onSubmit = async (formData: IIssueLabels) => { if (!workspaceSlug) return; - await issueLabelService - .createIssueLabel(workspaceSlug as string, projectId as string, formData, user) + await projectLabelStore + .createLabel(workspaceSlug.toString(), projectId.toString(), formData) .then((res) => { - mutate(PROJECT_ISSUE_LABELS(projectId), (prevData) => [res, ...(prevData ?? [])], false); onClose(); if (onSuccess) onSuccess(res); }) @@ -197,4 +198,4 @@ export const CreateLabelModal: React.FC = ({ isOpen, projectId, handleClo ); -}; +}); diff --git a/web/components/labels/create-update-label-inline.tsx b/web/components/labels/create-update-label-inline.tsx index b5ce8ccef..16cab89ed 100644 --- a/web/components/labels/create-update-label-inline.tsx +++ b/web/components/labels/create-update-label-inline.tsx @@ -1,21 +1,18 @@ -import { forwardRef, useEffect, Fragment } from "react"; +import React, { forwardRef, useEffect } from "react"; import { useRouter } from "next/router"; -import { mutate } from "swr"; -import { Controller, SubmitHandler, useForm } from "react-hook-form"; -// hooks -import useUserAuth from "hooks/use-user-auth"; -// react-color import { TwitterPicker } from "react-color"; +import { Controller, SubmitHandler, useForm } from "react-hook-form"; + +// stores +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; // headless ui import { Popover, Transition } from "@headlessui/react"; -// services -import { IssueLabelService } from "services/issue"; // ui import { Button, Input } from "@plane/ui"; // types import { IIssueLabels } from "types"; // fetch-keys -import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label"; type Props = { @@ -31,171 +28,162 @@ const defaultValues: Partial = { color: "rgb(var(--color-text-200))", }; -const issueLabelService = new IssueLabelService(); +export const CreateUpdateLabelInline = observer( + forwardRef(function CreateUpdateLabelInline(props, ref) { + const { labelForm, setLabelForm, isUpdating, labelToUpdate, onClose } = props; -export const CreateUpdateLabelInline = forwardRef(function CreateUpdateLabelInline(props, ref) { - const { labelForm, setLabelForm, isUpdating, labelToUpdate, onClose } = props; + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + // store + const { projectLabel: projectLabelStore } = useMobxStore(); - const { user } = useUserAuth(); + const { + handleSubmit, + control, + reset, + formState: { errors, isSubmitting }, + watch, + setValue, + setFocus, + } = useForm({ + defaultValues, + }); - const { - handleSubmit, - control, - reset, - formState: { errors, isSubmitting }, - watch, - setValue, - } = useForm({ - defaultValues, - }); + const handleClose = () => { + setLabelForm(false); + reset(defaultValues); + if (onClose) onClose(); + }; - const handleClose = () => { - setLabelForm(false); - reset(defaultValues); - if (onClose) onClose(); - }; + const handleLabelCreate: SubmitHandler = async (formData) => { + if (!workspaceSlug || !projectId || isSubmitting) return; - const handleLabelCreate: SubmitHandler = async (formData) => { - if (!workspaceSlug || !projectId || isSubmitting) return; - - await issueLabelService - .createIssueLabel(workspaceSlug as string, projectId as string, formData, user) - .then((res) => { - mutate( - PROJECT_ISSUE_LABELS(projectId as string), - (prevData) => [res, ...(prevData ?? [])], - false - ); + await projectLabelStore.createLabel(workspaceSlug.toString(), projectId.toString(), formData).then(() => { handleClose(); - }); - }; - - const handleLabelUpdate: SubmitHandler = async (formData) => { - if (!workspaceSlug || !projectId || isSubmitting) return; - - await issueLabelService - .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(); }); - }; + }; - useEffect(() => { - if (!labelForm && isUpdating) return; + const handleLabelUpdate: SubmitHandler = async (formData) => { + if (!workspaceSlug || !projectId || isSubmitting) return; - reset(); - }, [labelForm, isUpdating, reset]); + await projectLabelStore + .updateLabel(workspaceSlug.toString(), projectId.toString(), labelToUpdate?.id!, formData) + .then(() => { + reset(defaultValues); + handleClose(); + }); + }; - useEffect(() => { - if (!labelToUpdate) return; + /** + * For settings focus on name input + */ + useEffect(() => { + setFocus("name"); + }, [setFocus, labelForm]); - setValue("color", labelToUpdate.color && labelToUpdate.color !== "" ? labelToUpdate.color : "#000"); - setValue("name", labelToUpdate.name); - }, [labelToUpdate, setValue]); + useEffect(() => { + if (!labelToUpdate) return; - useEffect(() => { - if (labelToUpdate) { + setValue("name", labelToUpdate.name); setValue("color", labelToUpdate.color && labelToUpdate.color !== "" ? labelToUpdate.color : "#000"); - return; - } + }, [labelToUpdate, setValue]); - setValue("color", getRandomLabelColor()); - }, [labelToUpdate, setValue]); + useEffect(() => { + if (labelToUpdate) { + setValue("color", labelToUpdate.color && labelToUpdate.color !== "" ? labelToUpdate.color : "#000"); + return; + } - return ( -
-
- - {({ open }) => ( - <> - - - + setValue("color", getRandomLabelColor()); + }, [labelToUpdate, setValue]); - - - ( - onChange(value.hex)} - /> - )} + return ( +
{ + e.preventDefault(); + handleSubmit(isUpdating ? handleLabelUpdate : handleLabelCreate)(); + }} + className={`flex scroll-m-8 items-center gap-2 rounded border border-custom-border-200 bg-custom-background-100 px-3.5 py-2 ${ + labelForm ? "" : "hidden" + }`} + > +
+ + {({ open }) => ( + <> + + - - - - )} - -
-
- ( - - )} - /> -
- - {isUpdating ? ( -
+
+ ( + + )} + /> +
+ - ) : ( - - )} -
- ); -}); + + ); + }) +); diff --git a/web/components/labels/delete-label-modal.tsx b/web/components/labels/delete-label-modal.tsx index dee1c2f68..b70e28dda 100644 --- a/web/components/labels/delete-label-modal.tsx +++ b/web/components/labels/delete-label-modal.tsx @@ -1,40 +1,39 @@ import React, { useState } from "react"; - import { useRouter } from "next/router"; - -import { mutate } from "swr"; - -// headless ui import { Dialog, Transition } from "@headlessui/react"; + +// store +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; // icons import { AlertTriangle } from "lucide-react"; -// services -import { IssueLabelService } from "services/issue"; // hooks import useToast from "hooks/use-toast"; // ui import { Button } from "@plane/ui"; // types -import type { IUser, IIssueLabels } from "types"; -// fetch-keys -import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; +import type { IIssueLabels } from "types"; type Props = { isOpen: boolean; onClose: () => void; data: IIssueLabels | null; - user: IUser | undefined; }; -// services -const issueLabelService = new IssueLabelService(); - -export const DeleteLabelModal: React.FC = ({ isOpen, onClose, data, user }) => { - const [isDeleteLoading, setIsDeleteLoading] = useState(false); +export const DeleteLabelModal: React.FC = observer((props) => { + const { isOpen, onClose, data } = props; + // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; + // store + const { projectLabel: projectLabelStore } = useMobxStore(); + + // states + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + // hooks const { setToastAlert } = useToast(); const handleClose = () => { @@ -47,23 +46,19 @@ export const DeleteLabelModal: React.FC = ({ isOpen, onClose, data, user setIsDeleteLoading(true); - mutate( - PROJECT_ISSUE_LABELS(projectId.toString()), - (prevData) => (prevData ?? []).filter((p) => p.id !== data.id), - false - ); - - await issueLabelService - .deleteIssueLabel(workspaceSlug.toString(), projectId.toString(), data.id, user) - .then(() => handleClose()) - .catch(() => { + await projectLabelStore + .deleteLabel(workspaceSlug.toString(), projectId.toString(), data.id) + .then(() => { + handleClose(); + }) + .catch((err) => { setIsDeleteLoading(false); - mutate(PROJECT_ISSUE_LABELS(projectId.toString())); + const error = err?.error || "Label could not be deleted. Please try again."; setToastAlert({ type: "error", title: "Error!", - message: "Label could not be deleted. Please try again.", + message: error, }); }); }; @@ -129,4 +124,4 @@ export const DeleteLabelModal: React.FC = ({ isOpen, onClose, data, user ); -}; +}); diff --git a/web/components/labels/index.ts b/web/components/labels/index.ts index 4a012c6ba..c7316b037 100644 --- a/web/components/labels/index.ts +++ b/web/components/labels/index.ts @@ -3,5 +3,6 @@ export * from "./create-update-label-inline"; export * from "./delete-label-modal"; export * from "./label-select"; export * from "./labels-list-modal"; -export * from "./single-label-group"; -export * from "./single-label"; +export * from "./project-setting-label-group"; +export * from "./project-setting-label-list-item"; +export * from "./project-setting-label-list"; diff --git a/web/components/labels/labels-list-modal.tsx b/web/components/labels/labels-list-modal.tsx index 078a07d86..650745caa 100644 --- a/web/components/labels/labels-list-modal.tsx +++ b/web/components/labels/labels-list-modal.tsx @@ -2,38 +2,47 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; import { Combobox, Dialog, Transition } from "@headlessui/react"; + +// store +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; + // icons import { LayerStackIcon } from "@plane/ui"; import { Search } from "lucide-react"; -// services -import { IssueLabelService } from "services/issue"; // types -import { IUser, IIssueLabels } from "types"; -// constants -import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; +import { IIssueLabels } from "types"; type Props = { isOpen: boolean; handleClose: () => void; parent: IIssueLabels | undefined; - user: IUser | undefined; }; -const issueLabelService = new IssueLabelService(); - -export const LabelsListModal: React.FC = ({ isOpen, handleClose, parent, user }) => { - const [query, setQuery] = useState(""); +export const LabelsListModal: React.FC = observer((props) => { + const { isOpen, handleClose, parent } = props; + // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { data: issueLabels, mutate } = useSWR( - workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, + // store + const { projectLabel: projectLabelStore, project: projectStore } = useMobxStore(); + + // states + const [query, setQuery] = useState(""); + + // api call to fetch project details + useSWR( + workspaceSlug && projectId ? "PROJECT_LABELS" : null, workspaceSlug && projectId - ? () => issueLabelService.getProjectIssueLabels(workspaceSlug as string, projectId as string) + ? () => projectStore.fetchProjectLabels(workspaceSlug.toString(), projectId.toString()) : null ); + // derived values + const issueLabels = projectStore.labels?.[projectId?.toString()!] ?? null; + const filteredLabels: IIssueLabels[] = query === "" ? issueLabels ?? [] @@ -47,27 +56,9 @@ export const LabelsListModal: React.FC = ({ isOpen, handleClose, parent, const addChildLabel = async (label: IIssueLabels) => { if (!workspaceSlug || !projectId) return; - mutate( - (prevData: any) => - prevData?.map((l: any) => { - if (l.id === label.id) return { ...l, parent: parent?.id ?? "" }; - - return l; - }), - false - ); - - await issueLabelService - .patchIssueLabel( - workspaceSlug as string, - projectId as string, - label.id, - { - parent: parent?.id ?? "", - }, - user - ) - .then(() => mutate()); + await projectLabelStore.updateLabel(workspaceSlug.toString(), projectId.toString(), label.id, { + parent: parent?.id!, + }); }; return ( @@ -122,7 +113,7 @@ export const LabelsListModal: React.FC = ({ isOpen, handleClose, parent, 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 + children?.length === 0 // issue doesn't have any other children ) return ( = ({ isOpen, handleClose, parent, ); -}; +}); diff --git a/web/components/labels/single-label-group.tsx b/web/components/labels/project-setting-label-group.tsx similarity index 82% rename from web/components/labels/single-label-group.tsx rename to web/components/labels/project-setting-label-group.tsx index 79ef480b2..675bcee72 100644 --- a/web/components/labels/single-label-group.tsx +++ b/web/components/labels/project-setting-label-group.tsx @@ -1,72 +1,41 @@ import React from "react"; - import { useRouter } from "next/router"; - -import { mutate } from "swr"; - -// headless ui import { Disclosure, Transition } from "@headlessui/react"; -// services -import { IssueLabelService } from "services/issue"; + +// store +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; // ui -import { CustomMenu } from "@plane/ui"; +import { CustomMenu } from "components/ui"; // icons import { ChevronDown, Component, Pencil, Plus, Trash2, X } from "lucide-react"; // types -import { IUser, IIssueLabels } from "types"; -// fetch-keys -import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; +import { IIssueLabels } from "types"; type Props = { label: IIssueLabels; labelChildren: IIssueLabels[]; - addLabelToGroup: (parentLabel: IIssueLabels) => void; - editLabel: (label: IIssueLabels) => void; handleLabelDelete: () => void; - user: IUser | undefined; + editLabel: (label: IIssueLabels) => void; + addLabelToGroup: (parentLabel: IIssueLabels) => void; }; -// services -const issueLabelService = new IssueLabelService(); +export const ProjectSettingLabelGroup: React.FC = observer((props) => { + const { label, labelChildren, addLabelToGroup, editLabel, handleLabelDelete } = props; -export const SingleLabelGroup: React.FC = ({ - label, - labelChildren, - addLabelToGroup, - editLabel, - handleLabelDelete, - user, -}) => { + // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; + // store + const { projectLabel: projectLabelStore } = useMobxStore(); + const removeFromGroup = (label: IIssueLabels) => { if (!workspaceSlug || !projectId) return; - mutate( - PROJECT_ISSUE_LABELS(projectId as string), - (prevData) => - prevData?.map((l) => { - if (l.id === label.id) return { ...l, parent: null }; - - return l; - }), - false - ); - - issueLabelService - .patchIssueLabel( - workspaceSlug as string, - projectId as string, - label.id, - { - parent: null, - }, - user - ) - .then(() => { - mutate(PROJECT_ISSUE_LABELS(projectId as string)); - }); + projectLabelStore.updateLabel(workspaceSlug.toString(), projectId.toString(), label.id, { + parent: null, + }); }; return ( @@ -163,7 +132,7 @@ export const SingleLabelGroup: React.FC = ({
@@ -176,4 +145,4 @@ export const SingleLabelGroup: React.FC = ({ )} ); -}; +}); diff --git a/web/components/labels/single-label.tsx b/web/components/labels/project-setting-label-list-item.tsx similarity index 91% rename from web/components/labels/single-label.tsx rename to web/components/labels/project-setting-label-list-item.tsx index 6b7cbcd0b..41b4a8cb7 100644 --- a/web/components/labels/single-label.tsx +++ b/web/components/labels/project-setting-label-list-item.tsx @@ -3,11 +3,11 @@ import React, { useRef, useState } from "react"; //hook import useOutsideClickDetector from "hooks/use-outside-click-detector"; // ui -import { CustomMenu } from "@plane/ui"; +import { CustomMenu } from "components/ui"; // types import { IIssueLabels } from "types"; //icons -import { Component, Pencil, X } from "lucide-react"; +import { Component, X, Pencil } from "lucide-react"; type Props = { label: IIssueLabels; @@ -16,7 +16,9 @@ type Props = { handleLabelDelete: () => void; }; -export const SingleLabel: React.FC = ({ label, addLabelToGroup, editLabel, handleLabelDelete }) => { +export const ProjectSettingLabelItem: React.FC = (props) => { + const { label, addLabelToGroup, editLabel, handleLabelDelete } = props; + const [isMenuActive, setIsMenuActive] = useState(false); const actionSectionRef = useRef(null); @@ -33,6 +35,7 @@ export const SingleLabel: React.FC = ({ label, addLabelToGroup, editLabel />
{label.name}
+
{ + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + // store + const { project: projectStore } = useMobxStore(); + + // states + const [labelForm, setLabelForm] = useState(false); + const [isUpdating, setIsUpdating] = useState(false); + const [labelsListModal, setLabelsListModal] = useState(false); + const [labelToUpdate, setLabelToUpdate] = useState(null); + const [parentLabel, setParentLabel] = useState(undefined); + const [selectDeleteLabel, setSelectDeleteLabel] = useState(null); + + // ref + const scrollToRef = useRef(null); + + // api call to fetch project details + useSWR( + workspaceSlug && projectId ? "PROJECT_LABELS" : null, + workspaceSlug && projectId + ? () => projectStore.fetchProjectLabels(workspaceSlug.toString(), projectId.toString()) + : null + ); + + // derived values + const issueLabels = projectStore.labels?.[projectId?.toString()!] ?? null; + + const newLabel = () => { + setIsUpdating(false); + setLabelForm(true); + }; + + const addLabelToGroup = (parentLabel: IIssueLabels) => { + setLabelsListModal(true); + setParentLabel(parentLabel); + }; + + const editLabel = (label: IIssueLabels) => { + setLabelForm(true); + setIsUpdating(true); + setLabelToUpdate(label); + }; + + return ( + <> + setLabelsListModal(false)} /> + setSelectDeleteLabel(null)} + /> + +
+

Labels

+ +
+
+ {labelForm && ( + { + setLabelForm(false); + setIsUpdating(false); + setLabelToUpdate(null); + }} + /> + )} + + {/* labels */} + {issueLabels && + issueLabels.map((label) => { + const children = issueLabels?.filter((l) => l.parent === label.id); + + if (children && children.length === 0) { + if (!label.parent) + return ( + addLabelToGroup(label)} + editLabel={(label) => { + editLabel(label); + scrollToRef.current?.scrollIntoView({ + behavior: "smooth", + }); + }} + handleLabelDelete={() => setSelectDeleteLabel(label)} + /> + ); + } else { + return ( + { + editLabel(label); + scrollToRef.current?.scrollIntoView({ + behavior: "smooth", + }); + }} + handleLabelDelete={() => setSelectDeleteLabel(label)} + /> + ); + } + })} + + {/* loading state */} + {!issueLabels && ( + + + + + + + )} + + {/* empty state */} + {issueLabels && issueLabels.length === 0 && ( + newLabel(), + }} + /> + )} +
+ + ); +}); diff --git a/web/components/project/confirm-project-member-remove.tsx b/web/components/project/confirm-project-member-remove.tsx index 512e979a3..128ea2233 100644 --- a/web/components/project/confirm-project-member-remove.tsx +++ b/web/components/project/confirm-project-member-remove.tsx @@ -13,7 +13,9 @@ type Props = { data?: any; }; -const ConfirmProjectMemberRemove: React.FC = ({ isOpen, onClose, data, handleDelete }) => { +export const ConfirmProjectMemberRemove: React.FC = (props) => { + const { isOpen, onClose, data, handleDelete } = props; + const [isDeleteLoading, setIsDeleteLoading] = useState(false); const handleClose = () => { @@ -89,5 +91,3 @@ const ConfirmProjectMemberRemove: React.FC = ({ isOpen, onClose, data, ha ); }; - -export default ConfirmProjectMemberRemove; diff --git a/web/components/project/form.tsx b/web/components/project/form.tsx index b4bcdd808..1bc942fe1 100644 --- a/web/components/project/form.tsx +++ b/web/components/project/form.tsx @@ -68,11 +68,11 @@ export const ProjectDetailsForm: FC = (props) => { message: "Project updated successfully", }); }) - .catch(() => { + .catch((error) => { setToastAlert({ type: "error", title: "Error!", - message: "Project could not be updated. Please try again.", + message: error?.error ?? "Project could not be updated. Please try again.", }); }); }; diff --git a/web/components/project/index.ts b/web/components/project/index.ts index f3b44c143..56c5ba38c 100644 --- a/web/components/project/index.ts +++ b/web/components/project/index.ts @@ -13,4 +13,9 @@ export * from "./members-select"; export * from "./priority-select"; export * from "./sidebar-list-item"; export * from "./sidebar-list"; -export * from "./single-integration-card"; +export * from "./integration-card"; +export * from "./member-list"; +export * from "./member-list-item"; +export * from "./project-settings-member-defaults"; +export * from "./send-project-invitation-modal"; +export * from "./confirm-project-member-remove"; diff --git a/web/components/project/single-integration-card.tsx b/web/components/project/integration-card.tsx similarity index 98% rename from web/components/project/single-integration-card.tsx rename to web/components/project/integration-card.tsx index ae916c782..8a53fa50d 100644 --- a/web/components/project/single-integration-card.tsx +++ b/web/components/project/integration-card.tsx @@ -37,7 +37,7 @@ const integrationDetails: { [key: string]: any } = { // services const projectService = new ProjectService(); -export const SingleIntegration: React.FC = ({ integration }) => { +export const IntegrationCard: React.FC = ({ integration }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; diff --git a/web/components/project/member-list-item.tsx b/web/components/project/member-list-item.tsx new file mode 100644 index 000000000..8454b6a10 --- /dev/null +++ b/web/components/project/member-list-item.tsx @@ -0,0 +1,203 @@ +import { useState } from "react"; +import { useRouter } from "next/router"; +import Link from "next/link"; +import useSWR, { mutate } from "swr"; +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; +// services +import { ProjectInvitationService } from "services/project"; +// hooks +import useToast from "hooks/use-toast"; +// components +import { ConfirmProjectMemberRemove } from "components/project"; +// ui +import { CustomMenu, CustomSelect } from "@plane/ui"; +// icons +import { ChevronDown, X } from "lucide-react"; +// constants +import { ROLE } from "constants/workspace"; + +// services +const projectInvitationService = new ProjectInvitationService(); + +type Props = { + member: any; +}; + +export const ProjectMemberListItem: React.FC = observer((props) => { + const { member } = props; + + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + // states + const [selectedRemoveMember, setSelectedRemoveMember] = useState(null); + const [selectedInviteRemoveMember, setSelectedInviteRemoveMember] = useState(null); + + // store + const { user: userStore, project: projectStore } = useMobxStore(); + + const { setToastAlert } = useToast(); + + useSWR( + workspaceSlug && projectId ? `PROJECT_MEMBERS_${projectId.toString().toUpperCase()}` : null, + workspaceSlug && projectId + ? () => projectStore.fetchProjectMembers(workspaceSlug.toString(), projectId.toString()) + : null + ); + + // derived values + const user = userStore.currentUser; + const memberDetails = userStore.projectMemberInfo; + const isAdmin = memberDetails?.role === 20; + const isOwner = memberDetails?.role === 20; + const projectMembers = projectStore.members?.[projectId?.toString()!]; + const currentUser = projectMembers?.find((item) => item.member.id === user?.id); + + return ( + <> + { + setSelectedRemoveMember(null); + setSelectedInviteRemoveMember(null); + }} + data={selectedRemoveMember ?? selectedInviteRemoveMember} + handleDelete={async () => { + if (!workspaceSlug || !projectId) return; + + // if the user is a member + if (selectedRemoveMember) { + await projectStore.removeMemberFromProject( + workspaceSlug.toString(), + projectId.toString(), + selectedRemoveMember + ); + } + // if the user is an invite + if (selectedInviteRemoveMember) { + await projectInvitationService.deleteProjectInvitation( + workspaceSlug.toString(), + projectId.toString(), + selectedInviteRemoveMember + ); + mutate(`PROJECT_INVITATIONS_${projectId.toString()}`); + } + + setToastAlert({ + type: "success", + message: "Member removed successfully", + title: "Success", + }); + }} + /> + +
+
+ {member.avatar && member.avatar !== "" ? ( +
+ {member.display_name} +
+ ) : member.display_name || member.email ? ( +
+ {(member.display_name || member.email)?.charAt(0)} +
+ ) : ( +
+ ? +
+ )} +
+ {member.member ? ( + + + + {member.first_name} {member.last_name} + + ({member.display_name}) + + + ) : ( +

{member.display_name || member.email}

+ )} + {isOwner &&

{member.email}

} +
+
+
+ {!member.member && ( +
+ Pending +
+ )} + + + {ROLE[member.role as keyof typeof ROLE]} + + {member.memberId !== user?.id && } +
+ } + value={member.role} + onChange={(value: 5 | 10 | 15 | 20 | undefined) => { + if (!workspaceSlug || !projectId) return; + + projectStore + .updateMember(workspaceSlug.toString(), projectId.toString(), member.id, { + role: value, + }) + .catch((err) => { + const error = err.error; + const errorString = Array.isArray(error) ? error[0] : error; + + setToastAlert({ + type: "error", + title: "Error!", + message: errorString ?? "An error occurred while updating member role. Please try again.", + }); + }); + }} + disabled={ + member.memberId === user?.id || + !member.member || + (currentUser && currentUser.role !== 20 && currentUser.role < member.role) + } + > + {Object.keys(ROLE).map((key) => { + if (currentUser && currentUser.role !== 20 && currentUser.role < parseInt(key)) return null; + + return ( + + <>{ROLE[parseInt(key) as keyof typeof ROLE]} + + ); + })} + + + { + if (member.member) setSelectedRemoveMember(member.id); + else setSelectedInviteRemoveMember(member.id); + }} + > + + + + {member.memberId !== user?.id ? "Remove member" : "Leave project"} + + + +
+
+ + ); +}); diff --git a/web/components/project/member-list.tsx b/web/components/project/member-list.tsx new file mode 100644 index 000000000..7a8f6184d --- /dev/null +++ b/web/components/project/member-list.tsx @@ -0,0 +1,110 @@ +import { useState } from "react"; +import { useRouter } from "next/router"; +import useSWR, { mutate } from "swr"; +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; +// services +import { ProjectInvitationService } from "services/project"; +// hooks +import useUser from "hooks/use-user"; +// components +import { ProjectMemberListItem, SendProjectInvitationModal } from "components/project"; +// ui +import { Button, Loader } from "@plane/ui"; + +// services +const projectInvitationService = new ProjectInvitationService(); + +export const ProjectMemberList: React.FC = observer(() => { + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + // store + const { project: projectStore } = useMobxStore(); + + // states + const [inviteModal, setInviteModal] = useState(false); + + const { user } = useUser(); + + useSWR( + workspaceSlug && projectId ? `PROJECT_MEMBERS_${projectId.toString().toUpperCase()}` : null, + workspaceSlug && projectId + ? () => projectStore.fetchProjectMembers(workspaceSlug.toString(), projectId.toString()) + : null + ); + + const { data: projectInvitations } = useSWR( + workspaceSlug && projectId ? `PROJECT_INVITATIONS_${projectId.toString()}` : null, + workspaceSlug && projectId + ? () => projectInvitationService.fetchProjectInvitations(workspaceSlug.toString(), projectId.toString()) + : null + ); + + // derived values + const projectMembers = projectStore.projectMembers; + + const members = [ + ...(projectMembers?.map((item) => ({ + id: item.id, + memberId: item.member?.id, + avatar: item.member?.avatar, + first_name: item.member?.first_name, + last_name: item.member?.last_name, + email: item.member?.email, + display_name: item.member?.display_name, + role: item.role, + status: true, + member: true, + })) || []), + ...(projectInvitations?.map((item: any) => ({ + id: item.id, + memberId: item.id, + avatar: item.avatar ?? "", + first_name: item.first_name ?? item.email, + last_name: item.last_name ?? "", + email: item.email, + display_name: item.email, + role: item.role, + status: item.accepted, + member: false, + })) || []), + ]; + + return ( + <> + { + mutate(`PROJECT_INVITATIONS_${projectId?.toString()}`); + projectStore.fetchProjectMembers(workspaceSlug?.toString()!, projectId?.toString()!); + }} + /> + +
+

Members

+ +
+ {!projectMembers || !projectInvitations ? ( + + + + + + + ) : ( +
+ {members.length > 0 + ? members.map((member) => ) + : null} +
+ )} + + ); +}); diff --git a/web/components/project/member-select.tsx b/web/components/project/member-select.tsx index 686568513..8885544fc 100644 --- a/web/components/project/member-select.tsx +++ b/web/components/project/member-select.tsx @@ -1,18 +1,15 @@ import React from "react"; - import { useRouter } from "next/router"; - import useSWR from "swr"; -// services -import { ProjectService } from "services/project"; +// store +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; // ui import { Avatar } from "components/ui"; import { CustomSearchSelect } from "@plane/ui"; // icon import { Ban } from "lucide-react"; -// fetch-keys -import { PROJECT_MEMBERS } from "constants/fetch-keys"; type Props = { value: any; @@ -20,20 +17,25 @@ type Props = { isDisabled?: boolean; }; -// services -const projectService = new ProjectService(); +export const MemberSelect: React.FC = observer((props) => { + const { value, onChange, isDisabled = false } = props; -export const MemberSelect: React.FC = ({ value, onChange, isDisabled = false }) => { + // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { data: members } = useSWR( - workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null, + // store + const { project: projectStore } = useMobxStore(); + + useSWR( + workspaceSlug && projectId ? `PROJECT_MEMBERS_${projectId.toString().toUpperCase()}` : null, workspaceSlug && projectId - ? () => projectService.fetchProjectMembers(workspaceSlug as string, projectId as string) + ? () => projectStore.fetchProjectMembers(workspaceSlug.toString(), projectId.toString()) : null ); + const members = projectStore.members?.[projectId?.toString()!]; + const options = members?.map((member) => ({ value: member.member.id, query: member.member.display_name, @@ -86,4 +88,4 @@ export const MemberSelect: React.FC = ({ value, onChange, isDisabled = fa disabled={isDisabled} /> ); -}; +}); diff --git a/web/components/project/project-settings-member-defaults.tsx b/web/components/project/project-settings-member-defaults.tsx new file mode 100644 index 000000000..8dc539683 --- /dev/null +++ b/web/components/project/project-settings-member-defaults.tsx @@ -0,0 +1,149 @@ +import { useEffect } from "react"; +import { useRouter } from "next/router"; +import useSWR from "swr"; +// store +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import useToast from "hooks/use-toast"; +import { Controller, useForm } from "react-hook-form"; + +import { MemberSelect } from "components/project"; +// ui +import { Loader } from "@plane/ui"; +// types +import { IProject, IUserLite, IWorkspace } from "types"; +// fetch-keys +import { PROJECT_MEMBERS } from "constants/fetch-keys"; + +const defaultValues: Partial = { + project_lead: null, + default_assignee: null, +}; + +export const ProjectSettingsMemberDefaults: React.FC = observer(() => { + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + // store + const { user: userStore, project: projectStore } = useMobxStore(); + + // hooks + const { setToastAlert } = useToast(); + + // derived values + const memberDetails = userStore.projectMemberInfo; + const isAdmin = memberDetails?.role === 20; + const projectDetails = projectStore.project_details[projectId?.toString()!]; + + const { reset, control } = useForm({ defaultValues }); + + useSWR( + workspaceSlug && projectId ? PROJECT_MEMBERS(projectId.toString()) : null, + workspaceSlug && projectId + ? () => projectStore.fetchProjectDetails(workspaceSlug.toString(), projectId.toString()) + : null + ); + + useEffect(() => { + if (!projectDetails) return; + + reset({ + ...projectDetails, + default_assignee: projectDetails.default_assignee?.id ?? projectDetails.default_assignee, + project_lead: (projectDetails.project_lead as IUserLite)?.id ?? projectDetails.project_lead, + workspace: (projectDetails.workspace as IWorkspace).id, + }); + }, [projectDetails, reset]); + + const submitChanges = async (formData: Partial) => { + if (!workspaceSlug || !projectId) return; + + reset({ + ...projectDetails, + default_assignee: projectDetails.default_assignee?.id ?? projectDetails.default_assignee, + project_lead: (projectDetails.project_lead as IUserLite)?.id ?? projectDetails.project_lead, + ...formData, + }); + + await projectStore + .updateProject(workspaceSlug.toString(), projectId.toString(), { + default_assignee: formData.default_assignee === "none" ? null : formData.default_assignee, + project_lead: formData.project_lead === "none" ? null : formData.project_lead, + }) + .then(() => { + projectStore.fetchProjectDetails(workspaceSlug.toString(), projectId.toString()); + setToastAlert({ + title: "Success", + type: "success", + message: "Project updated successfully", + }); + }) + .catch((err) => { + console.log(err); + }); + }; + + return ( + <> +
+

Defaults

+
+ +
+
+
+

Project Lead

+
+ {projectDetails ? ( + ( + { + submitChanges({ project_lead: val }); + }} + isDisabled={!isAdmin} + /> + )} + /> + ) : ( + + + + )} +
+
+ +
+

Default Assignee

+
+ {projectDetails ? ( + ( + { + submitChanges({ default_assignee: val }); + }} + isDisabled={!isAdmin} + /> + )} + /> + ) : ( + + + + )} +
+
+
+
+ + ); +}); diff --git a/web/components/project/send-project-invitation-modal.tsx b/web/components/project/send-project-invitation-modal.tsx index be27088fd..5252f50cb 100644 --- a/web/components/project/send-project-invitation-modal.tsx +++ b/web/components/project/send-project-invitation-modal.tsx @@ -1,11 +1,7 @@ import React, { useEffect } from "react"; - import { useRouter } from "next/router"; - import useSWR from "swr"; - import { useForm, Controller, useFieldArray } from "react-hook-form"; - import { Dialog, Transition } from "@headlessui/react"; // ui import { Button, CustomSelect, CustomSearchSelect } from "@plane/ui"; @@ -56,7 +52,7 @@ const defaultValues: FormValues = { const projectService = new ProjectService(); const workspaceService = new WorkspaceService(); -const SendProjectInvitationModal: React.FC = (props) => { +export const SendProjectInvitationModal: React.FC = (props) => { const { isOpen, setIsOpen, members, user, onSuccess } = props; const router = useRouter(); @@ -308,5 +304,3 @@ const SendProjectInvitationModal: React.FC = (props) => { ); }; - -export default SendProjectInvitationModal; diff --git a/web/components/states/create-state-modal.tsx b/web/components/states/create-state-modal.tsx index b6caa8382..c89937c07 100644 --- a/web/components/states/create-state-modal.tsx +++ b/web/components/states/create-state-modal.tsx @@ -1,29 +1,29 @@ import React from "react"; import { useRouter } from "next/router"; -import { mutate } from "swr"; import { Controller, useForm } from "react-hook-form"; import { TwitterPicker } from "react-color"; import { Dialog, Popover, Transition } from "@headlessui/react"; -// services -import { ProjectStateService } from "services/project"; + +// store +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; // hooks import useToast from "hooks/use-toast"; // ui -import { Button, CustomSelect, Input, TextArea } from "@plane/ui"; +import { CustomSelect } from "components/ui"; +import { Button, Input, TextArea } from "@plane/ui"; // icons import { ChevronDown } from "lucide-react"; // types -import type { IUser, IState, IStateResponse } from "types"; -// fetch keys -import { STATES_LIST } from "constants/fetch-keys"; +import type { IState } from "types"; // constants import { GROUP_CHOICES } from "constants/project"; + // types type Props = { isOpen: boolean; projectId: string; handleClose: () => void; - user: IUser | undefined; }; const defaultValues: Partial = { @@ -33,12 +33,15 @@ const defaultValues: Partial = { group: "backlog", }; -const projectStateService = new ProjectStateService(); +export const CreateStateModal: React.FC = observer((props) => { + const { isOpen, projectId, handleClose } = props; -export const CreateStateModal: React.FC = ({ isOpen, projectId, handleClose, user }) => { const router = useRouter(); const { workspaceSlug } = router.query; + // store + const { projectState: projectStateStore } = useMobxStore(); + const { setToastAlert } = useToast(); const { @@ -63,36 +66,32 @@ export const CreateStateModal: React.FC = ({ isOpen, projectId, handleClo ...formData, }; - await projectStateService - .createState(workspaceSlug as string, projectId, payload, user) - .then((res) => { - mutate( - STATES_LIST(projectId.toString()), - (prevData) => { - if (!prevData) return prevData; - - return { - ...prevData, - [res.group]: [...prevData[res.group], res], - }; - }, - false - ); + await projectStateStore + .createState(workspaceSlug.toString(), projectId.toString(), payload) + .then(() => { onClose(); }) .catch((err) => { - if (err.status === 400) + const error = err.response; + + if (typeof error === "object") { + Object.keys(error).forEach((key) => { + setToastAlert({ + type: "error", + title: "Error!", + message: Array.isArray(error[key]) ? error[key].join(", ") : error[key], + }); + }); + } else { setToastAlert({ type: "error", title: "Error!", - message: "Another state exists with the same name. Please try again with another name.", - }); - else - setToastAlert({ - type: "error", - title: "Error!", - message: "State could not be created. Please try again.", + message: + error ?? err.status === 400 + ? "Another state exists with the same name. Please try again with another name." + : "State could not be created. Please try again.", }); + } }); }; @@ -264,4 +263,4 @@ export const CreateStateModal: React.FC = ({ isOpen, projectId, handleClo ); -}; +}); diff --git a/web/components/states/create-update-state-inline.tsx b/web/components/states/create-update-state-inline.tsx index ec83214c3..8c910daa4 100644 --- a/web/components/states/create-update-state-inline.tsx +++ b/web/components/states/create-update-state-inline.tsx @@ -1,23 +1,20 @@ import React, { useEffect } from "react"; - import { useRouter } from "next/router"; - import { mutate } from "swr"; - -// react-hook-form import { useForm, Controller } from "react-hook-form"; -// react-color import { TwitterPicker } from "react-color"; -// headless ui import { Popover, Transition } from "@headlessui/react"; -// services -import { ProjectStateService } from "services/project"; + +// store +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; // hooks import useToast from "hooks/use-toast"; // ui -import { Button, CustomSelect, Input, Tooltip } from "@plane/ui"; +import { CustomSelect } from "components/ui"; +import { Button, Input, Tooltip } from "@plane/ui"; // types -import type { IUser, IState, IStateResponse } from "types"; +import type { IState } from "types"; // fetch-keys import { STATES_LIST } from "constants/fetch-keys"; // constants @@ -26,25 +23,30 @@ import { GROUP_CHOICES } from "constants/project"; type Props = { data: IState | null; onClose: () => void; - selectedGroup: StateGroup | null; - user: IUser | undefined; groupLength: number; + selectedGroup: StateGroup | null; }; export type StateGroup = "backlog" | "unstarted" | "started" | "completed" | "cancelled" | null; const defaultValues: Partial = { name: "", + description: "", color: "rgb(var(--color-text-200))", group: "backlog", }; -const projectStateService = new ProjectStateService(); +export const CreateUpdateStateInline: React.FC = observer((props) => { + const { data, onClose, selectedGroup, groupLength } = props; -export const CreateUpdateStateInline: React.FC = ({ data, onClose, selectedGroup, user, groupLength }) => { + // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; + // store + const { projectState: projectStateStore } = useMobxStore(); + + // hooks const { setToastAlert } = useToast(); const { @@ -57,15 +59,19 @@ export const CreateUpdateStateInline: React.FC = ({ data, onClose, select defaultValues, }); + /** + * @description pre-populate form with data if data is present + */ useEffect(() => { if (!data) return; - reset(data); }, [data, reset]); + /** + * @description pre-populate form with default values if data is not present + */ useEffect(() => { if (data) return; - reset({ ...defaultValues, group: selectedGroup ?? "backlog", @@ -77,87 +83,73 @@ export const CreateUpdateStateInline: React.FC = ({ data, onClose, select reset({ name: "", color: "#000000", group: "backlog" }); }; - const onSubmit = async (formData: IState) => { + const handleCreate = async (formData: IState) => { if (!workspaceSlug || !projectId || isSubmitting) return; + await projectStateStore + .createState(workspaceSlug.toString(), projectId.toString(), formData) + .then(() => { + handleClose(); + setToastAlert({ + type: "success", + title: "Success!", + message: "State created successfully.", + }); + }) + .catch((error) => { + if (error.status === 400) + setToastAlert({ + type: "error", + title: "Error!", + message: "State with that name already exists. Please try again with another name.", + }); + else + setToastAlert({ + type: "error", + title: "Error!", + message: "State could not be created. Please try again.", + }); + }); + }; + + const handleUpdate = async (formData: IState) => { + if (!workspaceSlug || !projectId || !data || isSubmitting) return; + + await projectStateStore + .updateState(workspaceSlug.toString(), projectId.toString(), data.id, formData) + .then(() => { + mutate(STATES_LIST(projectId.toString())); + handleClose(); + + setToastAlert({ + type: "success", + title: "Success!", + message: "State updated successfully.", + }); + }) + .catch((error) => { + if (error.status === 400) + setToastAlert({ + type: "error", + title: "Error!", + message: "Another state exists with the same name. Please try again with another name.", + }); + else + setToastAlert({ + type: "error", + title: "Error!", + message: "State could not be updated. Please try again.", + }); + }); + }; + + const onSubmit = async (formData: IState) => { const payload: IState = { ...formData, }; - if (!data) { - await projectStateService - .createState(workspaceSlug.toString(), projectId.toString(), { ...payload }, user) - .then((res) => { - mutate( - STATES_LIST(projectId.toString()), - (prevData) => { - if (!prevData) return prevData; - - return { - ...prevData, - [res.group]: [...prevData[res.group], res], - }; - }, - false - ); - handleClose(); - - setToastAlert({ - type: "success", - title: "Success!", - message: "State created successfully.", - }); - }) - .catch((err) => { - if (err.status === 400) - setToastAlert({ - type: "error", - title: "Error!", - message: "State with that name already exists. Please try again with another name.", - }); - else - setToastAlert({ - type: "error", - title: "Error!", - message: "State could not be created. Please try again.", - }); - }); - } else { - await projectStateService - .updateState( - workspaceSlug.toString(), - projectId.toString(), - data.id, - { - ...payload, - }, - user - ) - .then(() => { - mutate(STATES_LIST(projectId.toString())); - handleClose(); - - setToastAlert({ - type: "success", - title: "Success!", - message: "State updated successfully.", - }); - }) - .catch((err) => { - if (err.status === 400) - setToastAlert({ - type: "error", - title: "Error!", - message: "Another state exists with the same name. Please try again with another name.", - }); - else - setToastAlert({ - type: "error", - title: "Error!", - message: "State could not be updated. Please try again.", - }); - }); - } + if (data) await handleUpdate(payload); + else await handleCreate(payload); }; return ( @@ -281,4 +273,4 @@ export const CreateUpdateStateInline: React.FC = ({ data, onClose, select ); -}; +}); diff --git a/web/components/states/delete-state-modal.tsx b/web/components/states/delete-state-modal.tsx index 726be5007..b0c609702 100644 --- a/web/components/states/delete-state-modal.tsx +++ b/web/components/states/delete-state-modal.tsx @@ -1,35 +1,38 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -import { mutate } from "swr"; import { Dialog, Transition } from "@headlessui/react"; + +// store +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; // icons import { AlertTriangle } from "lucide-react"; -// services -import { ProjectStateService } from "services/project"; // hooks import useToast from "hooks/use-toast"; // ui import { Button } from "@plane/ui"; // types -import type { IUser, IState, IStateResponse } from "types"; -// fetch-keys -import { STATES_LIST } from "constants/fetch-keys"; +import type { IState } from "types"; type Props = { isOpen: boolean; onClose: () => void; data: IState | null; - user: IUser | undefined; }; -const projectStateService = new ProjectStateService(); - -export const DeleteStateModal: React.FC = ({ isOpen, onClose, data, user }) => { - const [isDeleteLoading, setIsDeleteLoading] = useState(false); +export const DeleteStateModal: React.FC = observer((props) => { + const { isOpen, onClose, data } = props; + // router const router = useRouter(); const { workspaceSlug } = router.query; + // store + const { projectState: projectStateStore } = useMobxStore(); + + // states + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const { setToastAlert } = useToast(); const handleClose = () => { @@ -42,28 +45,12 @@ export const DeleteStateModal: React.FC = ({ isOpen, onClose, data, user setIsDeleteLoading(true); - await projectStateService - .deleteState(workspaceSlug as string, data.project, data.id, user) + await projectStateStore + .deleteState(workspaceSlug.toString(), data.project, data.id) .then(() => { - mutate( - STATES_LIST(data.project), - (prevData) => { - if (!prevData) return prevData; - - const stateGroup = [...prevData[data.group]].filter((s) => s.id !== data.id); - - return { - ...prevData, - [data.group]: stateGroup, - }; - }, - false - ); handleClose(); }) .catch((err) => { - setIsDeleteLoading(false); - if (err.status === 400) setToastAlert({ type: "error", @@ -77,6 +64,9 @@ export const DeleteStateModal: React.FC = ({ isOpen, onClose, data, user title: "Error!", message: "State could not be deleted. Please try again.", }); + }) + .finally(() => { + setIsDeleteLoading(false); }); }; @@ -141,4 +131,4 @@ export const DeleteStateModal: React.FC = ({ isOpen, onClose, data, user ); -}; +}); diff --git a/web/components/states/index.ts b/web/components/states/index.ts index 96c26eee3..f10af487a 100644 --- a/web/components/states/index.ts +++ b/web/components/states/index.ts @@ -1,5 +1,6 @@ export * from "./create-update-state-inline"; export * from "./create-state-modal"; export * from "./delete-state-modal"; -export * from "./single-state"; +export * from "./project-setting-state-list-item"; export * from "./state-select"; +export * from "./project-setting-state-list"; diff --git a/web/components/states/single-state.tsx b/web/components/states/project-setting-state-list-item.tsx similarity index 52% rename from web/components/states/single-state.tsx rename to web/components/states/project-setting-state-list-item.tsx index ec0282fb4..7fc5dfc88 100644 --- a/web/components/states/single-state.tsx +++ b/web/components/states/project-setting-state-list-item.tsx @@ -1,24 +1,18 @@ import { useState } from "react"; - import { useRouter } from "next/router"; -import { mutate } from "swr"; - -// services -import { ProjectStateService } from "services/project"; +// store +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; // ui import { Tooltip, StateGroupIcon } from "@plane/ui"; // icons -import { ArrowDown, ArrowUp, Pencil, X } from "lucide-react"; +import { Pencil, X, ArrowDown, ArrowUp } from "lucide-react"; // helpers import { addSpaceIfCamelCase } from "helpers/string.helper"; -import { groupBy, orderArrayBy } from "helpers/array.helper"; -import { orderStateGroups } from "helpers/state.helper"; // types -import { IUser, IState } from "types"; -// fetch-keys -import { STATES_LIST } from "constants/fetch-keys"; +import { IState } from "types"; type Props = { index: number; @@ -26,127 +20,39 @@ type Props = { statesList: IState[]; handleEditState: () => void; handleDeleteState: () => void; - user: IUser | undefined; }; -// services -const projectStateService = new ProjectStateService(); - -export const SingleState: React.FC = ({ - index, - state, - statesList, - handleEditState, - handleDeleteState, - user, -}) => { - const [isSubmitting, setIsSubmitting] = useState(false); +export const ProjectSettingListItem: React.FC = observer((props) => { + const { index, state, statesList, handleEditState, handleDeleteState } = props; + // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; + // store + const { projectState: projectStateStore } = useMobxStore(); + + // states + const [isSubmitting, setIsSubmitting] = useState(false); + + // derived values const groupStates = statesList.filter((s) => s.group === state.group); const groupLength = groupStates.length; const handleMakeDefault = () => { + if (!workspaceSlug || !projectId) return; + setIsSubmitting(true); - const currentDefaultState = statesList.find((s) => s.default); - - let newStatesList = statesList.map((s) => ({ - ...s, - default: s.id === state.id ? true : s.id === currentDefaultState?.id ? false : s.default, - })); - newStatesList = orderArrayBy(newStatesList, "sequence", "ascending"); - - mutate(STATES_LIST(projectId as string), orderStateGroups(groupBy(newStatesList, "group")), false); - - if (currentDefaultState) - projectStateService - .patchState( - workspaceSlug as string, - projectId as string, - currentDefaultState?.id ?? "", - { - default: false, - }, - user - ) - .then(() => { - projectStateService - .patchState( - workspaceSlug as string, - projectId as string, - state.id, - { - default: true, - }, - user - ) - .then(() => { - mutate(STATES_LIST(projectId as string)); - setIsSubmitting(false); - }) - .catch(() => { - setIsSubmitting(false); - }); - }); - else - projectStateService - .patchState( - workspaceSlug as string, - projectId as string, - state.id, - { - default: true, - }, - user - ) - .then(() => { - mutate(STATES_LIST(projectId as string)); - setIsSubmitting(false); - }) - .catch(() => { - setIsSubmitting(false); - }); + projectStateStore.markStateAsDefault(workspaceSlug.toString(), projectId.toString(), state.id).finally(() => { + setIsSubmitting(false); + }); }; const handleMove = (state: IState, direction: "up" | "down") => { - let newSequence = 15000; + if (!workspaceSlug || !projectId) return; - if (direction === "up") { - if (index === 1) newSequence = groupStates[0].sequence - 15000; - else newSequence = (groupStates[index - 2].sequence + groupStates[index - 1].sequence) / 2; - } else { - if (index === groupLength - 2) newSequence = groupStates[groupLength - 1].sequence + 15000; - else newSequence = (groupStates[index + 2].sequence + groupStates[index + 1].sequence) / 2; - } - - let newStatesList = statesList.map((s) => ({ - ...s, - sequence: s.id === state.id ? newSequence : s.sequence, - })); - newStatesList = orderArrayBy(newStatesList, "sequence", "ascending"); - - mutate(STATES_LIST(projectId as string), orderStateGroups(groupBy(newStatesList, "group")), false); - - projectStateService - .patchState( - workspaceSlug as string, - projectId as string, - state.id, - { - sequence: newSequence, - }, - user - ) - .then((res) => { - console.log(res); - mutate(STATES_LIST(projectId as string)); - }) - .catch((err) => { - console.error(err); - }); + projectStateStore.moveStatePosition(workspaceSlug.toString(), projectId.toString(), state.id, direction, index); }; return ( @@ -223,4 +129,4 @@ export const SingleState: React.FC = ({ ); -}; +}); diff --git a/web/components/states/project-setting-state-list.tsx b/web/components/states/project-setting-state-list.tsx new file mode 100644 index 000000000..d8405f675 --- /dev/null +++ b/web/components/states/project-setting-state-list.tsx @@ -0,0 +1,129 @@ +import React, { useState } from "react"; + +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// store +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; +// components +import { CreateUpdateStateInline, DeleteStateModal, ProjectSettingListItem, StateGroup } from "components/states"; +// ui +import { Loader } from "@plane/ui"; +// icons +import { Plus } from "lucide-react"; +// helpers +import { getStatesList, orderStateGroups } from "helpers/state.helper"; +// fetch-keys +import { STATES_LIST } from "constants/fetch-keys"; + +export const ProjectSettingStateList: React.FC = observer(() => { + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + // store + const { project: projectStore } = useMobxStore(); + + // state + const [activeGroup, setActiveGroup] = useState(null); + const [selectedState, setSelectedState] = useState(null); + const [selectDeleteState, setSelectDeleteState] = useState(null); + + useSWR( + workspaceSlug && projectId ? "PROJECT_DETAILS" : null, + workspaceSlug && projectId + ? () => projectStore.fetchProjectDetails(workspaceSlug.toString(), projectId.toString()) + : null + ); + + useSWR( + workspaceSlug && projectId ? STATES_LIST(projectId.toString()) : null, + workspaceSlug && projectId + ? () => projectStore.fetchProjectStates(workspaceSlug.toString(), projectId.toString()) + : null + ); + + // derived values + const states = projectStore.projectStatesByGroups; + const projectDetails = projectStore.project_details[projectId?.toString()!] ?? null; + const orderedStateGroups = orderStateGroups(states!); + const statesList = getStatesList(orderedStateGroups); + + return ( + <> + setSelectDeleteState(null)} + data={statesList?.find((s) => s.id === selectDeleteState) ?? null} + /> + +
+ {states && projectDetails && orderedStateGroups ? ( + Object.keys(orderedStateGroups || {}).map((key) => { + if (orderedStateGroups[key].length !== 0) + return ( +
+
+

{key}

+ +
+
+ {key === activeGroup && ( + { + setActiveGroup(null); + setSelectedState(null); + }} + selectedGroup={key as keyof StateGroup} + /> + )} + {orderedStateGroups[key].map((state, index) => + state.id !== selectedState ? ( + setSelectedState(state.id)} + handleDeleteState={() => setSelectDeleteState(state.id)} + /> + ) : ( +
+ { + setActiveGroup(null); + setSelectedState(null); + }} + groupLength={orderedStateGroups[key].length} + data={statesList?.find((state) => state.id === selectedState) ?? null} + selectedGroup={key as keyof StateGroup} + /> +
+ ) + )} +
+
+ ); + }) + ) : ( + + + + + + + )} +
+ + ); +}); diff --git a/web/components/states/state-select.tsx b/web/components/states/state-select.tsx index ba756da50..b3765e767 100644 --- a/web/components/states/state-select.tsx +++ b/web/components/states/state-select.tsx @@ -21,17 +21,19 @@ type Props = { disabled?: boolean; }; -export const StateSelect: React.FC = ({ - value, - onChange, - states, - className = "", - buttonClassName = "", - optionsClassName = "", - placement, - hideDropdownArrow = false, - disabled = false, -}) => { +export const StateSelect: React.FC = (props) => { + const { + value, + onChange, + states, + className = "", + buttonClassName = "", + optionsClassName = "", + placement, + hideDropdownArrow = false, + disabled = false, + } = props; + const [query, setQuery] = useState(""); const [referenceElement, setReferenceElement] = useState(null); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx index a6b17ace2..cacdc7319 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx @@ -602,7 +602,6 @@ const SinglePage: NextPage = () => { isOpen={labelModal} handleClose={() => setLabelModal(false)} projectId={projectId} - user={user} onSuccess={(response) => { partialUpdatePage({ labels: [...(pageDetails.labels ?? []), response.id], diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx index 0bfbd04b0..39af14681 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/estimates.tsx @@ -1,179 +1,21 @@ -import React, { useState } from "react"; -import { useRouter } from "next/router"; - -import useSWR, { mutate } from "swr"; -// services -import { ProjectService, ProjectEstimateService } from "services/project"; -// hooks -import useProjectDetails from "hooks/use-project-details"; +import React from "react"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/setting-layout"; // components -import { CreateUpdateEstimateModal, SingleEstimate } from "components/estimates"; import { ProjectSettingHeader } from "components/headers"; -//hooks -import useToast from "hooks/use-toast"; -import useUserAuth from "hooks/use-user-auth"; -// ui -import { Button, Loader } from "@plane/ui"; -import { EmptyState } from "components/common"; -// icons -import { Plus } from "lucide-react"; -// images -import emptyEstimate from "public/empty-state/estimate.svg"; +import { EstimatesList } from "components/estimates/estimate-list"; // types -import { IEstimate, IProject } from "types"; import type { NextPage } from "next"; -// fetch-keys -import { ESTIMATES_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; -// services -const projectService = new ProjectService(); -const projectEstimateService = new ProjectEstimateService(); - -const EstimatesSettings: NextPage = () => { - const [estimateFormOpen, setEstimateFormOpen] = useState(false); - - const [estimateToUpdate, setEstimateToUpdate] = useState(); - - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { user } = useUserAuth(); - - const { setToastAlert } = useToast(); - - const { projectDetails } = useProjectDetails(); - - const { data: estimatesList } = useSWR( - workspaceSlug && projectId ? ESTIMATES_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => projectEstimateService.getEstimatesList(workspaceSlug as string, projectId as string) - : null - ); - - const editEstimate = (estimate: IEstimate) => { - setEstimateToUpdate(estimate); - setEstimateFormOpen(true); - }; - - const removeEstimate = (estimateId: string) => { - if (!workspaceSlug || !projectId) return; - - mutate( - ESTIMATES_LIST(projectId as string), - (prevData) => (prevData ?? []).filter((p) => p.id !== estimateId), - false - ); - - projectEstimateService.deleteEstimate(workspaceSlug as string, projectId as string, estimateId, user).catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Error: Estimate could not be deleted. Please try again", - }); - }); - }; - - const disableEstimates = () => { - if (!workspaceSlug || !projectId) return; - - mutate( - PROJECT_DETAILS(projectId as string), - (prevData) => { - if (!prevData) return prevData; - - return { ...prevData, estimate: null }; - }, - false - ); - - projectService.updateProject(workspaceSlug as string, projectId as string, { estimate: null }, user).catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "Estimate could not be disabled. Please try again", - }) - ); - }; - - return ( - } withProjectWrapper> - - { - setEstimateFormOpen(false); - setEstimateToUpdate(undefined); - }} - user={user} - /> -
-
-

Estimates

-
-
- - {projectDetails?.estimate && ( - - )} -
-
-
- {estimatesList ? ( - estimatesList.length > 0 ? ( -
- {estimatesList.map((estimate) => ( - editEstimate(estimate)} - handleEstimateDelete={(estimateId) => removeEstimate(estimateId)} - user={user} - /> - ))} -
- ) : ( -
- , - text: "Add Estimate", - onClick: () => { - setEstimateToUpdate(undefined); - setEstimateFormOpen(true); - }, - }} - /> -
- ) - ) : ( - - - - - - - )} -
-
-
- ); -}; +const EstimatesSettings: NextPage = () => ( + } withProjectWrapper> + +
+ +
+
+
+); export default EstimatesSettings; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx index 0011ceff8..041db27c8 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx @@ -11,7 +11,7 @@ import { ProjectSettingLayout } from "layouts/setting-layout"; import { IntegrationService } from "services/integrations"; import { ProjectService } from "services/project"; // components -import { SingleIntegration } from "components/project"; +import { IntegrationCard } from "components/project"; import { ProjectSettingHeader } from "components/headers"; // ui import { EmptyState } from "components/common"; @@ -23,8 +23,6 @@ import { IProject } from "types"; import type { NextPage } from "next"; // fetch-keys import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; -// helper -// import { useMobxStore } from "lib/mobx/store-provider"; // services const integrationService = new IntegrationService(); @@ -34,10 +32,6 @@ const ProjectIntegrations: NextPage = () => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // const { project: projectStore } = useMobxStore(); - - // const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : null; - const { data: projectDetails } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null @@ -51,7 +45,7 @@ const ProjectIntegrations: NextPage = () => { const isAdmin = projectDetails?.member_role === 20; return ( - }> + }>
@@ -61,7 +55,7 @@ const ProjectIntegrations: NextPage = () => { workspaceIntegrations.length > 0 ? (
{workspaceIntegrations.map((integration) => ( - + ))}
) : ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx index 6b0ff25e1..6f3575c47 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx @@ -1,189 +1,22 @@ -import React, { useState, useRef } from "react"; +import React from "react"; -import { useRouter } from "next/router"; - -import useSWR from "swr"; - -// hooks -import useUserAuth from "hooks/use-user-auth"; -// services -import { IssueLabelService } from "services/issue"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/setting-layout"; // components -import { - CreateUpdateLabelInline, - DeleteLabelModal, - LabelsListModal, - SingleLabel, - SingleLabelGroup, -} from "components/labels"; +import { ProjectSettingsLabelList } from "components/labels"; import { ProjectSettingHeader } from "components/headers"; -// ui -import { Button, Loader } from "@plane/ui"; -import { EmptyState } from "components/common"; -// images -import emptyLabel from "public/empty-state/label.svg"; // types -import { IIssueLabels } from "types"; import type { NextPage } from "next"; -// fetch-keys -import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; -// services -const issueLabelService = new IssueLabelService(); - -const LabelsSettings: NextPage = () => { - // create/edit label form - const [labelForm, setLabelForm] = useState(false); - - // edit label - const [isUpdating, setIsUpdating] = useState(false); - const [labelToUpdate, setLabelToUpdate] = useState(null); - - // labels list modal - const [labelsListModal, setLabelsListModal] = useState(false); - const [parentLabel, setParentLabel] = useState(undefined); - - // delete label - const [selectDeleteLabel, setSelectDeleteLabel] = useState(null); - - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { user } = useUserAuth(); - - const scrollToRef = useRef(null); - - const { data: issueLabels } = useSWR( - workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, - workspaceSlug && projectId - ? () => issueLabelService.getProjectIssueLabels(workspaceSlug as string, projectId as string) - : null - ); - - const newLabel = () => { - setIsUpdating(false); - setLabelForm(true); - }; - - const addLabelToGroup = (parentLabel: IIssueLabels) => { - setLabelsListModal(true); - setParentLabel(parentLabel); - }; - - const editLabel = (label: IIssueLabels) => { - setLabelForm(true); - setIsUpdating(true); - setLabelToUpdate(label); - }; - - return ( - <> - setLabelsListModal(false)} - parent={parentLabel} - user={user} - /> - setSelectDeleteLabel(null)} - user={user} - /> - }> - -
-
-

Labels

- - -
-
- {labelForm && ( - { - setLabelForm(false); - setIsUpdating(false); - setLabelToUpdate(null); - }} - ref={scrollToRef} - /> - )} - <> - {issueLabels ? ( - issueLabels.length > 0 ? ( - issueLabels.map((label) => { - const children = issueLabels?.filter((l) => l.parent === label.id); - - if (children && children.length === 0) { - if (!label.parent) - return ( - addLabelToGroup(label)} - editLabel={(label) => { - editLabel(label); - scrollToRef.current?.scrollIntoView({ - behavior: "smooth", - }); - }} - handleLabelDelete={() => setSelectDeleteLabel(label)} - /> - ); - } else - return ( - { - editLabel(label); - scrollToRef.current?.scrollIntoView({ - behavior: "smooth", - }); - }} - handleLabelDelete={() => setSelectDeleteLabel(label)} - user={user} - /> - ); - }) - ) : ( - newLabel(), - }} - /> - ) - ) : ( - - - - - - - )} - -
-
-
-
- - ); -}; +const LabelsSettings: NextPage = () => ( + }> + +
+ +
+
+
+); export default LabelsSettings; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx index c55b33710..f1d6eac2c 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx @@ -1,436 +1,21 @@ -import { useState, useEffect } from "react"; -import { useRouter } from "next/router"; -import Link from "next/link"; -import useSWR, { mutate } from "swr"; -// services -import { ProjectService, ProjectInvitationService } from "services/project"; -import { WorkspaceService } from "services/workspace.service"; -// hooks -import useToast from "hooks/use-toast"; -import useUser from "hooks/use-user"; -import useProjectMembers from "hooks/use-project-members"; -import useProjectDetails from "hooks/use-project-details"; -import { Controller, useForm } from "react-hook-form"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/setting-layout"; // components -import ConfirmProjectMemberRemove from "components/project/confirm-project-member-remove"; -import SendProjectInvitationModal from "components/project/send-project-invitation-modal"; -import { MemberSelect } from "components/project"; import { ProjectSettingHeader } from "components/headers"; -// ui -import { Button, CustomMenu, CustomSelect, Loader } from "@plane/ui"; -// icons -import { ChevronDown, X } from "lucide-react"; +import { ProjectMemberList, ProjectSettingsMemberDefaults } from "components/project"; // types import type { NextPage } from "next"; -import { IProject, IUserLite, IWorkspace } from "types"; -// fetch-keys -import { - PROJECTS_LIST, - PROJECT_DETAILS, - PROJECT_INVITATIONS, - PROJECT_MEMBERS, - USER_PROJECT_VIEW, - WORKSPACE_DETAILS, -} from "constants/fetch-keys"; -// constants -import { ROLE } from "constants/workspace"; -const defaultValues: Partial = { - project_lead: null, - default_assignee: null, -}; - -// services -const projectService = new ProjectService(); -const projectInvitationService = new ProjectInvitationService(); -const workspaceService = new WorkspaceService(); - -const MembersSettings: NextPage = () => { - const [inviteModal, setInviteModal] = useState(false); - const [selectedRemoveMember, setSelectedRemoveMember] = useState(null); - const [selectedInviteRemoveMember, setSelectedInviteRemoveMember] = useState(null); - - const { setToastAlert } = useToast(); - - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { user } = useUser(); - const { projectDetails } = useProjectDetails(); - const { isOwner } = useProjectMembers( - workspaceSlug?.toString(), - projectId?.toString(), - Boolean(workspaceSlug && projectId) - ); - - const { reset, control } = useForm({ defaultValues }); - - const { data: activeWorkspace } = useSWR(workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug.toString()) : null, () => - workspaceSlug ? workspaceService.getWorkspace(workspaceSlug.toString()) : null - ); - - const { data: projectMembers, mutate: mutateMembers } = useSWR( - workspaceSlug && projectId ? PROJECT_MEMBERS(projectId.toString()) : null, - workspaceSlug && projectId - ? () => projectService.fetchProjectMembers(workspaceSlug.toString(), projectId.toString()) - : null - ); - - const { data: projectInvitations, mutate: mutateInvitations } = useSWR( - workspaceSlug && projectId ? PROJECT_INVITATIONS(projectId.toString()) : null, - workspaceSlug && projectId - ? () => projectInvitationService.fetchProjectInvitations(workspaceSlug.toString(), projectId.toString()) - : null - ); - - const { data: memberDetails } = useSWR( - workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null, - workspaceSlug && projectId - ? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString()) - : null - ); - - const members = [ - ...(projectMembers?.map((item) => ({ - id: item.id, - memberId: item.member?.id, - avatar: item.member?.avatar, - first_name: item.member?.first_name, - last_name: item.member?.last_name, - email: item.member?.email, - display_name: item.member?.display_name, - role: item.role, - status: true, - member: true, - })) || []), - ...(projectInvitations?.map((item: any) => ({ - id: item.id, - memberId: item.id, - avatar: item.avatar ?? "", - first_name: item.first_name ?? item.email, - last_name: item.last_name ?? "", - email: item.email, - display_name: item.email, - role: item.role, - status: item.accepted, - member: false, - })) || []), - ]; - - const currentUser = projectMembers?.find((item) => item.member.id === user?.id); - - // const handleProjectInvitationSuccess = () => {}; - - // const onSubmit = async (formData: IProject) => { - // if (!workspaceSlug || !projectId || !projectDetails) return; - - // const payload: Partial = { - // default_assignee: formData.default_assignee, - // project_lead: formData.project_lead === "none" ? null : formData.project_lead, - // }; - - // await projectService - // .updateProject(workspaceSlug.toString(), projectId.toString(), payload, user) - // .then((res) => { - // mutate(PROJECT_DETAILS(projectId.toString())); - - // mutate( - // PROJECTS_LIST(workspaceSlug.toString(), { - // is_favorite: "all", - // }) - // ); - - // setToastAlert({ - // title: "Success", - // type: "success", - // message: "Project updated successfully", - // }); - // }) - // .catch((err) => { - // console.log(err); - // }); - // }; - - useEffect(() => { - if (projectDetails) - reset({ - ...projectDetails, - default_assignee: projectDetails.default_assignee?.id ?? projectDetails.default_assignee, - project_lead: (projectDetails.project_lead as IUserLite)?.id ?? projectDetails.project_lead, - workspace: (projectDetails.workspace as IWorkspace).id, - }); - }, [projectDetails, reset]); - - const submitChanges = async (formData: Partial) => { - if (!workspaceSlug || !projectId) return; - - const payload: Partial = { - default_assignee: formData.default_assignee === "none" ? null : formData.default_assignee, - project_lead: formData.project_lead === "none" ? null : formData.project_lead, - }; - - await projectService - .updateProject(workspaceSlug.toString(), projectId.toString(), payload, user) - .then(() => { - mutate(PROJECT_DETAILS(projectId.toString())); - - mutate( - PROJECTS_LIST(workspaceSlug.toString(), { - is_favorite: "all", - }) - ); - - setToastAlert({ - title: "Success", - type: "success", - message: "Project updated successfully", - }); - }) - .catch((err) => { - console.log(err); - }); - }; - - const isAdmin = memberDetails?.role === 20; - - return ( - }> - - { - setSelectedRemoveMember(null); - setSelectedInviteRemoveMember(null); - }} - data={members.find((item) => item.id === selectedRemoveMember || item.id === selectedInviteRemoveMember)} - handleDelete={async () => { - if (!activeWorkspace || !projectDetails) return; - if (selectedRemoveMember) { - await projectService.deleteProjectMember(activeWorkspace.slug, projectDetails.id, selectedRemoveMember); - mutateMembers( - (prevData: any) => prevData?.filter((item: any) => item.id !== selectedRemoveMember), - false - ); - } - if (selectedInviteRemoveMember) { - await projectInvitationService.deleteProjectInvitation( - activeWorkspace.slug, - projectDetails.id, - selectedInviteRemoveMember - ); - mutateInvitations( - (prevData: any) => prevData?.filter((item: any) => item.id !== selectedInviteRemoveMember), - false - ); - } - setToastAlert({ - type: "success", - message: "Member removed successfully", - title: "Success", - }); - }} - /> - mutateMembers()} - /> -
-
-

Defaults

-
-
-
-
-

Project Lead

-
- {projectDetails ? ( - ( - { - submitChanges({ project_lead: val }); - }} - isDisabled={!isAdmin} - /> - )} - /> - ) : ( - - - - )} -
-
- -
-

Default Assignee

-
- {projectDetails ? ( - ( - { - submitChanges({ default_assignee: val }); - }} - isDisabled={!isAdmin} - /> - )} - /> - ) : ( - - - - )} -
-
-
-
- -
-

Members

- -
- {!projectMembers || !projectInvitations ? ( - - - - - - - ) : ( -
- {members.length > 0 - ? members.map((member) => ( -
-
- {member.avatar && member.avatar !== "" ? ( -
- {member.display_name} -
- ) : member.display_name || member.email ? ( -
- {(member.display_name || member.email)?.charAt(0)} -
- ) : ( -
- ? -
- )} -
- {member.member ? ( - - - - {member.first_name} {member.last_name} - - ({member.display_name}) - - - ) : ( -

{member.display_name || member.email}

- )} - {isOwner &&

{member.email}

} -
-
-
- {!member.member && ( -
- Pending -
- )} - - - {ROLE[member.role as keyof typeof ROLE]} - - {member.memberId !== user?.id && } -
- } - value={member.role} - onChange={(value: 5 | 10 | 15 | 20 | undefined) => { - if (!activeWorkspace || !projectDetails) return; - - mutateMembers( - (prevData: any) => - prevData.map((m: any) => (m.id === member.id ? { ...m, role: value } : m)), - false - ); - - projectService - .updateProjectMember(activeWorkspace.slug, projectDetails.id, member.id, { - role: value, - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "An error occurred while updating member role. Please try again.", - }); - }); - }} - disabled={ - member.memberId === user?.id || - !member.member || - (currentUser && currentUser.role !== 20 && currentUser.role < member.role) - } - > - {Object.keys(ROLE).map((key) => { - if (currentUser && currentUser.role !== 20 && currentUser.role < parseInt(key)) return null; - - return ( - - <>{ROLE[parseInt(key) as keyof typeof ROLE]} - - ); - })} - - - { - if (member.member) setSelectedRemoveMember(member.id); - else setSelectedInviteRemoveMember(member.id); - }} - > - - - - {member.memberId !== user?.id ? "Remove member" : "Leave project"} - - - -
-
- )) - : null} -
- )} - - - - ); -}; +const MembersSettings: NextPage = () => ( + } withProjectWrapper> + +
+ + +
+
+
+); export default MembersSettings; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx index d27fe1dcc..4ab4369d9 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx @@ -1,142 +1,25 @@ -import React, { useState } from "react"; - -import { useRouter } from "next/router"; - -import useSWR from "swr"; - -// services -import { ProjectStateService } from "services/project"; -// hooks -import useProjectDetails from "hooks/use-project-details"; -import useUserAuth from "hooks/use-user-auth"; -// layouts +import React from "react"; +// layout import { AppLayout } from "layouts/app-layout"; -import { ProjectSettingLayout } from "layouts/setting-layout"; // components -import { CreateUpdateStateInline, DeleteStateModal, SingleState, StateGroup } from "components/states"; +import { ProjectSettingStateList } from "components/states"; +import { ProjectSettingLayout } from "layouts/setting-layout"; import { ProjectSettingHeader } from "components/headers"; -// ui -import { Loader } from "@plane/ui"; -// icons -import { Plus } from "lucide-react"; -// helpers -import { getStatesList, orderStateGroups } from "helpers/state.helper"; // types import type { NextPage } from "next"; -// fetch-keys -import { STATES_LIST } from "constants/fetch-keys"; -// services -const projectStateService = new ProjectStateService(); +const StatesSettings: NextPage = () => ( + }> + +
+
+

States

+
-const StatesSettings: NextPage = () => { - const [activeGroup, setActiveGroup] = useState(null); - const [selectedState, setSelectedState] = useState(null); - const [selectDeleteState, setSelectDeleteState] = useState(null); - - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { user } = useUserAuth(); - - const { projectDetails } = useProjectDetails(); - - const { data: states } = useSWR( - workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => projectStateService.getStates(workspaceSlug as string, projectId as string) - : null - ); - const orderedStateGroups = orderStateGroups(states); - const statesList = getStatesList(orderedStateGroups); - - return ( - <> - s.id === selectDeleteState) ?? null} - onClose={() => setSelectDeleteState(null)} - user={user} - /> - }> - -
-
-

States

-
-
- {states && projectDetails && orderedStateGroups ? ( - Object.keys(orderedStateGroups).map((key) => { - if (orderedStateGroups[key].length !== 0) - return ( -
-
-

{key}

- -
-
- {key === activeGroup && ( - { - setActiveGroup(null); - setSelectedState(null); - }} - data={null} - selectedGroup={key as keyof StateGroup} - user={user} - /> - )} - {orderedStateGroups[key].map((state, index) => - state.id !== selectedState ? ( - setSelectedState(state.id)} - handleDeleteState={() => setSelectDeleteState(state.id)} - user={user} - /> - ) : ( -
- { - setActiveGroup(null); - setSelectedState(null); - }} - groupLength={orderedStateGroups[key].length} - data={statesList?.find((state) => state.id === selectedState) ?? null} - selectedGroup={key as keyof StateGroup} - user={user} - /> -
- ) - )} -
-
- ); - }) - ) : ( - - - - - - - )} -
-
-
-
- - ); -}; + +
+
+
+); export default StatesSettings; diff --git a/web/store/project/index.ts b/web/store/project/index.ts index 38b2dfd08..ccd0abfaf 100644 --- a/web/store/project/index.ts +++ b/web/store/project/index.ts @@ -1,2 +1,5 @@ export * from "./project_publish.store"; export * from "./project.store"; +export * from "./project_estimates.store"; +export * from "./project_label_store"; +export * from "./project_state.store"; diff --git a/web/store/project/project.store.ts b/web/store/project/project.store.ts index 225ca76bc..a1716579c 100644 --- a/web/store/project/project.store.ts +++ b/web/store/project/project.store.ts @@ -57,7 +57,7 @@ export interface IProjectStore { fetchProjectStates: (workspaceSlug: string, projectId: string) => Promise; fetchProjectLabels: (workspaceSlug: string, projectId: string) => Promise; fetchProjectMembers: (workspaceSlug: string, projectId: string) => Promise; - fetchProjectEstimates: (workspaceSlug: string, projectId: string) => Promise; + fetchProjectEstimates: (workspaceSlug: string, projectId: string) => Promise; addProjectToFavorites: (workspaceSlug: string, projectId: string) => Promise; removeProjectFromFavorites: (workspaceSlug: string, projectId: string) => Promise; @@ -70,6 +70,15 @@ export interface IProjectStore { createProject: (workspaceSlug: string, data: any) => Promise; updateProject: (workspaceSlug: string, projectId: string, data: Partial) => Promise; deleteProject: (workspaceSlug: string, projectId: string) => Promise; + + // write operations + removeMemberFromProject: (workspaceSlug: string, projectId: string, memberId: string) => Promise; + updateMember: ( + workspaceSlug: string, + projectId: string, + memberId: string, + data: Partial + ) => Promise; } export class ProjectStore implements IProjectStore { @@ -117,6 +126,7 @@ export class ProjectStore implements IProjectStore { states: observable.ref, labels: observable.ref, members: observable.ref, + estimates: observable.ref, // computed searchedProjects: computed, @@ -140,6 +150,7 @@ export class ProjectStore implements IProjectStore { getProjectStateById: action, getProjectLabelById: action, getProjectMemberById: action, + getProjectEstimateById: action, fetchProjectStates: action, fetchProjectLabels: action, @@ -154,6 +165,10 @@ export class ProjectStore implements IProjectStore { createProject: action, updateProject: action, leaveProject: action, + + // write operations + removeMemberFromProject: action, + updateMember: action, }); this.rootStore = _rootStore; @@ -605,4 +620,58 @@ export class ProjectStore implements IProjectStore { console.log("Failed to delete project from project store"); } }; + + removeMemberFromProject = async (workspaceSlug: string, projectId: string, memberId: string) => { + const originalMembers = this.projectMembers || []; + + runInAction(() => { + this.members = { + ...this.members, + [projectId]: this.projectMembers?.filter((member) => member.id !== memberId) || [], + }; + }); + + try { + await this.projectService.deleteProjectMember(workspaceSlug, projectId, memberId); + await this.fetchProjectMembers(workspaceSlug, projectId); + } catch (error) { + console.log("Failed to delete project from project store"); + // revert back to original members in case of error + runInAction(() => { + this.members = { + ...this.members, + [projectId]: originalMembers, + }; + }); + } + }; + + updateMember = async (workspaceSlug: string, projectId: string, memberId: string, data: Partial) => { + const originalMembers = this.projectMembers || []; + + runInAction(() => { + this.members = { + ...this.members, + [projectId]: (this.projectMembers || [])?.map((member) => + member.id === memberId ? { ...member, ...data } : member + ), + }; + }); + + try { + const response = await this.projectService.updateProjectMember(workspaceSlug, projectId, memberId, data); + await this.fetchProjectMembers(workspaceSlug, projectId); + return response; + } catch (error) { + console.log("Failed to update project member from project store"); + // revert back to original members in case of error + runInAction(() => { + this.members = { + ...this.members, + [projectId]: originalMembers, + }; + }); + throw error; + } + }; } diff --git a/web/store/project/project_estimates.store.ts b/web/store/project/project_estimates.store.ts new file mode 100644 index 000000000..0cb51c2dc --- /dev/null +++ b/web/store/project/project_estimates.store.ts @@ -0,0 +1,141 @@ +import { observable, action, makeObservable, runInAction } from "mobx"; +// types +import { RootStore } from "../root"; +import { IEstimate, IEstimateFormData } from "types"; +// services +import { ProjectService, ProjectEstimateService } from "services/project"; + +export interface IProjectEstimateStore { + loader: boolean; + error: any | null; + + // estimates + createEstimate: (workspaceSlug: string, projectId: string, data: IEstimateFormData) => Promise; + updateEstimate: ( + workspaceSlug: string, + projectId: string, + estimateId: string, + data: IEstimateFormData + ) => Promise; + deleteEstimate: (workspaceSlug: string, projectId: string, estimateId: string) => Promise; +} + +export class ProjectEstimatesStore implements IProjectEstimateStore { + loader: boolean = false; + error: any | null = null; + + // root store + rootStore; + // service + projectService; + estimateService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable, + error: observable, + + // estimates + createEstimate: action, + updateEstimate: action, + deleteEstimate: action, + }); + + this.rootStore = _rootStore; + this.projectService = new ProjectService(); + this.estimateService = new ProjectEstimateService(); + } + + createEstimate = async (workspaceSlug: string, projectId: string, data: IEstimateFormData) => { + try { + const response = await this.estimateService.createEstimate( + workspaceSlug, + projectId, + data, + this.rootStore.user.currentUser! + ); + + const responseEstimate = { + ...response.estimate, + points: response.estimate_points, + }; + + runInAction(() => { + this.rootStore.project.estimates = { + ...this.rootStore.project.estimates, + [projectId]: [responseEstimate, ...(this.rootStore.project.estimates?.[projectId] || [])], + }; + }); + + return response; + } catch (error) { + console.log("Failed to create estimate from project store"); + throw error; + } + }; + + updateEstimate = async (workspaceSlug: string, projectId: string, estimateId: string, data: IEstimateFormData) => { + const originalEstimates = this.rootStore.project.getProjectEstimateById(estimateId); + + runInAction(() => { + this.rootStore.project.estimates = { + ...this.rootStore.project.estimates, + [projectId]: (this.rootStore.project.estimates?.[projectId] || [])?.map((estimate) => + estimate.id === estimateId ? { ...estimate, ...data.estimate } : estimate + ), + }; + }); + + try { + const response = await this.estimateService.patchEstimate( + workspaceSlug, + projectId, + estimateId, + data, + this.rootStore.user.currentUser! + ); + await this.rootStore.project.fetchProjectEstimates(workspaceSlug, projectId); + + return response; + } catch (error) { + console.log("Failed to update estimate from project store"); + runInAction(() => { + this.rootStore.project.estimates = { + ...this.rootStore.project.estimates, + [projectId]: (this.rootStore.project.estimates?.[projectId] || [])?.map((estimate) => + estimate.id === estimateId ? { ...estimate, ...originalEstimates } : estimate + ), + }; + }); + throw error; + } + }; + + deleteEstimate = async (workspaceSlug: string, projectId: string, estimateId: string) => { + const originalEstimateList = this.rootStore.project.projectEstimates || []; + + runInAction(() => { + this.rootStore.project.estimates = { + ...this.rootStore.project.estimates, + [projectId]: (this.rootStore.project.estimates?.[projectId] || [])?.filter( + (estimate) => estimate.id !== estimateId + ), + }; + }); + + try { + // deleting using api + await this.estimateService.deleteEstimate(workspaceSlug, projectId, estimateId, this.rootStore.user.currentUser!); + } catch (error) { + console.log("Failed to delete estimate from project store"); + // reverting back to original estimate list + runInAction(() => { + this.rootStore.project.estimates = { + ...this.rootStore.project.estimates, + [projectId]: originalEstimateList, + }; + }); + } + }; +} diff --git a/web/store/project/project_label_store.ts b/web/store/project/project_label_store.ts new file mode 100644 index 000000000..495b8e9f1 --- /dev/null +++ b/web/store/project/project_label_store.ts @@ -0,0 +1,140 @@ +import { observable, action, makeObservable, runInAction } from "mobx"; +// types +import { RootStore } from "../root"; +import { IIssueLabels } from "types"; +// services +import { IssueLabelService } from "services/issue"; +import { ProjectService } from "services/project"; + +export interface IProjectLabelStore { + loader: boolean; + error: any | null; + + // labels + createLabel: (workspaceSlug: string, projectId: string, data: Partial) => Promise; + updateLabel: ( + workspaceSlug: string, + projectId: string, + labelId: string, + data: Partial + ) => Promise; + deleteLabel: (workspaceSlug: string, projectId: string, labelId: string) => Promise; +} + +export class ProjectLabelStore implements IProjectLabelStore { + loader: boolean = false; + error: any | null = null; + + // root store + rootStore; + // service + projectService; + issueLabelService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable, + error: observable, + + // labels + createLabel: action, + updateLabel: action, + deleteLabel: action, + }); + + this.rootStore = _rootStore; + this.projectService = new ProjectService(); + this.issueLabelService = new IssueLabelService(); + } + + createLabel = async (workspaceSlug: string, projectId: string, data: Partial) => { + try { + const response = await this.issueLabelService.createIssueLabel( + workspaceSlug, + projectId, + data, + this.rootStore.user.currentUser! + ); + + runInAction(() => { + this.rootStore.project.labels = { + ...this.rootStore.project.labels, + [projectId]: [response, ...(this.rootStore.project.labels?.[projectId] || [])], + }; + }); + + return response; + } catch (error) { + console.log("Failed to create label from project store"); + throw error; + } + }; + + updateLabel = async (workspaceSlug: string, projectId: string, labelId: string, data: Partial) => { + const originalLabel = this.rootStore.project.getProjectLabelById(labelId); + + runInAction(() => { + this.rootStore.project.labels = { + ...this.rootStore.project.labels, + [projectId]: + this.rootStore.project.labels?.[projectId]?.map((label) => + label.id === labelId ? { ...label, ...data } : label + ) || [], + }; + }); + + try { + const response = await this.issueLabelService.patchIssueLabel( + workspaceSlug, + projectId, + labelId, + data, + this.rootStore.user.currentUser! + ); + + return response; + } catch (error) { + console.log("Failed to update label from project store"); + runInAction(() => { + this.rootStore.project.labels = { + ...this.rootStore.project.labels, + [projectId]: (this.rootStore.project.labels?.[projectId] || [])?.map((label) => + label.id === labelId ? { ...label, ...originalLabel } : label + ), + } as any; + }); + throw error; + } + }; + + deleteLabel = async (workspaceSlug: string, projectId: string, labelId: string) => { + const originalLabelList = this.rootStore.project.projectLabels; + + runInAction(() => { + this.rootStore.project.labels = { + ...this.rootStore.project.labels, + [projectId]: (this.rootStore.project.labels?.[projectId] || [])?.filter((label) => label.id !== labelId), + }; + }); + + try { + // deleting using api + await this.issueLabelService.deleteIssueLabel( + workspaceSlug, + projectId, + labelId, + this.rootStore.user.currentUser! + ); + } catch (error) { + console.log("Failed to delete label from project store"); + // reverting back to original label list + runInAction(() => { + this.rootStore.project.labels = { + ...this.rootStore.project.labels, + [projectId]: originalLabelList || [], + }; + }); + } + }; +} diff --git a/web/store/project/project_state.store.ts b/web/store/project/project_state.store.ts new file mode 100644 index 000000000..e5c7732fe --- /dev/null +++ b/web/store/project/project_state.store.ts @@ -0,0 +1,279 @@ +import { observable, action, makeObservable, runInAction } from "mobx"; +// types +import { RootStore } from "../root"; +import { IState } from "types"; +// services +import { ProjectService, ProjectStateService } from "services/project"; +import { groupBy, orderArrayBy } from "helpers/array.helper"; +import { orderStateGroups } from "helpers/state.helper"; + +export interface IProjectStateStore { + loader: boolean; + error: any | null; + + // states + createState: (workspaceSlug: string, projectId: string, data: Partial) => Promise; + updateState: (workspaceSlug: string, projectId: string, stateId: string, data: Partial) => Promise; + deleteState: (workspaceSlug: string, projectId: string, stateId: string) => Promise; + markStateAsDefault: (workspaceSlug: string, projectId: string, stateId: string) => Promise; + moveStatePosition: ( + workspaceSlug: string, + projectId: string, + stateId: string, + direction: "up" | "down", + groupIndex: number + ) => Promise; +} + +export class ProjectStateStore implements IProjectStateStore { + loader: boolean = false; + error: any | null = null; + + // root store + rootStore; + // service + projectService; + stateService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable, + error: observable, + + // states + createState: action, + updateState: action, + deleteState: action, + markStateAsDefault: action, + moveStatePosition: action, + }); + + this.rootStore = _rootStore; + this.projectService = new ProjectService(); + this.stateService = new ProjectStateService(); + } + + createState = async (workspaceSlug: string, projectId: string, data: Partial) => { + try { + const response = await this.stateService.createState( + workspaceSlug, + projectId, + data, + this.rootStore.user.currentUser! + ); + + runInAction(() => { + this.rootStore.project.states = { + ...this.rootStore.project.states, + [projectId]: { + ...this.rootStore.project.states?.[projectId], + [response.group]: [...(this.rootStore.project.states?.[projectId]?.[response.group] || []), response], + }, + }; + }); + + return response; + } catch (error) { + console.log("Failed to create state from project store"); + throw error; + } + }; + + updateState = async (workspaceSlug: string, projectId: string, stateId: string, data: Partial) => { + const originalStates = this.rootStore.project.states; + + runInAction(() => { + this.rootStore.project.states = { + ...this.rootStore.project.states, + [projectId]: { + ...this.rootStore.project.states?.[projectId], + [data.group as string]: (this.rootStore.project.states?.[projectId]?.[data.group as string] || []).map( + (state) => (state.id === stateId ? { ...state, ...data } : state) + ), + }, + }; + }); + + try { + const response = await this.stateService.patchState( + workspaceSlug, + projectId, + stateId, + data, + this.rootStore.user.currentUser! + ); + + runInAction(() => { + this.rootStore.project.states = { + ...this.rootStore.project.states, + [projectId]: { + ...this.rootStore.project.states?.[projectId], + [response.group]: (this.rootStore.project.states?.[projectId]?.[response.group] || []).map((state) => + state.id === stateId ? { ...state, ...response } : state + ), + }, + }; + }); + + return response; + } catch (error) { + console.log("Failed to update state from project store"); + runInAction(() => { + this.rootStore.project.states = { + ...this.rootStore.project.states, + [projectId]: { + ...this.rootStore.project.states?.[projectId], + [data.group as string]: originalStates || [], + }, + } as any; + }); + throw error; + } + }; + + deleteState = async (workspaceSlug: string, projectId: string, stateId: string) => { + const originalStates = this.rootStore.project.projectStates; + + try { + runInAction(() => { + this.rootStore.project.states = { + ...this.rootStore.project.states, + [projectId]: { + ...this.rootStore.project.states?.[projectId], + [originalStates?.[0]?.group || ""]: ( + this.rootStore.project.states?.[projectId]?.[originalStates?.[0]?.group || ""] || [] + ).filter((state) => state.id !== stateId), + }, + }; + }); + + // deleting using api + await this.stateService.deleteState(workspaceSlug, projectId, stateId, this.rootStore.user.currentUser!); + } catch (error) { + console.log("Failed to delete state from project store"); + // reverting back to original label list + runInAction(() => { + this.rootStore.project.states = { + ...this.rootStore.project.states, + [projectId]: { + ...this.rootStore.project.states?.[projectId], + [originalStates?.[0]?.group || ""]: originalStates || [], + }, + }; + }); + } + }; + + markStateAsDefault = async (workspaceSlug: string, projectId: string, stateId: string) => { + const states = this.rootStore.project.projectStates; + const currentDefaultState = states?.find((state) => state.default); + + let newStateList = + states?.map((state) => { + if (state.id === stateId) return { ...state, default: true }; + if (state.id === currentDefaultState?.id) return { ...state, default: false }; + return state; + }) ?? []; + newStateList = orderArrayBy(newStateList, "sequence", "ascending"); + + const newOrderedStateGroups = orderStateGroups(groupBy(newStateList, "group")); + const oldOrderedStateGroup = this.rootStore.project.states?.[projectId] || {}; // for reverting back to old state group if api fails + + runInAction(() => { + this.rootStore.project.states = { + ...this.rootStore.project.states, + [projectId]: newOrderedStateGroups || {}, + }; + }); + + // updating using api + try { + this.stateService.patchState( + workspaceSlug, + projectId, + stateId, + { default: true }, + this.rootStore.user.currentUser! + ); + + if (currentDefaultState) + this.stateService.patchState( + workspaceSlug, + projectId, + currentDefaultState.id, + { default: false }, + this.rootStore.user.currentUser! + ); + } catch (err) { + console.log("Failed to mark state as default"); + runInAction(() => { + this.rootStore.project.states = { + ...this.rootStore.project.states, + [projectId]: oldOrderedStateGroup, + }; + }); + } + }; + + moveStatePosition = async ( + workspaceSlug: string, + projectId: string, + stateId: string, + direction: "up" | "down", + groupIndex: number + ) => { + const SEQUENCE_GAP = 15000; + let newSequence = SEQUENCE_GAP; + + const states = this.rootStore.project.projectStates || []; + const groupedStates = groupBy(states || [], "group"); + + const selectedState = states?.find((state) => state.id === stateId); + const groupStates = states?.filter((state) => state.group === selectedState?.group); + const groupLength = groupStates.length; + + if (direction === "up") { + if (groupIndex === 1) newSequence = groupStates[0].sequence - SEQUENCE_GAP; + else newSequence = (groupStates[groupIndex - 2].sequence + groupStates[groupIndex - 1].sequence) / 2; + } else { + if (groupIndex === groupLength - 2) newSequence = groupStates[groupLength - 1].sequence + SEQUENCE_GAP; + else newSequence = (groupStates[groupIndex + 2].sequence + groupStates[groupIndex + 1].sequence) / 2; + } + + const newStateList = states?.map((state) => { + if (state.id === stateId) return { ...state, sequence: newSequence }; + return state; + }); + const newOrderedStateGroups = orderStateGroups( + groupBy(orderArrayBy(newStateList, "sequence", "ascending"), "group") + ); + + runInAction(() => { + this.rootStore.project.states = { + ...this.rootStore.project.states, + [projectId]: newOrderedStateGroups || {}, + }; + }); + + // updating using api + try { + await this.stateService.patchState( + workspaceSlug, + projectId, + stateId, + { sequence: newSequence }, + this.rootStore.user.currentUser! + ); + } catch (err) { + console.log("Failed to move state position"); + // reverting back to old state group if api fails + runInAction(() => { + this.rootStore.project.states = { + ...this.rootStore.project.states, + [projectId]: groupedStates, + }; + }); + } + }; +} diff --git a/web/store/root.ts b/web/store/root.ts index fabf7ed9d..525d18e72 100644 --- a/web/store/root.ts +++ b/web/store/root.ts @@ -19,7 +19,18 @@ import { IssueQuickAddStore, } from "store/issue"; import { IWorkspaceFilterStore, IWorkspaceStore, WorkspaceFilterStore, WorkspaceStore } from "store/workspace"; -import { IProjectPublishStore, IProjectStore, ProjectPublishStore, ProjectStore } from "store/project"; +import { + IProjectPublishStore, + IProjectStore, + ProjectPublishStore, + ProjectStore, + IProjectStateStore, + ProjectStateStore, + IProjectLabelStore, + ProjectLabelStore, + ProjectEstimatesStore, + IProjectEstimateStore, +} from "store/project"; import { IModuleFilterStore, IModuleIssueKanBanViewStore, @@ -99,6 +110,9 @@ export class RootStore { projectPublish: IProjectPublishStore; project: IProjectStore; + projectState: IProjectStateStore; + projectLabel: IProjectLabelStore; + projectEstimates: IProjectEstimateStore; issue: IIssueStore; module: IModuleStore; @@ -154,6 +168,9 @@ export class RootStore { this.workspaceFilter = new WorkspaceFilterStore(this); this.project = new ProjectStore(this); + this.projectState = new ProjectStateStore(this); + this.projectLabel = new ProjectLabelStore(this); + this.projectEstimates = new ProjectEstimatesStore(this); this.projectPublish = new ProjectPublishStore(this); this.module = new ModuleStore(this); diff --git a/yarn.lock b/yarn.lock index 6c3ee538e..e1a3a4a85 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1309,9 +1309,9 @@ eslint-visitor-keys "^3.3.0" "@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.9.1.tgz#449dfa81a57a1d755b09aa58d826c1262e4283b4" - integrity sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA== + version "4.10.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== "@eslint/eslintrc@^0.4.3": version "0.4.3" @@ -2193,25 +2193,25 @@ dependencies: "@daybrush/utils" "^1.4.0" -"@sentry-internal/tracing@7.75.0": - version "7.75.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.75.0.tgz#0d6cb4d3ff4ea6dd456f64455b2d505d7eb27656" - integrity sha512-/j4opF/jB9j8qnSiQK75/lFLtkfqXS5/MoOKc2KWK/pOaf15W+6uJzGQ8jRBHLYd9dDg6AyqsF48Wqy561/mNg== +"@sentry-internal/tracing@7.76.0": + version "7.76.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.76.0.tgz#36c54425bc20c08e569e6da52e13d325611cad66" + integrity sha512-QQVIv+LS2sbGf/e5P2dRisHzXpy02dAcLqENLPG4sZ9otRaFNjdFYEqnlJ4qko+ORpJGQEQp/BX7Q/qzZQHlAg== dependencies: - "@sentry/core" "7.75.0" - "@sentry/types" "7.75.0" - "@sentry/utils" "7.75.0" + "@sentry/core" "7.76.0" + "@sentry/types" "7.76.0" + "@sentry/utils" "7.76.0" -"@sentry/browser@7.75.0": - version "7.75.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.75.0.tgz#7ea88f335c7bbaf3b5eecbf4e12590785abc0ee7" - integrity sha512-DXH/69vzp2j8xjydX+lrUYasrk7a1mpbXFGA9GtnII7shMCy55+QkVxpa6cLojYUaG2K/8yFDMcrP9N395LnWg== +"@sentry/browser@7.76.0": + version "7.76.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.76.0.tgz#7d73573790023523f7d9c3757b8424b7ad60d664" + integrity sha512-83xA+cWrBhhkNuMllW5ucFsEO2NlUh2iBYtmg07lp3fyVW+6+b1yMKRnc4RFArJ+Wcq6UO+qk2ZEvrSAts1wEw== dependencies: - "@sentry-internal/tracing" "7.75.0" - "@sentry/core" "7.75.0" - "@sentry/replay" "7.75.0" - "@sentry/types" "7.75.0" - "@sentry/utils" "7.75.0" + "@sentry-internal/tracing" "7.76.0" + "@sentry/core" "7.76.0" + "@sentry/replay" "7.76.0" + "@sentry/types" "7.76.0" + "@sentry/utils" "7.76.0" "@sentry/cli@^1.74.6": version "1.75.2" @@ -2225,94 +2225,94 @@ proxy-from-env "^1.1.0" which "^2.0.2" -"@sentry/core@7.75.0": - version "7.75.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.75.0.tgz#d5477faf9afdfbf45b4ff46b809729f14d4e1b80" - integrity sha512-vXg3cdJgwzP24oTS9zFCgLW4MgTkMZqXx+ESRq7gTD9qJTpcmAmYT+Ckmvebg8K6DBThV6+0v61r50na2+XdrA== +"@sentry/core@7.76.0": + version "7.76.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.76.0.tgz#b0d1dc399a862ea8a1c8a1c60a409e92eaf8e9e1" + integrity sha512-M+ptkCTeCNf6fn7p2MmEb1Wd9/JXUWxIT/0QEc+t11DNR4FYy1ZP2O9Zb3Zp2XacO7ORrlL3Yc+VIfl5JTgjfw== dependencies: - "@sentry/types" "7.75.0" - "@sentry/utils" "7.75.0" + "@sentry/types" "7.76.0" + "@sentry/utils" "7.76.0" -"@sentry/integrations@7.75.0": - version "7.75.0" - resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.75.0.tgz#0dcee498b34b4075856d67698d317f8e3c0b96f0" - integrity sha512-dnKZvPJBj+KiOIteYJEVuZcB3Hcd6NYdQ3xJhGk5FD4+gGOHTF+8kMdBC6q+Rnkyc63IB0vPRMhhs/T5XbWByg== +"@sentry/integrations@7.76.0": + version "7.76.0" + resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.76.0.tgz#ea1b6d86609c5f25999f1d8d87383afdb00c77f0" + integrity sha512-4ea0PNZrGN9wKuE/8bBCRrxxw4Cq5T710y8rhdKHAlSUpbLqr/atRF53h8qH3Fi+ec0m38PB+MivKem9zUwlwA== dependencies: - "@sentry/core" "7.75.0" - "@sentry/types" "7.75.0" - "@sentry/utils" "7.75.0" + "@sentry/core" "7.76.0" + "@sentry/types" "7.76.0" + "@sentry/utils" "7.76.0" localforage "^1.8.1" "@sentry/nextjs@^7.36.0": - version "7.75.0" - resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-7.75.0.tgz#5f93743fbbe6add7b7a01fb3aebdb844c37ee2ec" - integrity sha512-EKdTUe5Q48qRgFM7T9s9sXwOEMvaouepHF5m343jSuTugTQ7CCJIR9jLGgUuRPgaUdE0F+PyJWopgVAZpaVFSg== + version "7.76.0" + resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-7.76.0.tgz#a3018ecf3df25cd8021aeaa8fb0314f8f6f6b38f" + integrity sha512-3/iTnBJ7qOrhoEUQw85CmZ+S2wTZapRui5yfWO6/We11T8q6tvrUPIYmnE0BY/2BIelz4dfPwXRHXRJlgEarhg== dependencies: "@rollup/plugin-commonjs" "24.0.0" - "@sentry/core" "7.75.0" - "@sentry/integrations" "7.75.0" - "@sentry/node" "7.75.0" - "@sentry/react" "7.75.0" - "@sentry/types" "7.75.0" - "@sentry/utils" "7.75.0" - "@sentry/vercel-edge" "7.75.0" + "@sentry/core" "7.76.0" + "@sentry/integrations" "7.76.0" + "@sentry/node" "7.76.0" + "@sentry/react" "7.76.0" + "@sentry/types" "7.76.0" + "@sentry/utils" "7.76.0" + "@sentry/vercel-edge" "7.76.0" "@sentry/webpack-plugin" "1.20.0" chalk "3.0.0" resolve "1.22.8" rollup "2.78.0" stacktrace-parser "^0.1.10" -"@sentry/node@7.75.0": - version "7.75.0" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.75.0.tgz#49f851d49e1c5cdaca64933ca5a9214edeed5e82" - integrity sha512-z5Xanf9QeTd4YrEuZiJfvtAy2C874Zg4KpurEo3okJ8uYjnbXMsQ3EwVHbKEoYSwE3ExTrqOggPfk2NNSJIECA== +"@sentry/node@7.76.0": + version "7.76.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.76.0.tgz#9efc8bbe4825b4a5a4f853210364d21980dd790e" + integrity sha512-C+YZ5S5W9oTphdWTBgV+3nDdcV1ldnupIHylHzf2Co+xNtJ76V06N5NjdJ/l9+qvQjMn0DdSp7Uu7KCEeNBT/g== dependencies: - "@sentry-internal/tracing" "7.75.0" - "@sentry/core" "7.75.0" - "@sentry/types" "7.75.0" - "@sentry/utils" "7.75.0" + "@sentry-internal/tracing" "7.76.0" + "@sentry/core" "7.76.0" + "@sentry/types" "7.76.0" + "@sentry/utils" "7.76.0" https-proxy-agent "^5.0.0" -"@sentry/react@7.75.0": - version "7.75.0" - resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.75.0.tgz#5299ad2f42832cb7fd733a5bc09edc1ca47d2251" - integrity sha512-v3293R4YSF4HXLf0AKr5Oa0+cctXiGAHlygiqatMdOrEh/HqjTm2YGIoE8uYUM3/aI+xsr7ZmJ1KS6o0WWR6yA== +"@sentry/react@7.76.0": + version "7.76.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.76.0.tgz#8c91f6401372c29c74cc4005aaed080b92ffc3f6" + integrity sha512-FtwB1TjCaHLbyAnEEu3gBdcnh/hhpC+j0dII5bOqp4YvmkGkXfgQcjZskZFX7GydMcRAjWX35s0VRjuBBZu/fA== dependencies: - "@sentry/browser" "7.75.0" - "@sentry/types" "7.75.0" - "@sentry/utils" "7.75.0" + "@sentry/browser" "7.76.0" + "@sentry/types" "7.76.0" + "@sentry/utils" "7.76.0" hoist-non-react-statics "^3.3.2" -"@sentry/replay@7.75.0": - version "7.75.0" - resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.75.0.tgz#0b1d9e9a6954ecc004597456f2c82e7630b8139c" - integrity sha512-TAAlj7JCMF6hFFL71RmPzVX89ltyPYFWR+t4SuWaBmU6HmTliI2eJvK+M36oE+N7s3CkyRVTaXXRe0YMwRMuZQ== +"@sentry/replay@7.76.0": + version "7.76.0" + resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.76.0.tgz#bccf9ea4a6efc332a79d6a78f923697b9b283371" + integrity sha512-OACT7MfMHC/YGKnKST8SF1d6znr3Yu8fpUpfVVh2t9TNeh3+cQJVTOliHDqLy+k9Ljd5FtitgSn4IHtseCHDLQ== dependencies: - "@sentry-internal/tracing" "7.75.0" - "@sentry/core" "7.75.0" - "@sentry/types" "7.75.0" - "@sentry/utils" "7.75.0" + "@sentry-internal/tracing" "7.76.0" + "@sentry/core" "7.76.0" + "@sentry/types" "7.76.0" + "@sentry/utils" "7.76.0" -"@sentry/types@7.75.0": - version "7.75.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.75.0.tgz#e171f1680785a155cb828942af890ad4ee657ca3" - integrity sha512-xG8OLADxG7HpGhMxrF4v4tKq/v/gqmLsTZ858R51pz0xCWM8SK6ZSWOKudkAGBIpRjI6RUHMnkBtRAN2aKDOkQ== +"@sentry/types@7.76.0": + version "7.76.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.76.0.tgz#628c9899bfa82ea762708314c50fd82f2138587d" + integrity sha512-vj6z+EAbVrKAXmJPxSv/clpwS9QjPqzkraMFk2hIdE/kii8s8kwnkBwTSpIrNc8GnzV3qYC4r3qD+BXDxAGPaw== -"@sentry/utils@7.75.0": - version "7.75.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.75.0.tgz#7a638c4c027ca2018518ee8d2eead1397cb97d66" - integrity sha512-UHWKeevhUNRp+mAWDbMVFOMgseoq8t/xFgdUywO/2PC14qZKRBH+0k1BKoNkp5sOzDT06ETj2w6wYoYhy6i+dA== +"@sentry/utils@7.76.0": + version "7.76.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.76.0.tgz#6b540b387d3ac539abd20978f4d3ae235114f6ab" + integrity sha512-40jFD+yfQaKpFYINghdhovzec4IEpB7aAuyH/GtE7E0gLpcqnC72r55krEIVILfqIR2Mlr5OKUzyeoCyWAU/yw== dependencies: - "@sentry/types" "7.75.0" + "@sentry/types" "7.76.0" -"@sentry/vercel-edge@7.75.0": - version "7.75.0" - resolved "https://registry.yarnpkg.com/@sentry/vercel-edge/-/vercel-edge-7.75.0.tgz#ef3cda5807b76692b210c3b89781116789af6504" - integrity sha512-A1ydzbyxoqgLidvgEW6saP2yts8xGTcxEcnETBI/8j95gQfQRwdtqWrYfYKHoTGMbMdGnE/UR4e+H1n1jL1CyQ== +"@sentry/vercel-edge@7.76.0": + version "7.76.0" + resolved "https://registry.yarnpkg.com/@sentry/vercel-edge/-/vercel-edge-7.76.0.tgz#8986d4b7cb1f1dcf0ed6e5f34d5b9ce441f707dc" + integrity sha512-CU/besmv2SWNfVh4v7yVs1VknxU4aG7+kIW001wTYnaNXF8IjV8Bgyn0lDRxFuBXRcrTn8KJO/rUN7aJEmeg4Q== dependencies: - "@sentry/core" "7.75.0" - "@sentry/types" "7.75.0" - "@sentry/utils" "7.75.0" + "@sentry/core" "7.76.0" + "@sentry/types" "7.76.0" + "@sentry/utils" "7.76.0" "@sentry/webpack-plugin@1.20.0": version "1.20.0" @@ -2600,9 +2600,9 @@ integrity sha512-xQT2XxtDGP1WFfTB/Lti629HpguNrfZ3dg84bWXASd6JUay6WgR73Wb6DG3kmr2/iGAWZ7NNLceGVWYWfgPX0g== "@types/estree@*", "@types/estree@^1.0.0": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.3.tgz#2be19e759a3dd18c79f9f436bd7363556c1a73dd" - integrity sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ== + version "1.0.4" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.4.tgz#d9748f5742171b26218516cf1828b8eafaf8a9fa" + integrity sha512-2JwWnHK9H+wUZNorf2Zr6ves96WHoWDJIftkcxPKsS7Djta6Zu519LarhRNljPXkpsZR2ZMwNCPeW7omW07BJw== "@types/estree@0.0.39": version "0.0.39" @@ -2702,11 +2702,11 @@ integrity sha512-AuHIyzR5Hea7ij0P9q7vx7xu4z0C28ucwjAZC0ja7JhINyCnOw8/DnvAPQQ9TfOlCtZAmCERKQX9+o1mgQhuOQ== "@types/node@*", "@types/node@^20.5.2": - version "20.8.8" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.8.tgz#adee050b422061ad5255fc38ff71b2bb96ea2a0e" - integrity sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ== + version "20.8.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.9.tgz#646390b4fab269abce59c308fc286dcd818a2b08" + integrity sha512-UzykFsT3FhHb1h7yD4CA4YhBHq545JC0YnEz41xkipN88eKQtL6rSgocL5tbAP6Ola9Izm/Aw4Ora8He4x0BHg== dependencies: - undici-types "~5.25.1" + undici-types "~5.26.4" "@types/node@18.0.6": version "18.0.6" @@ -2979,9 +2979,9 @@ acorn@^7.4.0: integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== acorn@^8.8.2, acorn@^8.9.0: - version "8.10.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" - integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + version "8.11.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" + integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== agent-base@6: version "6.0.2" @@ -3234,9 +3234,9 @@ axe-core@^4.6.2: integrity sha512-/dlp0fxyM3R8YW7MFzaHWXrf4zzbr0vaYb23VBFCl83R7nWNPg/yaQw2Dc8jzCMmDVLhSdzH8MjrsuIUuvX+6g== axios@^1.1.3, axios@^1.3.4: - version "1.5.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.5.1.tgz#11fbaa11fc35f431193a9564109c88c1f27b585f" - integrity sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A== + version "1.6.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.0.tgz#f1e5292f26b2fd5c2e66876adc5b06cdbd7d2102" + integrity sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg== dependencies: follow-redirects "^1.15.0" form-data "^4.0.0" @@ -3428,9 +3428,9 @@ camelcase-css@^2.0.1: integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001541: - version "1.0.30001553" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001553.tgz#e64e7dc8fd4885cd246bb476471420beb5e474b5" - integrity sha512-N0ttd6TrFfuqKNi+pMgWJTb9qrdJu4JSpgPFLe/lrD19ugC6fZgF0pUewRowDwzdDnb9V41mFcdlYgl/PyKf4A== + version "1.0.30001558" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001558.tgz#d2c6e21fdbfe83817f70feab902421a19b7983ee" + integrity sha512-/Et7DwLqpjS47JPEcz6VnxU9PwcIdVi0ciLXRWBQdj1XFye68pSQYpV0QtPTfUKWuOaEig+/Vez2l74eDc1tPQ== capital-case@^1.0.4: version "1.0.4" @@ -3663,9 +3663,9 @@ cookie@^0.5.0: integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== core-js-compat@^3.31.0, core-js-compat@^3.33.1: - version "3.33.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.33.1.tgz#debe80464107d75419e00c2ee29f35982118ff84" - integrity sha512-6pYKNOgD/j/bkC5xS5IIg6bncid3rfrI42oBH1SQJbsmYPKF7rhzcFzYCcxYMmNQQ0rCEB8WqpW7QHndOggaeQ== + version "3.33.2" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.33.2.tgz#3ea4563bfd015ad4e4b52442865b02c62aba5085" + integrity sha512-axfo+wxFVxnqf8RvxTzoAlzW4gRoacrHeoFlc9n0x50+7BEyZL/Rt3hicaED1/CEd7I6tPCPVUYcJwCMO5XUYw== dependencies: browserslist "^4.22.1" @@ -4027,9 +4027,9 @@ ejs@^3.1.6: jake "^10.8.5" electron-to-chromium@^1.4.535: - version "1.4.566" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.566.tgz#5c5ba1d2dc895f4887043f0cc7e61798c7e5919a" - integrity sha512-mv+fAy27uOmTVlUULy15U3DVJ+jg+8iyKH1bpwboCRhtDC69GKf1PPTZvEIhCyDr81RFqfxZJYrbgp933a1vtg== + version "1.4.571" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.571.tgz#8aa71539eb82db98740c3ec861256cc34e0356fd" + integrity sha512-Sc+VtKwKCDj3f/kLBjdyjMpNzoZsU6WuL/wFb6EH8USmHEcebxRXcRrVpOpayxd52tuey4RUDpUsw5OS5LhJqg== emoji-regex@^8.0.0: version "8.0.0" @@ -5764,9 +5764,9 @@ jest-worker@^27.4.5: supports-color "^8.0.0" jiti@^1.19.1: - version "1.20.0" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.20.0.tgz#2d823b5852ee8963585c8dd8b7992ffc1ae83b42" - integrity sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA== + version "1.21.0" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" + integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== joycon@^3.0.1: version "3.1.1" @@ -7221,9 +7221,9 @@ prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transfor prosemirror-model "^1.0.0" prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.27.0, prosemirror-view@^1.28.2, prosemirror-view@^1.31.0: - version "1.32.1" - resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.32.1.tgz#bcd0877f1673ffe5f94c1e966b6fbdadcd2d5bbf" - integrity sha512-9SnB4HBgRczzTyIMZLPE1iszegL04hNfUyS8uPtP1RPxNM2NTCiIs8KwNsJU4nbZO9rxJTwVTv7Jm3zU4CR78A== + version "1.32.2" + resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.32.2.tgz#e55fcf5b55847e3b9d9b7982c9a129ef237e60e1" + integrity sha512-l2RQUGaiDI8SG8ZjWIkjT8yjGmNwdzMFMzQmxv/Kh8Vx+ICnz5R+K0mrOS16rhfjX7n2t4emU0goh7TerQC3mw== dependencies: prosemirror-model "^1.16.0" prosemirror-state "^1.0.0" @@ -7243,9 +7243,9 @@ pump@^3.0.0: once "^1.3.1" punycode@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== queue-microtask@^1.2.2: version "1.2.3" @@ -8006,9 +8006,9 @@ stacktrace-parser@^0.1.10: type-fest "^0.7.1" streamx@^2.15.0: - version "2.15.1" - resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.15.1.tgz#396ad286d8bc3eeef8f5cea3f029e81237c024c6" - integrity sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA== + version "2.15.2" + resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.15.2.tgz#680eacebdc9c43ede7362c2e6695b34dd413c741" + integrity sha512-b62pAV/aeMjUoRN2C/9F0n+G8AfcJjNC0zw/ZmOHeFsIe4m4GzjVW9m6VHXVjk536NbdU9JRwKMJRfkc+zUFTg== dependencies: fast-fifo "^1.1.0" queue-tick "^1.0.1" @@ -8198,9 +8198,9 @@ tailwindcss-animate@^1.0.6: integrity sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA== tailwindcss@^3.2.7, tailwindcss@^3.3.3: - version "3.3.4" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.4.tgz#f08c493ff3ddf03081c40e780e98f129e1c8214d" - integrity sha512-JXZNOkggUAc9T5E7nCrimoXHcSf9h3NWFe5sh36CGD/3M5TRLuQeFnQoDsit2uVTqgoOZHLx5rTykLUu16vsMQ== + version "3.3.5" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.5.tgz#22a59e2fbe0ecb6660809d9cc5f3976b077be3b8" + integrity sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA== dependencies: "@alloc/quick-lru" "^5.2.0" arg "^5.0.2" @@ -8296,9 +8296,9 @@ terser-webpack-plugin@^5.3.3: terser "^5.16.8" terser@^5.0.0, terser@^5.16.8: - version "5.22.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.22.0.tgz#4f18103f84c5c9437aafb7a14918273310a8a49d" - integrity sha512-hHZVLgRA2z4NWcN6aS5rQDc+7Dcy58HOf2zbYwmFcQ+ua3h6eEFf5lIDKTzbWwlazPyOZsFQO8V80/IjVNExEw== + version "5.23.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.23.0.tgz#a9c02bc3087d0f5b1cc63bbfb4fe0f7e5dbbde82" + integrity sha512-Iyy83LN0uX9ZZLCX4Qbu5JiHiWjOCTwrmM9InWOzVeM++KNWEsqV4YgN9U9E8AlohQ6Gs42ztczlWOG/lwDAMA== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -8357,9 +8357,9 @@ tiptap-markdown@^0.8.2: prosemirror-markdown "^1.11.1" tlds@^1.238.0: - version "1.243.0" - resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.243.0.tgz#0dd9e6373633c495fa0c20fdafe9aaea6ff34550" - integrity sha512-jA0EMB5YFZFX2VqmK/Ra4O1UqDuWnpiw/9miYFdG1lVIDg6w9IsjlXK0TGlqFnzsnuBIpP5rCDFT2iPZNOfvqA== + version "1.244.0" + resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.244.0.tgz#b25468d18c53633706081434a337786f695d7662" + integrity sha512-nkacnxHmN5USM/cpmPx29sc2/AnmvUA9han0tNtAJ9yOhh4bPmZm4dGhyg/isWBIES4a70mjd0Q8FSaof6Vf0g== to-fast-properties@^2.0.0: version "2.0.0" @@ -8622,10 +8622,10 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" -undici-types@~5.25.1: - version "5.25.3" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.25.3.tgz#e044115914c85f0bcbb229f346ab739f064998c3" - integrity sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0"