diff --git a/.eslintrc.json b/apps/app/.eslintrc.json similarity index 100% rename from .eslintrc.json rename to apps/app/.eslintrc.json diff --git a/apps/app/components/command-palette/shortcuts.tsx b/apps/app/components/command-palette/shortcuts.tsx index dcba28c82..e4521b25a 100644 --- a/apps/app/components/command-palette/shortcuts.tsx +++ b/apps/app/components/command-palette/shortcuts.tsx @@ -1,15 +1,53 @@ -import React from "react"; +import React, { useState } from "react"; // headless ui import { Dialog, Transition } from "@headlessui/react"; // icons import { XMarkIcon } from "@heroicons/react/20/solid"; +// ui +import { Input } from "ui"; type Props = { isOpen: boolean; setIsOpen: React.Dispatch>; }; +const shortcuts = [ + { + title: "Navigation", + shortcuts: [ + { keys: "ctrl,/", description: "To open navigator" }, + { keys: "↑", description: "Move up" }, + { keys: "↓", description: "Move down" }, + { keys: "←", description: "Move left" }, + { keys: "→", description: "Move right" }, + { keys: "Enter", description: "Select" }, + { keys: "Esc", description: "Close" }, + ], + }, + { + title: "Common", + shortcuts: [ + { keys: "ctrl,p", description: "To create project" }, + { keys: "ctrl,i", description: "To create issue" }, + { keys: "ctrl,q", description: "To create cycle" }, + { keys: "ctrl,h", description: "To open shortcuts guide" }, + { + keys: "ctrl,alt,c", + description: "To copy issue url when on issue detail page.", + }, + ], + }, +]; + const ShortcutsModal: React.FC = ({ isOpen, setIsOpen }) => { + const [query, setQuery] = useState(""); + + const filteredShortcuts = shortcuts.filter((shortcut) => + shortcut.shortcuts.some((item) => item.description.includes(query.trim())) || query === "" + ? true + : false + ); + return ( @@ -39,7 +77,7 @@ const ShortcutsModal: React.FC = ({ isOpen, setIsOpen }) => {
-
+
= ({ isOpen, setIsOpen }) => { -
- {[ - { - title: "Navigation", - shortcuts: [ - { keys: "ctrl,/", description: "To open navigator" }, - { keys: "↑", description: "Move up" }, - { keys: "↓", description: "Move down" }, - { keys: "←", description: "Move left" }, - { keys: "→", description: "Move right" }, - { keys: "Enter", description: "Select" }, - { keys: "Esc", description: "Close" }, - ], - }, - { - title: "Common", - shortcuts: [ - { keys: "ctrl,p", description: "To create project" }, - { keys: "ctrl,i", description: "To create issue" }, - { keys: "ctrl,q", description: "To create cycle" }, - { keys: "ctrl,m", description: "To create module" }, - { keys: "ctrl,h", description: "To open shortcuts guide" }, - { - keys: "ctrl,alt,c", - description: "To copy issue url when on issue detail page.", - }, - ], - }, - ].map(({ title, shortcuts }) => ( -
-

{title}

-
- {shortcuts.map(({ keys, description }, index) => ( -
-

{description}

-
- {keys.split(",").map((key, index) => ( - - - {key} - - {/* {index !== keys.split(",").length - 1 ? ( - + - ) : null} */} - - ))} +
+ setQuery(e.target.value)} + /> +
+
+ {filteredShortcuts.length > 0 ? ( + filteredShortcuts.map(({ title, shortcuts }) => ( +
+

{title}

+
+ {shortcuts.map(({ keys, description }, index) => ( +
+

{description}

+
+ {keys.split(",").map((key, index) => ( + + + {key} + + + ))} +
-
- ))} + ))} +
+ )) + ) : ( +
+

+ No shortcuts found for{" "} + + {`"`} + {query} + {`"`} + +

- ))} + )}
diff --git a/apps/app/components/project/issues/BoardView/state/ConfirmStateDeletion.tsx b/apps/app/components/project/issues/BoardView/state/confirm-state-delete.tsx similarity index 82% rename from apps/app/components/project/issues/BoardView/state/ConfirmStateDeletion.tsx rename to apps/app/components/project/issues/BoardView/state/confirm-state-delete.tsx index 448c50acc..8b884788c 100644 --- a/apps/app/components/project/issues/BoardView/state/ConfirmStateDeletion.tsx +++ b/apps/app/components/project/issues/BoardView/state/confirm-state-delete.tsx @@ -9,6 +9,8 @@ import stateServices from "lib/services/state.service"; import { STATE_LIST } from "constants/fetch-keys"; // hooks import useUser from "lib/hooks/useUser"; +// common +import { groupBy } from "constants/common"; // icons import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // ui @@ -18,25 +20,27 @@ import { Button } from "ui"; import type { IState } from "types"; type Props = { isOpen: boolean; - setIsOpen: React.Dispatch>; - data?: IState; + onClose: () => void; + data: IState | null; }; -const ConfirmStateDeletion: React.FC = ({ isOpen, setIsOpen, data }) => { +const ConfirmStateDeletion: React.FC = ({ isOpen, onClose, data }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const { activeWorkspace } = useUser(); + const [issuesWithThisStateExist, setIssuesWithThisStateExist] = useState(true); + + const { activeWorkspace, issues } = useUser(); const cancelButtonRef = useRef(null); const handleClose = () => { - setIsOpen(false); + onClose(); setIsDeleteLoading(false); }; const handleDeletion = async () => { setIsDeleteLoading(true); - if (!data || !activeWorkspace) return; + if (!data || !activeWorkspace || issuesWithThisStateExist) return; await stateServices .deleteState(activeWorkspace.slug, data.project, data.id) .then(() => { @@ -53,9 +57,11 @@ const ConfirmStateDeletion: React.FC = ({ isOpen, setIsOpen, data }) => { }); }; + const groupedIssues = groupBy(issues?.results ?? [], "state"); + useEffect(() => { - data && setIsOpen(true); - }, [data, setIsOpen]); + if (data) setIssuesWithThisStateExist(!!groupedIssues[data.id]); + }, [groupedIssues, data]); return ( @@ -109,6 +115,14 @@ const ConfirmStateDeletion: React.FC = ({ isOpen, setIsOpen, data }) => { This action cannot be undone.

