diff --git a/apps/app/components/command-palette/index.tsx b/apps/app/components/command-palette/command-pallette.tsx similarity index 98% rename from apps/app/components/command-palette/index.tsx rename to apps/app/components/command-palette/command-pallette.tsx index e6138da948f..678e17d46b5 100644 --- a/apps/app/components/command-palette/index.tsx +++ b/apps/app/components/command-palette/command-pallette.tsx @@ -1,4 +1,3 @@ -// TODO: Refactor this component: into a different file, use this file to export the components import React, { useState, useCallback, useEffect } from "react"; import { useRouter } from "next/router"; @@ -14,7 +13,7 @@ import useTheme from "hooks/use-theme"; import useToast from "hooks/use-toast"; import useUser from "hooks/use-user"; // components -import ShortcutsModal from "components/command-palette/shortcuts"; +import { ShortcutsModal } from "components/command-palette"; import { BulkDeleteIssuesModal } from "components/core"; import { CreateProjectModal } from "components/project"; import { CreateUpdateIssueModal } from "components/issues"; @@ -36,7 +35,7 @@ import { IIssue } from "types"; // fetch-keys import { USER_ISSUE } from "constants/fetch-keys"; -const CommandPalette: React.FC = () => { +export const CommandPalette: React.FC = () => { const [query, setQuery] = useState(""); const [isPaletteOpen, setIsPaletteOpen] = useState(false); @@ -369,5 +368,3 @@ const CommandPalette: React.FC = () => { ); }; - -export default CommandPalette; diff --git a/apps/app/components/command-palette/index.ts b/apps/app/components/command-palette/index.ts new file mode 100644 index 00000000000..542d6921475 --- /dev/null +++ b/apps/app/components/command-palette/index.ts @@ -0,0 +1,2 @@ +export * from "./command-pallette"; +export * from "./shortcuts-modal"; diff --git a/apps/app/components/command-palette/shortcuts.tsx b/apps/app/components/command-palette/shortcuts-modal.tsx similarity index 98% rename from apps/app/components/command-palette/shortcuts.tsx rename to apps/app/components/command-palette/shortcuts-modal.tsx index f5435055c90..c1800ab17aa 100644 --- a/apps/app/components/command-palette/shortcuts.tsx +++ b/apps/app/components/command-palette/shortcuts-modal.tsx @@ -41,7 +41,7 @@ const shortcuts = [ }, ]; -const ShortcutsModal: React.FC = ({ isOpen, setIsOpen }) => { +export const ShortcutsModal: React.FC = ({ isOpen, setIsOpen }) => { const [query, setQuery] = useState(""); const filteredShortcuts = shortcuts.filter((shortcut) => @@ -150,5 +150,3 @@ const ShortcutsModal: React.FC = ({ isOpen, setIsOpen }) => { ); }; - -export default ShortcutsModal; diff --git a/apps/app/components/core/index.ts b/apps/app/components/core/index.ts index 0865ea44136..482258b4a75 100644 --- a/apps/app/components/core/index.ts +++ b/apps/app/components/core/index.ts @@ -1,5 +1,6 @@ export * from "./board-view"; export * from "./list-view"; +export * from "./sidebar"; export * from "./bulk-delete-issues-modal"; export * from "./existing-issues-list-modal"; export * from "./image-upload-modal"; diff --git a/apps/app/components/core/sidebar/index.ts b/apps/app/components/core/sidebar/index.ts new file mode 100644 index 00000000000..20d186d1eed --- /dev/null +++ b/apps/app/components/core/sidebar/index.ts @@ -0,0 +1,2 @@ +export * from "./sidebar-progress-stats"; +export * from "./single-progress-stats"; diff --git a/apps/app/components/core/sidebar/sidebar-progress-stats.tsx b/apps/app/components/core/sidebar/sidebar-progress-stats.tsx index 2b150a890ef..9a3e5372307 100644 --- a/apps/app/components/core/sidebar/sidebar-progress-stats.tsx +++ b/apps/app/components/core/sidebar/sidebar-progress-stats.tsx @@ -11,7 +11,7 @@ import { Tab } from "@headlessui/react"; import issuesServices from "services/issues.service"; import projectService from "services/project.service"; // components -import SingleProgressStats from "components/core/sidebar/single-progress-stats"; +import { SingleProgressStats } from "components/core"; // ui import { Avatar } from "components/ui"; // icons @@ -36,7 +36,7 @@ const stateGroupColours: { completed: "#096e8d", }; -const SidebarProgressStats: React.FC = ({ groupedIssues, issues }) => { +export const SidebarProgressStats: React.FC = ({ groupedIssues, issues }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; const { data: issueLabels } = useSWR( @@ -180,5 +180,3 @@ const SidebarProgressStats: React.FC = ({ groupedIssues, issues }) => { ); }; - -export default SidebarProgressStats; diff --git a/apps/app/components/core/sidebar/single-progress-stats.tsx b/apps/app/components/core/sidebar/single-progress-stats.tsx index 885aed23ed2..4b3de9c9fab 100644 --- a/apps/app/components/core/sidebar/single-progress-stats.tsx +++ b/apps/app/components/core/sidebar/single-progress-stats.tsx @@ -8,22 +8,22 @@ type TSingleProgressStatsProps = { total: number; }; -const SingleProgressStats: React.FC = ({ title, completed, total }) => ( - <> -
-
{title}
-
-
- - - - {Math.floor((completed / total) * 100)}% -
- of - {total} +export const SingleProgressStats: React.FC = ({ + title, + completed, + total, +}) => ( +
+
{title}
+
+
+ + + + {Math.floor((completed / total) * 100)}%
+ of + {total}
- +
); - -export default SingleProgressStats; diff --git a/apps/app/components/cycles/modal.tsx b/apps/app/components/cycles/modal.tsx index 76e1c5ad1a5..878a629ca39 100644 --- a/apps/app/components/cycles/modal.tsx +++ b/apps/app/components/cycles/modal.tsx @@ -1,12 +1,15 @@ import { Fragment } from "react"; + import { mutate } from "swr"; + +// headless ui import { Dialog, Transition } from "@headlessui/react"; // services import cycleService from "services/cycles.service"; +// hooks +import useToast from "hooks/use-toast"; // components import { CycleForm } from "components/cycles"; -// helpers -import { renderDateFormat } from "helpers/date-time.helper"; // types import type { ICycle } from "types"; // fetch keys @@ -20,8 +23,14 @@ export interface CycleModalProps { initialData?: ICycle; } -export const CycleModal: React.FC = (props) => { - const { isOpen, handleClose, initialData, projectId, workspaceSlug } = props; +export const CycleModal: React.FC = ({ + isOpen, + handleClose, + initialData, + projectId, + workspaceSlug, +}) => { + const { setToastAlert } = useToast(); const createCycle = (payload: Partial) => { cycleService @@ -31,12 +40,11 @@ export const CycleModal: React.FC = (props) => { handleClose(); }) .catch((err) => { - // TODO: Handle this ERROR. - // Object.keys(err).map((key) => { - // setError(key as keyof typeof defaultValues, { - // message: err[key].join(", "), - // }); - // }); + setToastAlert({ + type: "error", + title: "Error", + message: "Error in creating cycle. Please try again!", + }); }); }; @@ -48,12 +56,11 @@ export const CycleModal: React.FC = (props) => { handleClose(); }) .catch((err) => { - // TODO: Handle this ERROR. - // Object.keys(err).map((key) => { - // setError(key as keyof typeof defaultValues, { - // message: err[key].join(", "), - // }); - // }); + setToastAlert({ + type: "error", + title: "Error", + message: "Error in updating cycle. Please try again!", + }); }); }; diff --git a/apps/app/components/issues/select/label.tsx b/apps/app/components/issues/select/label.tsx index b1b1c43382d..2d4e5a179c3 100644 --- a/apps/app/components/issues/select/label.tsx +++ b/apps/app/components/issues/select/label.tsx @@ -9,7 +9,7 @@ import { useForm } from "react-hook-form"; // headless ui import { Combobox, Transition } from "@headlessui/react"; // icons -import { TagIcon } from "@heroicons/react/24/outline"; +import { RectangleGroupIcon, TagIcon } from "@heroicons/react/24/outline"; // services import issuesServices from "services/issues.service"; // types @@ -58,8 +58,6 @@ export const IssueLabelSelect: React.FC = ({ value, onChange, projectId } }; const { - register, - handleSubmit, formState: { isSubmitting }, setFocus, reset, @@ -69,16 +67,10 @@ export const IssueLabelSelect: React.FC = ({ value, onChange, projectId } isOpen && setFocus("name"); }, [isOpen, setFocus]); - const options = issueLabels?.map((label) => ({ - value: label.id, - display: label.name, - color: label.color, - })); - const filteredOptions = query === "" - ? options - : options?.filter((option) => option.display.toLowerCase().includes(query.toLowerCase())); + ? issueLabels + : issueLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase())); return ( <> @@ -98,10 +90,9 @@ export const IssueLabelSelect: React.FC = ({ value, onChange, projectId } {Array.isArray(value) - ? value - .map((v) => options?.find((option) => option.value === v)?.display) - .join(", ") || "Labels" - : options?.find((option) => option.value === value)?.display || "Labels"} + ? value.map((v) => issueLabels?.find((l) => l.id === v)?.name).join(", ") || + "Labels" + : issueLabels?.find((l) => l.id === value)?.name || "Labels"} @@ -122,31 +113,62 @@ export const IssueLabelSelect: React.FC = ({ value, onChange, projectId } displayValue={(assigned: any) => assigned?.name} />
- {filteredOptions ? ( + {issueLabels && filteredOptions ? ( filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `${active ? "bg-indigo-50" : ""} ${ - selected ? "bg-indigo-50 font-medium" : "" - } flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900` - } - value={option.value} - > - {issueLabels && ( - <> - - {option.display} - - )} - - )) + filteredOptions.map((label) => { + const children = issueLabels?.filter((l) => l.parent === label.id); + + if (children.length === 0) { + if (!label.parent) + return ( + + `${active ? "bg-indigo-50" : ""} ${ + selected ? "bg-indigo-50 font-medium" : "" + } flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900` + } + value={label.id} + > + + {label.name} + + ); + } else + return ( +
+
+ {label.name} +
+
+ {children.map((child) => ( + + `${active ? "bg-indigo-50" : ""} ${ + selected ? "bg-indigo-50 font-medium" : "" + } flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900` + } + value={child.id} + > + + {child.name} + + ))} +
+
+ ); + }) ) : (

No labels found

) diff --git a/apps/app/components/issues/sidebar.tsx b/apps/app/components/issues/sidebar.tsx index 62a99eac142..c321f0a31c4 100644 --- a/apps/app/components/issues/sidebar.tsx +++ b/apps/app/components/issues/sidebar.tsx @@ -38,6 +38,7 @@ import { TrashIcon, PlusIcon, XMarkIcon, + RectangleGroupIcon, } from "@heroicons/react/24/outline"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; @@ -298,30 +299,31 @@ export const IssueDetailsSidebar: React.FC = ({
- {watchIssue("labels_list")?.map((label) => { - const singleLabel = issueLabels?.find((l) => l.id === label); + {watchIssue("labels_list")?.map((labelId) => { + const label = issueLabels?.find((l) => l.id === labelId); - if (!singleLabel) return null; - - return ( - { - const updatedLabels = watchIssue("labels_list")?.filter((l) => l !== label); - submitChanges({ - labels_list: updatedLabels, - }); - }} - > + if (label) + return ( - {singleLabel.name} - - - ); + key={label.id} + className="group flex cursor-pointer items-center gap-1 rounded-2xl border px-1 py-0.5 text-xs hover:border-red-500 hover:bg-red-50" + onClick={() => { + const updatedLabels = watchIssue("labels_list")?.filter( + (l) => l !== labelId + ); + submitChanges({ + labels_list: updatedLabels, + }); + }} + > + + {label.name} + + + ); })} = ({ disabled={isNotAllowed} > {({ open }) => ( - <> - Label -
- - Select Label - +
+ + Select Label + + + + +
+ {issueLabels ? ( + issueLabels.length > 0 ? ( + issueLabels.map((label: IIssueLabels) => { + const children = issueLabels?.filter( + (l) => l.parent === label.id + ); - - -
- {issueLabels ? ( - issueLabels.length > 0 ? ( - issueLabels.map((label: IIssueLabels) => ( - - `${ - active || selected ? "bg-indigo-50" : "" - } relative flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900` - } - value={label.id} - > - - {label.name} - - )) - ) : ( -
No labels found
- ) + if (children.length === 0) { + if (!label.parent) + return ( + + `${active || selected ? "bg-indigo-50" : ""} ${ + selected ? "font-medium" : "" + } flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900` + } + value={label.id} + > + + {label.name} + + ); + } else + return ( +
+
+ {" "} + {label.name} +
+
+ {children.map((child) => ( + + `${active || selected ? "bg-indigo-50" : ""} ${ + selected ? "font-medium" : "" + } flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900` + } + value={child.id} + > + + {child.name} + + ))} +
+
+ ); + }) ) : ( - - )} -
-
-
-
- +
No labels found
+ ) + ) : ( + + )} +
+ + +
)} )} diff --git a/apps/app/components/issues/view-select/priority.tsx b/apps/app/components/issues/view-select/priority.tsx index 5517494b2cc..9f55937a2bd 100644 --- a/apps/app/components/issues/view-select/priority.tsx +++ b/apps/app/components/issues/view-select/priority.tsx @@ -69,10 +69,10 @@ export const ViewPrioritySelect: React.FC = ({ {PRIORITIES?.map((priority) => ( - `flex cursor-pointer select-none items-center gap-x-2 px-3 py-2 capitalize ${ - active ? "bg-indigo-50" : "bg-white" - }` + className={({ active, selected }) => + `${active || selected ? "bg-indigo-50" : ""} ${ + selected ? "font-medium" : "" + } flex cursor-pointer select-none items-center gap-x-2 px-3 py-2 capitalize` } value={priority} > diff --git a/apps/app/components/labels/create-update-label-inline.tsx b/apps/app/components/labels/create-update-label-inline.tsx new file mode 100644 index 00000000000..3b7fe614c2c --- /dev/null +++ b/apps/app/components/labels/create-update-label-inline.tsx @@ -0,0 +1,189 @@ +import React, { useEffect } from "react"; + +import { useRouter } from "next/router"; + +import { mutate } from "swr"; + +// react-hook-form +import { Controller, SubmitHandler, useForm } from "react-hook-form"; +// react-color +import { TwitterPicker } from "react-color"; +// headless ui +import { Popover, Transition } from "@headlessui/react"; +// services +import issuesService from "services/issues.service"; +// ui +import { Button, Input } from "components/ui"; +// types +import { IIssueLabels } from "types"; +// fetch-keys +import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; + +type Props = { + labelForm: boolean; + setLabelForm: React.Dispatch>; + isUpdating: boolean; + labelToUpdate: IIssueLabels | null; +}; + +const defaultValues: Partial = { + name: "", + color: "#ff0000", +}; + +export const CreateUpdateLabelInline: React.FC = ({ + labelForm, + setLabelForm, + isUpdating, + labelToUpdate, +}) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { + handleSubmit, + control, + register, + reset, + formState: { errors, isSubmitting }, + watch, + setValue, + } = useForm({ + defaultValues, + }); + + const handleLabelCreate: SubmitHandler = async (formData) => { + if (!workspaceSlug || !projectId || isSubmitting) return; + + await issuesService + .createIssueLabel(workspaceSlug as string, projectId as string, formData) + .then((res) => { + mutate( + PROJECT_ISSUE_LABELS(projectId as string), + (prevData) => [res, ...(prevData ?? [])], + false + ); + reset(defaultValues); + setLabelForm(false); + }); + }; + + const handleLabelUpdate: SubmitHandler = async (formData) => { + if (!workspaceSlug || !projectId || isSubmitting) return; + + await issuesService + .patchIssueLabel( + workspaceSlug as string, + projectId as string, + labelToUpdate?.id ?? "", + formData + ) + .then((res) => { + console.log(res); + reset(defaultValues); + mutate( + PROJECT_ISSUE_LABELS(projectId as string), + (prevData) => + prevData?.map((p) => (p.id === labelToUpdate?.id ? { ...p, ...formData } : p)), + false + ); + setLabelForm(false); + }); + }; + + useEffect(() => { + if (!labelForm && isUpdating) return; + + reset(); + }, [labelForm, isUpdating, reset]); + + useEffect(() => { + if (!labelToUpdate) return; + + setValue("color", labelToUpdate.color); + setValue("name", labelToUpdate.name); + }, [labelToUpdate, setValue]); + + return ( +
+
+ + {({ open }) => ( + <> + + {watch("color") && watch("color") !== "" && ( + + )} + + + + + ( + onChange(value.hex)} /> + )} + /> + + + + )} + +
+
+ +
+ + {isUpdating ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/apps/app/components/labels/index.ts b/apps/app/components/labels/index.ts index 2a155c8dcfb..d407cd0741e 100644 --- a/apps/app/components/labels/index.ts +++ b/apps/app/components/labels/index.ts @@ -1,2 +1,4 @@ +export * from "./create-update-label-inline"; export * from "./labels-list-modal"; +export * from "./single-label-group"; export * from "./single-label"; diff --git a/apps/app/components/labels/single-label-group.tsx b/apps/app/components/labels/single-label-group.tsx new file mode 100644 index 00000000000..efdb26f3878 --- /dev/null +++ b/apps/app/components/labels/single-label-group.tsx @@ -0,0 +1,136 @@ +import React from "react"; + +import { useRouter } from "next/router"; + +import { mutate } from "swr"; + +// headless ui +import { Disclosure, Transition } from "@headlessui/react"; +// services +import issuesService from "services/issues.service"; +// ui +import { CustomMenu } from "components/ui"; +// icons +import { ChevronDownIcon, RectangleGroupIcon } from "@heroicons/react/24/outline"; +// types +import { IIssueLabels } from "types"; +// fetch-keys +import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; + +type Props = { + label: IIssueLabels; + labelChildren: IIssueLabels[]; + addLabelToGroup: (parentLabel: IIssueLabels) => void; + editLabel: (label: IIssueLabels) => void; + handleLabelDelete: (labelId: string) => void; +}; + +export const SingleLabelGroup: React.FC = ({ + label, + labelChildren, + addLabelToGroup, + editLabel, + handleLabelDelete, +}) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + 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 + ); + + issuesService + .patchIssueLabel(workspaceSlug as string, projectId as string, label.id, { + parent: null, + }) + .then((res) => { + mutate(PROJECT_ISSUE_LABELS(projectId as string)); + }); + }; + + return ( + + {({ open }) => ( + <> +
+ +
+ + + + + + +
{label.name}
+
+
+ + addLabelToGroup(label)}> + Add more labels + + editLabel(label)}>Edit + handleLabelDelete(label.id)}> + Delete + + +
+ + +
+ {labelChildren.map((child) => ( +
+
+ + {child.name} +
+
+ + removeFromGroup(child)}> + Remove from group + + editLabel(child)}> + Edit + + handleLabelDelete(child.id)}> + Delete + + +
+
+ ))} +
+
+
+ + )} +
+ ); +}; diff --git a/apps/app/components/labels/single-label.tsx b/apps/app/components/labels/single-label.tsx index eb721a8ac54..927a30d5a1b 100644 --- a/apps/app/components/labels/single-label.tsx +++ b/apps/app/components/labels/single-label.tsx @@ -1,171 +1,43 @@ -import React, { useState } from "react"; +import React from "react"; -import { useRouter } from "next/router"; - -import { mutate } from "swr"; - -// headless ui -import { Disclosure, Transition } from "@headlessui/react"; -// services -import issuesService from "services/issues.service"; -// components -import { LabelsListModal } from "components/labels"; // ui import { CustomMenu } from "components/ui"; -// icons -import { ChevronDownIcon } from "@heroicons/react/24/outline"; // types import { IIssueLabels } from "types"; -// fetch-keys -import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; type Props = { label: IIssueLabels; - issueLabels: IIssueLabels[]; + addLabelToGroup: (parentLabel: IIssueLabels) => void; editLabel: (label: IIssueLabels) => void; handleLabelDelete: (labelId: string) => void; }; export const SingleLabel: React.FC = ({ label, - issueLabels, + addLabelToGroup, editLabel, handleLabelDelete, -}) => { - const [labelsListModal, setLabelsListModal] = useState(false); - - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const children = issueLabels?.filter((l) => l.parent === label.id); - - 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 - ); - - issuesService - .patchIssueLabel(workspaceSlug as string, projectId as string, label.id, { - parent: null, - }) - .then((res) => { - mutate(PROJECT_ISSUE_LABELS(projectId as string)); - }); - }; - - return ( - <> - setLabelsListModal(false)} - parent={label} - /> - {children && children.length === 0 ? ( - label.parent === "" || !label.parent ? ( -
-
-
- -
{label.name}
-
- - setLabelsListModal(true)}> - Convert to group - - editLabel(label)}>Edit - handleLabelDelete(label.id)}> - Delete - - -
-
- ) : null - ) : ( - - {({ open }) => ( - <> -
- -
- - - -
{label.name}
-
-
- - setLabelsListModal(true)}> - Add more labels - - editLabel(label)}>Edit - handleLabelDelete(label.id)}> - Delete - - -
- - -
- {children.map((child) => ( -
-
- - {child.name} -
-
- - removeFromGroup(child)}> - Remove from group - - editLabel(child)}> - Edit - - handleLabelDelete(child.id)}> - Delete - - -
-
- ))} -
-
-
- - )} -
- )} - - ); -}; +}) => ( +
+
+
+ +
{label.name}
+
+ + addLabelToGroup(label)}> + Convert to group + + editLabel(label)}>Edit + handleLabelDelete(label.id)}> + Delete + + +
+
+); diff --git a/apps/app/components/modules/sidebar.tsx b/apps/app/components/modules/sidebar.tsx index 5625933846f..3ea95cec300 100644 --- a/apps/app/components/modules/sidebar.tsx +++ b/apps/app/components/modules/sidebar.tsx @@ -30,6 +30,8 @@ import { } from "components/modules"; import "react-circular-progressbar/dist/styles.css"; +// components +import { SidebarProgressStats } from "components/core"; // ui import { CustomDatePicker, Loader } from "components/ui"; // helpers @@ -40,7 +42,6 @@ import { groupBy } from "helpers/array.helper"; import { IIssue, IModule, ModuleIssueResponse } from "types"; // fetch-keys import { MODULE_DETAILS } from "constants/fetch-keys"; -import SidebarProgressStats from "components/core/sidebar/sidebar-progress-stats"; const defaultValues: Partial = { lead: "", diff --git a/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx b/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx index 1650607f0c4..c55f30591a0 100644 --- a/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx +++ b/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx @@ -20,17 +20,16 @@ import useToast from "hooks/use-toast"; // services import cyclesService from "services/cycles.service"; // components -import SidebarProgressStats from "components/core/sidebar/sidebar-progress-stats"; +import { SidebarProgressStats } from "components/core"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; import { groupBy } from "helpers/array.helper"; +import { renderShortNumericDateFormat } from "helpers/date-time.helper"; // types import { CycleIssueResponse, ICycle, IIssue } from "types"; // fetch-keys import { CYCLE_DETAILS } from "constants/fetch-keys"; -import { renderShortNumericDateFormat } from "helpers/date-time.helper"; - type Props = { issues: IIssue[]; cycle: ICycle | undefined; diff --git a/apps/app/components/states/create-update-state-inline.tsx b/apps/app/components/states/create-update-state-inline.tsx index 46043b0cdda..7d67f8fcd29 100644 --- a/apps/app/components/states/create-update-state-inline.tsx +++ b/apps/app/components/states/create-update-state-inline.tsx @@ -1,5 +1,7 @@ import React, { useEffect } from "react"; +import { useRouter } from "next/router"; + import { mutate } from "swr"; // react-hook-form @@ -15,15 +17,13 @@ import useToast from "hooks/use-toast"; // ui import { Button, CustomSelect, Input } from "components/ui"; // types -import type { IState, StateResponse } from "types"; +import type { IState } from "types"; // fetch-keys import { STATE_LIST } from "constants/fetch-keys"; // constants import { GROUP_CHOICES } from "constants/project"; type Props = { - workspaceSlug?: string; - projectId?: string; data: IState | null; onClose: () => void; selectedGroup: StateGroup | null; @@ -37,13 +37,10 @@ const defaultValues: Partial = { group: "backlog", }; -export const CreateUpdateStateInline: React.FC = ({ - workspaceSlug, - projectId, - data, - onClose, - selectedGroup, -}) => { +export const CreateUpdateStateInline: React.FC = ({ data, onClose, selectedGroup }) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + const { setToastAlert } = useToast(); const { @@ -59,16 +56,18 @@ export const CreateUpdateStateInline: React.FC = ({ }); useEffect(() => { - if (data === null) return; + if (!data) return; + reset(data); }, [data, reset]); useEffect(() => { - if (!data) - reset({ - ...defaultValues, - group: selectedGroup ?? "backlog", - }); + if (data) return; + + reset({ + ...defaultValues, + group: selectedGroup ?? "backlog", + }); }, [selectedGroup, data, reset]); const handleClose = () => { @@ -78,14 +77,15 @@ export const CreateUpdateStateInline: React.FC = ({ const onSubmit = async (formData: IState) => { if (!workspaceSlug || !projectId || isSubmitting) return; + const payload: IState = { ...formData, }; if (!data) { await stateService - .createState(workspaceSlug, projectId, { ...payload }) + .createState(workspaceSlug as string, projectId as string, { ...payload }) .then((res) => { - mutate(STATE_LIST(projectId)); + mutate(STATE_LIST(projectId as string)); handleClose(); setToastAlert({ @@ -103,11 +103,11 @@ export const CreateUpdateStateInline: React.FC = ({ }); } else { await stateService - .updateState(workspaceSlug, projectId, data.id, { + .updateState(workspaceSlug as string, projectId as string, data.id, { ...payload, }) .then((res) => { - mutate(STATE_LIST(projectId)); + mutate(STATE_LIST(projectId as string)); handleClose(); setToastAlert({ diff --git a/apps/app/components/states/index.ts b/apps/app/components/states/index.ts index 63bb55a9ced..167e12d8b13 100644 --- a/apps/app/components/states/index.ts +++ b/apps/app/components/states/index.ts @@ -1,3 +1,4 @@ export * from "./create-update-state-inline"; export * from "./create-update-state-modal"; export * from "./delete-state-modal"; +export * from "./single-state"; diff --git a/apps/app/components/states/single-state.tsx b/apps/app/components/states/single-state.tsx new file mode 100644 index 00000000000..5e9aff80c6c --- /dev/null +++ b/apps/app/components/states/single-state.tsx @@ -0,0 +1,217 @@ +import { useState } from "react"; + +import { useRouter } from "next/router"; + +import { mutate } from "swr"; + +// services +import stateService from "services/state.service"; +// hooks +import useToast from "hooks/use-toast"; +// ui +import { Tooltip } from "components/ui"; +// icons +import { + ArrowDownIcon, + ArrowUpIcon, + PencilSquareIcon, + TrashIcon, +} from "@heroicons/react/24/outline"; +// helpers +import { addSpaceIfCamelCase } from "helpers/string.helper"; +import { groupBy, orderArrayBy } from "helpers/array.helper"; +import { orderStateGroups } from "helpers/state.helper"; +// types +import { IState } from "types"; +import { StateGroup } from "components/states"; +// fetch-keys +import { STATE_LIST } from "constants/fetch-keys"; + +type Props = { + index: number; + currentGroup: string; + state: IState; + statesList: IState[]; + activeGroup: StateGroup; + handleEditState: () => void; + handleDeleteState: () => void; +}; + +export const SingleState: React.FC = ({ + index, + currentGroup, + state, + statesList, + activeGroup, + handleEditState, + handleDeleteState, +}) => { + const [isSubmitting, setIsSubmitting] = useState(false); + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { setToastAlert } = useToast(); + + const groupLength = statesList.filter((s) => s.group === currentGroup).length; + + const handleMakeDefault = (stateId: string) => { + setIsSubmitting(true); + + const currentDefaultState = statesList.find((s) => s.default); + + if (currentDefaultState) + stateService + .patchState(workspaceSlug as string, projectId as string, currentDefaultState?.id ?? "", { + default: false, + }) + .then(() => { + stateService + .patchState(workspaceSlug as string, projectId as string, stateId, { + default: true, + }) + .then((res) => { + mutate(STATE_LIST(projectId as string)); + setToastAlert({ + type: "success", + title: "Successful", + message: `${res.name} state set to default successfuly.`, + }); + setIsSubmitting(false); + }) + .catch((err) => { + setToastAlert({ + type: "error", + title: "Error", + message: "Error in setting the state to default.", + }); + setIsSubmitting(false); + }); + }); + else + stateService + .patchState(workspaceSlug as string, projectId as string, stateId, { + default: true, + }) + .then((res) => { + mutate(STATE_LIST(projectId as string)); + setToastAlert({ + type: "success", + title: "Successful", + message: `${res.name} state set to default successfuly.`, + }); + setIsSubmitting(false); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error", + message: "Error in setting the state to default.", + }); + setIsSubmitting(false); + }); + }; + + const handleMove = (state: IState, index: number, direction: "up" | "down") => { + let newSequence = 15000; + + if (direction === "up") { + if (index === 1) newSequence = statesList[0].sequence - 15000; + else newSequence = (statesList[index - 2].sequence + statesList[index - 1].sequence) / 2; + } else { + if (index === groupLength - 2) newSequence = statesList[groupLength - 1].sequence + 15000; + else newSequence = (statesList[index + 2].sequence + statesList[index + 1].sequence) / 2; + } + + let newStatesList = statesList.map((s) => { + if (s.id === state.id) + return { + ...s, + sequence: newSequence, + }; + + return s; + }); + newStatesList = orderArrayBy(newStatesList, "sequence", "ascending"); + mutate( + STATE_LIST(projectId as string), + orderStateGroups(groupBy(newStatesList, "group")), + false + ); + + stateService + .patchState(workspaceSlug as string, projectId as string, state.id, { + sequence: newSequence, + }) + .then((res) => { + console.log(res); + mutate(STATE_LIST(projectId as string)); + }) + .catch((err) => { + console.error(err); + }); + }; + + return ( +
+
+
+
{addSpaceIfCamelCase(state.name)}
+
+
+ {index !== 0 && ( + + )} + {!(index === groupLength - 1) && ( + + )} + {state.default ? ( + Default + ) : ( + + )} + + + + +
+
+ ); +}; diff --git a/apps/app/components/ui/tooltip.tsx b/apps/app/components/ui/tooltip.tsx new file mode 100644 index 00000000000..5c91b7f0d96 --- /dev/null +++ b/apps/app/components/ui/tooltip.tsx @@ -0,0 +1,66 @@ +import React, { useEffect, useState } from "react"; + +export type Props = { + direction?: "top" | "right" | "bottom" | "left"; + content: string | React.ReactNode; + margin?: string; + children: React.ReactNode; + className?: string; + disabled?: boolean; +}; + +export const Tooltip: React.FC = ({ + content, + direction = "top", + children, + margin = "24px", + className = "", + disabled = false, +}) => { + const [active, setActive] = useState(false); + const [styleConfig, setStyleConfig] = useState(`top-[calc(-100%-${margin})]`); + let timeout: any; + + const showToolTip = () => { + timeout = setTimeout(() => { + setActive(true); + }, 300); + }; + + const hideToolTip = () => { + clearInterval(timeout); + setActive(false); + }; + + const tooltipStyles = { + top: "left-[50%] translate-x-[-50%] before:contents-[''] before:border-solid before:border-transparent before:h-0 before:w-0 before:absolute before:pointer-events-none before:border-[6px] before:left-[50%] before:ml-[calc(6px*-1)] before:top-full before:border-t-black", + + right: "right-[-100%] top-[50%] translate-x-0 translate-y-[-50%]", + + bottom: + "left-[50%] translate-x-[-50%] before:contents-[''] before:border-solid before:border-transparent before:h-0 before:w-0 before:absolute before:pointer-events-none before:border-[6px] before:left-[50%] before:ml-[calc(6px*-1)] before:bottom-full before:border-b-black", + + left: "left-[-100%] top-[50%] translate-x-0 translate-y-[-50%]", + }; + + useEffect(() => { + const styleConfig = `${direction}-[calc(-100%-${margin})]`; + setStyleConfig(styleConfig); + }, [margin, direction]); + + return ( +
+ {children} + {active && ( +
+ {content} +
+ )} +
+ ); +}; diff --git a/apps/app/components/ui/tooltip/index.tsx b/apps/app/components/ui/tooltip/index.tsx deleted file mode 100644 index d49013a8bb9..00000000000 --- a/apps/app/components/ui/tooltip/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { useEffect, useState } from "react"; - -export type Props = { - direction?: "top" | "right" | "bottom" | "left"; - content: string | React.ReactNode; - margin?: string; - children: React.ReactNode; - customStyle?: string; -}; - -const Tooltip: React.FC = ({ - content, - direction = "top", - children, - margin = "24px", - customStyle, -}) => { - const [active, setActive] = useState(false); - const [styleConfig, setStyleConfig] = useState("top-[calc(-100%-24px)]"); - let timeout: any; - - const showToolTip = () => { - timeout = setTimeout(() => { - setActive(true); - }, 300); - }; - - const hideToolTip = () => { - clearInterval(timeout); - setActive(false); - }; - - const tooltipStyles = { - top: ` - left-[50%] translate-x-[-50%] before:contents-[""] before:border-solid - before:border-transparent before:h-0 before:w-0 before:absolute before:pointer-events-none - before:border-[6px] before:left-[50%] before:ml-[calc(6px*-1)] before:top-full before:border-t-black`, - - right: ` - right-[-100%] top-[50%] - translate-x-0 translate-y-[-50%] `, - - bottom: ` - left-[50%] translate-x-[-50%] before:contents-[""] before:border-solid - before:border-transparent before:h-0 before:w-0 before:absolute before:pointer-events-none - before:border-[6px] before:left-[50%] before:ml-[calc(6px*-1)] before:bottom-full before:border-b-black`, - - left: ` - left-[-100%] top-[50%] - translate-x-0 translate-y-[-50%] `, - }; - - useEffect(() => { - const styleConfig = direction + "-[calc(-100%-" + margin + ")]"; - setStyleConfig(styleConfig); - }, [margin, direction]); - - return ( -
- {children} - {active && ( -
- {content} -
- )} -
- ); -}; - -export default Tooltip; diff --git a/apps/app/hooks/use-issue-properties.tsx b/apps/app/hooks/use-issue-properties.tsx index c33cc3e61ba..08ff54362ce 100644 --- a/apps/app/hooks/use-issue-properties.tsx +++ b/apps/app/hooks/use-issue-properties.tsx @@ -17,7 +17,6 @@ const initialValues: Properties = { sub_issue_count: false, }; -// TODO: CHECK THIS LOGIC const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => { const [properties, setProperties] = useState(initialValues); @@ -34,6 +33,7 @@ const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => { useEffect(() => { if (!issueProperties || !workspaceSlug || !projectId || !user) return; + setProperties({ ...initialValues, ...issueProperties.properties }); if (Object.keys(issueProperties).length === 0) @@ -53,6 +53,7 @@ const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => { if (!workspaceSlug || !user) return; setProperties((prev) => ({ ...prev, [key]: !prev[key] })); + if (issueProperties && projectId) { mutateIssueProperties( (prev) => diff --git a/apps/app/layouts/app-layout/index.tsx b/apps/app/layouts/app-layout/index.tsx index d6462fdd374..fddc5f2f518 100644 --- a/apps/app/layouts/app-layout/index.tsx +++ b/apps/app/layouts/app-layout/index.tsx @@ -13,7 +13,7 @@ import useUser from "hooks/use-user"; import { Button, Spinner } from "components/ui"; // components import { NotAuthorizedView } from "components/core"; -import CommandPalette from "components/command-palette"; +import { CommandPalette } from "components/command-palette"; import { JoinProject } from "components/project"; // local components import Container from "layouts/container"; diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx index 41ca8734388..1915dc3890d 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx @@ -4,24 +4,22 @@ import { useRouter } from "next/router"; import useSWR from "swr"; -// react-hook-form -import { Controller, SubmitHandler, useForm } from "react-hook-form"; -// react-color -import { TwitterPicker } from "react-color"; -// headless ui -import { Popover, Transition } from "@headlessui/react"; // services import projectService from "services/project.service"; -import workspaceService from "services/workspace.service"; import issuesService from "services/issues.service"; // lib import { requiredAdmin } from "lib/auth"; // layouts import AppLayout from "layouts/app-layout"; // components -import { SingleLabel } from "components/labels"; +import { + CreateUpdateLabelInline, + LabelsListModal, + SingleLabel, + SingleLabelGroup, +} from "components/labels"; // ui -import { Button, Input, Loader } from "components/ui"; +import { Button, Loader } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons import { PlusIcon } from "@heroicons/react/24/outline"; @@ -29,19 +27,21 @@ import { PlusIcon } from "@heroicons/react/24/outline"; import { IIssueLabels, UserAuth } from "types"; import type { NextPageContext, NextPage } from "next"; // fetch-keys -import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS, WORKSPACE_DETAILS } from "constants/fetch-keys"; - -const defaultValues: Partial = { - name: "", - color: "#ff0000", -}; +import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; const LabelsSettings: NextPage = (props) => { const { isMember, isOwner, isViewer, isGuest } = props; + // create/edit label form const [labelForm, setLabelForm] = useState(false); + + // edit label const [isUpdating, setIsUpdating] = useState(false); - const [labelIdForUpdate, setLabelIdForUpdate] = useState(null); + const [labelToUpdate, setLabelToUpdate] = useState(null); + + // labels list modal + const [labelsListModal, setLabelsListModal] = useState(false); + const [parentLabel, setParentLabel] = useState(undefined); const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -60,57 +60,20 @@ const LabelsSettings: NextPage = (props) => { : null ); - const { - register, - handleSubmit, - reset, - control, - setValue, - formState: { errors, isSubmitting }, - watch, - } = useForm({ defaultValues }); - const newLabel = () => { - reset(); setIsUpdating(false); setLabelForm(true); }; + const addLabelToGroup = (parentLabel: IIssueLabels) => { + setLabelsListModal(true); + setParentLabel(parentLabel); + }; + const editLabel = (label: IIssueLabels) => { setLabelForm(true); - setValue("color", label.color); - setValue("name", label.name); setIsUpdating(true); - setLabelIdForUpdate(label.id); - }; - - const handleLabelCreate: SubmitHandler = async (formData) => { - if (!workspaceSlug || !projectDetails || isSubmitting) return; - - await issuesService - .createIssueLabel(workspaceSlug as string, projectDetails.id, formData) - .then((res) => { - mutate((prevData) => [res, ...(prevData ?? [])], false); - reset(defaultValues); - setLabelForm(false); - }); - }; - - const handleLabelUpdate: SubmitHandler = async (formData) => { - if (!workspaceSlug || !projectDetails || isSubmitting) return; - - await issuesService - .patchIssueLabel(workspaceSlug as string, projectDetails.id, labelIdForUpdate ?? "", formData) - .then((res) => { - console.log(res); - reset(defaultValues); - mutate( - (prevData) => - prevData?.map((p) => (p.id === labelIdForUpdate ? { ...p, ...formData } : p)), - false - ); - setLabelForm(false); - }); + setLabelToUpdate(label); }; const handleLabelDelete = (labelId: string) => { @@ -128,146 +91,85 @@ const LabelsSettings: NextPage = (props) => { }; return ( - - - - - } - > -
-
-

Labels

-

Manage the labels of this project.

-
-
-

Manage labels

- -
-
-
-
- - {({ open }) => ( - <> - - {watch("color") && watch("color") !== "" && ( - - )} - - - - - ( - onChange(value.hex)} - /> - )} - /> - - - - )} - -
-
- -
- - {isUpdating ? ( - - ) : ( - - )}
- <> - {issueLabels ? ( - issueLabels.map((label) => ( - - )) - ) : ( - - - - - - - )} - -
-
-
+
+ + <> + {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={editLabel} + handleLabelDelete={handleLabelDelete} + /> + ); + } else + return ( + + ); + }) + ) : ( + + + + + + + )} + +
+ + + ); }; diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx index 267f812a356..4cec9379e71 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx @@ -4,22 +4,26 @@ import { useRouter } from "next/router"; import useSWR from "swr"; +// lib +import { requiredAdmin } from "lib/auth"; // services import stateService from "services/state.service"; import projectService from "services/project.service"; -// lib -import { requiredAdmin } from "lib/auth"; // layouts import AppLayout from "layouts/app-layout"; // components -import { CreateUpdateStateInline, DeleteStateModal, StateGroup } from "components/states"; +import { + CreateUpdateStateInline, + DeleteStateModal, + SingleState, + StateGroup, +} from "components/states"; // ui import { Loader } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons -import { PencilSquareIcon, PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { PlusIcon } from "@heroicons/react/24/outline"; // helpers -import { addSpaceIfCamelCase } from "helpers/string.helper"; import { getStatesList, orderStateGroups } from "helpers/state.helper"; // types import { UserAuth } from "types"; @@ -34,9 +38,8 @@ const StatesSettings: NextPage = (props) => { const [selectedState, setSelectedState] = useState(null); const [selectDeleteState, setSelectDeleteState] = useState(null); - const { - query: { workspaceSlug, projectId }, - } = useRouter(); + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; const { data: projectDetails } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, @@ -99,54 +102,33 @@ const StatesSettings: NextPage = (props) => {
{key === activeGroup && ( { setActiveGroup(null); setSelectedState(null); }} - workspaceSlug={workspaceSlug as string} data={null} selectedGroup={key as keyof StateGroup} /> )} - {orderedStateGroups[key].map((state) => + {orderedStateGroups[key].map((state, index) => state.id !== selectedState ? ( -
-
-
-
{addSpaceIfCamelCase(state.name)}
-
-
- - -
-
+ index={index} + currentGroup={key} + state={state} + statesList={statesList} + activeGroup={activeGroup} + handleEditState={() => setSelectedState(state.id)} + handleDeleteState={() => setSelectDeleteState(state.id)} + /> ) : (
{ setActiveGroup(null); setSelectedState(null); }} - workspaceSlug={workspaceSlug as string} data={ statesList?.find((state) => state.id === selectedState) ?? null } diff --git a/apps/app/types/state.d.ts b/apps/app/types/state.d.ts index 5f69ae49f32..29096c30280 100644 --- a/apps/app/types/state.d.ts +++ b/apps/app/types/state.d.ts @@ -1,17 +1,18 @@ export interface IState { readonly id: string; + color: string; readonly created_at: Date; - readonly updated_at: Date; - name: string; + readonly created_by: string; + default: boolean; description: string; - color: string; + group: "backlog" | "unstarted" | "started" | "completed" | "cancelled"; + name: string; + project: string; + sequence: number; readonly slug: string; - readonly created_by: string; + readonly updated_at: Date; readonly updated_by: string; - project: string; workspace: string; - sequence: number; - group: "backlog" | "unstarted" | "started" | "completed" | "cancelled"; } export interface StateResponse {