+
+ {issuesWithThisStateExist && ( +

+ There are issues with this state. Please move them to another state + before deleting this state. +

+ )} +
@@ -117,7 +131,7 @@ const ConfirmStateDeletion: React.FC = ({ isOpen, setIsOpen, data }) => { type="button" onClick={handleDeletion} theme="danger" - disabled={isDeleteLoading} + disabled={isDeleteLoading || issuesWithThisStateExist} className="inline-flex sm:ml-3" > {isDeleteLoading ? "Deleting..." : "Delete"} diff --git a/apps/app/components/project/issues/BoardView/state/create-update-state-inline.tsx b/apps/app/components/project/issues/BoardView/state/create-update-state-inline.tsx new file mode 100644 index 000000000..10f186bd2 --- /dev/null +++ b/apps/app/components/project/issues/BoardView/state/create-update-state-inline.tsx @@ -0,0 +1,209 @@ +import React, { useEffect } from "react"; +// swr +import { mutate } from "swr"; +// react hook form +import { useForm, Controller } from "react-hook-form"; +// react color +import { TwitterPicker } from "react-color"; +// headless +import { Popover, Transition } from "@headlessui/react"; +// constants +import { GROUP_CHOICES } from "constants/"; +import { STATE_LIST } from "constants/fetch-keys"; +// services +import stateService from "lib/services/state.service"; +// ui +import { Button, Input, Select, Spinner } from "ui"; +// types +import type { IState } from "types"; + +type Props = { + workspaceSlug?: string; + projectId?: string; + data: IState | null; + onClose: () => void; + selectedGroup: StateGroup | null; +}; + +export type StateGroup = "backlog" | "unstarted" | "started" | "completed" | "cancelled" | null; + +const defaultValues: Partial = { + name: "", + color: "#000000", + group: "backlog", +}; + +export const CreateUpdateStateInline: React.FC = ({ + workspaceSlug, + projectId, + data, + onClose, + selectedGroup, +}) => { + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + setError, + watch, + reset, + control, + } = useForm({ + defaultValues, + }); + + const handleClose = () => { + onClose(); + reset({ name: "", color: "#000000", group: "backlog" }); + }; + + const onSubmit = async (formData: IState) => { + if (!workspaceSlug || !projectId || isSubmitting) return; + const payload: IState = { + ...formData, + }; + if (!data) { + await stateService + .createState(workspaceSlug, projectId, { ...payload }) + .then((res) => { + mutate(STATE_LIST(projectId), (prevData) => [...(prevData ?? []), res], false); + handleClose(); + }) + .catch((err) => { + Object.keys(err).map((key) => { + setError(key as keyof IState, { + message: err[key].join(", "), + }); + }); + }); + } else { + await stateService + .updateState(workspaceSlug, projectId, data.id, { + ...payload, + }) + .then((res) => { + mutate( + STATE_LIST(projectId), + (prevData) => { + const newData = prevData?.map((item) => { + if (item.id === res.id) { + return res; + } + return item; + }); + return newData; + }, + false + ); + handleClose(); + }) + .catch((err) => { + Object.keys(err).map((key) => { + setError(key as keyof IState, { + message: err[key].join(", "), + }); + }); + }); + } + }; + + useEffect(() => { + if (data === null) return; + reset(data); + }, [data, reset]); + + useEffect(() => { + if (!data) + reset({ + ...defaultValues, + group: selectedGroup ?? "backlog", + }); + }, [selectedGroup, data, reset]); + + return ( +
+
+ + {({ open }) => ( + <> + + {watch("color") && watch("color") !== "" && ( + + )} + + + + + ( + onChange(value.hex)} /> + )} + /> + + + + )} + +
+ + {data && ( + + + +
+ ); +}; diff --git a/apps/app/components/project/issues/BoardView/state/CreateUpdateStateModal.tsx b/apps/app/components/project/issues/BoardView/state/create-update-state-modal.tsx similarity index 91% rename from apps/app/components/project/issues/BoardView/state/CreateUpdateStateModal.tsx rename to apps/app/components/project/issues/BoardView/state/create-update-state-modal.tsx index 3ba149820..4cd5a1367 100644 --- a/apps/app/components/project/issues/BoardView/state/CreateUpdateStateModal.tsx +++ b/apps/app/components/project/issues/BoardView/state/create-update-state-modal.tsx @@ -11,10 +11,12 @@ import { Dialog, Popover, Transition } from "@headlessui/react"; import stateService from "lib/services/state.service"; // fetch keys import { STATE_LIST } from "constants/fetch-keys"; +// constants +import { GROUP_CHOICES } from "constants/"; // hooks import useUser from "lib/hooks/useUser"; // ui -import { Button, Input, TextArea } from "ui"; +import { Button, Input, Select, TextArea } from "ui"; // icons import { ChevronDownIcon } from "@heroicons/react/24/outline"; @@ -31,6 +33,7 @@ const defaultValues: Partial = { name: "", description: "", color: "#000000", + group: "backlog", }; const CreateUpdateStateModal: React.FC = ({ isOpen, data, projectId, handleClose }) => { @@ -161,6 +164,22 @@ const CreateUpdateStateModal: React.FC = ({ isOpen, data, projectId, hand }} />
+
+