forked from github/plane
feat: label grouping in dropdowns, default state in project settings (#266)
* feat: label grouping in dropdowns, default state in project settings * feat: label disclosure default open * refactor: label setting page * chore: tooltip component updated * chore: tooltip component updated * feat/state_sequence_change
This commit is contained in:
parent
7c06be19fc
commit
a403c0c346
@ -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 React, { useState, useCallback, useEffect } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@ -14,7 +13,7 @@ import useTheme from "hooks/use-theme";
|
|||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useUser from "hooks/use-user";
|
import useUser from "hooks/use-user";
|
||||||
// components
|
// components
|
||||||
import ShortcutsModal from "components/command-palette/shortcuts";
|
import { ShortcutsModal } from "components/command-palette";
|
||||||
import { BulkDeleteIssuesModal } from "components/core";
|
import { BulkDeleteIssuesModal } from "components/core";
|
||||||
import { CreateProjectModal } from "components/project";
|
import { CreateProjectModal } from "components/project";
|
||||||
import { CreateUpdateIssueModal } from "components/issues";
|
import { CreateUpdateIssueModal } from "components/issues";
|
||||||
@ -36,7 +35,7 @@ import { IIssue } from "types";
|
|||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { USER_ISSUE } from "constants/fetch-keys";
|
import { USER_ISSUE } from "constants/fetch-keys";
|
||||||
|
|
||||||
const CommandPalette: React.FC = () => {
|
export const CommandPalette: React.FC = () => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
const [isPaletteOpen, setIsPaletteOpen] = useState(false);
|
const [isPaletteOpen, setIsPaletteOpen] = useState(false);
|
||||||
@ -369,5 +368,3 @@ const CommandPalette: React.FC = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CommandPalette;
|
|
2
apps/app/components/command-palette/index.ts
Normal file
2
apps/app/components/command-palette/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./command-pallette";
|
||||||
|
export * from "./shortcuts-modal";
|
@ -41,7 +41,7 @@ const shortcuts = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
const filteredShortcuts = shortcuts.filter((shortcut) =>
|
const filteredShortcuts = shortcuts.filter((shortcut) =>
|
||||||
@ -150,5 +150,3 @@ const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
</Transition.Root>
|
</Transition.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ShortcutsModal;
|
|
@ -1,5 +1,6 @@
|
|||||||
export * from "./board-view";
|
export * from "./board-view";
|
||||||
export * from "./list-view";
|
export * from "./list-view";
|
||||||
|
export * from "./sidebar";
|
||||||
export * from "./bulk-delete-issues-modal";
|
export * from "./bulk-delete-issues-modal";
|
||||||
export * from "./existing-issues-list-modal";
|
export * from "./existing-issues-list-modal";
|
||||||
export * from "./image-upload-modal";
|
export * from "./image-upload-modal";
|
||||||
|
2
apps/app/components/core/sidebar/index.ts
Normal file
2
apps/app/components/core/sidebar/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./sidebar-progress-stats";
|
||||||
|
export * from "./single-progress-stats";
|
@ -11,7 +11,7 @@ import { Tab } from "@headlessui/react";
|
|||||||
import issuesServices from "services/issues.service";
|
import issuesServices from "services/issues.service";
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
// components
|
// components
|
||||||
import SingleProgressStats from "components/core/sidebar/single-progress-stats";
|
import { SingleProgressStats } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { Avatar } from "components/ui";
|
import { Avatar } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -36,7 +36,7 @@ const stateGroupColours: {
|
|||||||
completed: "#096e8d",
|
completed: "#096e8d",
|
||||||
};
|
};
|
||||||
|
|
||||||
const SidebarProgressStats: React.FC<Props> = ({ groupedIssues, issues }) => {
|
export const SidebarProgressStats: React.FC<Props> = ({ groupedIssues, issues }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
||||||
@ -180,5 +180,3 @@ const SidebarProgressStats: React.FC<Props> = ({ groupedIssues, issues }) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SidebarProgressStats;
|
|
||||||
|
@ -8,22 +8,22 @@ type TSingleProgressStatsProps = {
|
|||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SingleProgressStats: React.FC<TSingleProgressStatsProps> = ({ title, completed, total }) => (
|
export const SingleProgressStats: React.FC<TSingleProgressStatsProps> = ({
|
||||||
<>
|
title,
|
||||||
<div className="flex items-center justify-between w-full py-3 text-xs border-b-[1px] border-gray-200">
|
completed,
|
||||||
<div className="flex items-center justify-start w-1/2 gap-2">{title}</div>
|
total,
|
||||||
<div className="flex items-center justify-end w-1/2 gap-1 px-2">
|
}) => (
|
||||||
<div className="flex h-5 justify-center items-center gap-1 ">
|
<div className="flex items-center justify-between w-full py-3 text-xs border-b-[1px] border-gray-200">
|
||||||
<span className="h-4 w-4 ">
|
<div className="flex items-center justify-start w-1/2 gap-2">{title}</div>
|
||||||
<CircularProgressbar value={completed} maxValue={total} strokeWidth={10} />
|
<div className="flex items-center justify-end w-1/2 gap-1 px-2">
|
||||||
</span>
|
<div className="flex h-5 justify-center items-center gap-1 ">
|
||||||
<span className="w-8 text-right">{Math.floor((completed / total) * 100)}%</span>
|
<span className="h-4 w-4 ">
|
||||||
</div>
|
<CircularProgressbar value={completed} maxValue={total} strokeWidth={10} />
|
||||||
<span>of</span>
|
</span>
|
||||||
<span>{total}</span>
|
<span className="w-8 text-right">{Math.floor((completed / total) * 100)}%</span>
|
||||||
</div>
|
</div>
|
||||||
|
<span>of</span>
|
||||||
|
<span>{total}</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default SingleProgressStats;
|
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
|
|
||||||
|
// headless ui
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import cycleService from "services/cycles.service";
|
import cycleService from "services/cycles.service";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { CycleForm } from "components/cycles";
|
import { CycleForm } from "components/cycles";
|
||||||
// helpers
|
|
||||||
import { renderDateFormat } from "helpers/date-time.helper";
|
|
||||||
// types
|
// types
|
||||||
import type { ICycle } from "types";
|
import type { ICycle } from "types";
|
||||||
// fetch keys
|
// fetch keys
|
||||||
@ -20,8 +23,14 @@ export interface CycleModalProps {
|
|||||||
initialData?: ICycle;
|
initialData?: ICycle;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CycleModal: React.FC<CycleModalProps> = (props) => {
|
export const CycleModal: React.FC<CycleModalProps> = ({
|
||||||
const { isOpen, handleClose, initialData, projectId, workspaceSlug } = props;
|
isOpen,
|
||||||
|
handleClose,
|
||||||
|
initialData,
|
||||||
|
projectId,
|
||||||
|
workspaceSlug,
|
||||||
|
}) => {
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const createCycle = (payload: Partial<ICycle>) => {
|
const createCycle = (payload: Partial<ICycle>) => {
|
||||||
cycleService
|
cycleService
|
||||||
@ -31,12 +40,11 @@ export const CycleModal: React.FC<CycleModalProps> = (props) => {
|
|||||||
handleClose();
|
handleClose();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
// TODO: Handle this ERROR.
|
setToastAlert({
|
||||||
// Object.keys(err).map((key) => {
|
type: "error",
|
||||||
// setError(key as keyof typeof defaultValues, {
|
title: "Error",
|
||||||
// message: err[key].join(", "),
|
message: "Error in creating cycle. Please try again!",
|
||||||
// });
|
});
|
||||||
// });
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -48,12 +56,11 @@ export const CycleModal: React.FC<CycleModalProps> = (props) => {
|
|||||||
handleClose();
|
handleClose();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
// TODO: Handle this ERROR.
|
setToastAlert({
|
||||||
// Object.keys(err).map((key) => {
|
type: "error",
|
||||||
// setError(key as keyof typeof defaultValues, {
|
title: "Error",
|
||||||
// message: err[key].join(", "),
|
message: "Error in updating cycle. Please try again!",
|
||||||
// });
|
});
|
||||||
// });
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import { useForm } from "react-hook-form";
|
|||||||
// headless ui
|
// headless ui
|
||||||
import { Combobox, Transition } from "@headlessui/react";
|
import { Combobox, Transition } from "@headlessui/react";
|
||||||
// icons
|
// icons
|
||||||
import { TagIcon } from "@heroicons/react/24/outline";
|
import { RectangleGroupIcon, TagIcon } from "@heroicons/react/24/outline";
|
||||||
// services
|
// services
|
||||||
import issuesServices from "services/issues.service";
|
import issuesServices from "services/issues.service";
|
||||||
// types
|
// types
|
||||||
@ -58,8 +58,6 @@ export const IssueLabelSelect: React.FC<Props> = ({ value, onChange, projectId }
|
|||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { isSubmitting },
|
formState: { isSubmitting },
|
||||||
setFocus,
|
setFocus,
|
||||||
reset,
|
reset,
|
||||||
@ -69,16 +67,10 @@ export const IssueLabelSelect: React.FC<Props> = ({ value, onChange, projectId }
|
|||||||
isOpen && setFocus("name");
|
isOpen && setFocus("name");
|
||||||
}, [isOpen, setFocus]);
|
}, [isOpen, setFocus]);
|
||||||
|
|
||||||
const options = issueLabels?.map((label) => ({
|
|
||||||
value: label.id,
|
|
||||||
display: label.name,
|
|
||||||
color: label.color,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const filteredOptions =
|
const filteredOptions =
|
||||||
query === ""
|
query === ""
|
||||||
? options
|
? issueLabels
|
||||||
: options?.filter((option) => option.display.toLowerCase().includes(query.toLowerCase()));
|
: issueLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase()));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -98,10 +90,9 @@ export const IssueLabelSelect: React.FC<Props> = ({ value, onChange, projectId }
|
|||||||
<TagIcon className="h-3 w-3 text-gray-500" />
|
<TagIcon className="h-3 w-3 text-gray-500" />
|
||||||
<span className={`flex items-center gap-2 ${!value ? "" : "text-gray-900"}`}>
|
<span className={`flex items-center gap-2 ${!value ? "" : "text-gray-900"}`}>
|
||||||
{Array.isArray(value)
|
{Array.isArray(value)
|
||||||
? value
|
? value.map((v) => issueLabels?.find((l) => l.id === v)?.name).join(", ") ||
|
||||||
.map((v) => options?.find((option) => option.value === v)?.display)
|
"Labels"
|
||||||
.join(", ") || "Labels"
|
: issueLabels?.find((l) => l.id === value)?.name || "Labels"}
|
||||||
: options?.find((option) => option.value === value)?.display || "Labels"}
|
|
||||||
</span>
|
</span>
|
||||||
</Combobox.Button>
|
</Combobox.Button>
|
||||||
|
|
||||||
@ -122,31 +113,62 @@ export const IssueLabelSelect: React.FC<Props> = ({ value, onChange, projectId }
|
|||||||
displayValue={(assigned: any) => assigned?.name}
|
displayValue={(assigned: any) => assigned?.name}
|
||||||
/>
|
/>
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
{filteredOptions ? (
|
{issueLabels && filteredOptions ? (
|
||||||
filteredOptions.length > 0 ? (
|
filteredOptions.length > 0 ? (
|
||||||
filteredOptions.map((option) => (
|
filteredOptions.map((label) => {
|
||||||
<Combobox.Option
|
const children = issueLabels?.filter((l) => l.parent === label.id);
|
||||||
key={option.value}
|
|
||||||
className={({ active, selected }) =>
|
if (children.length === 0) {
|
||||||
`${active ? "bg-indigo-50" : ""} ${
|
if (!label.parent)
|
||||||
selected ? "bg-indigo-50 font-medium" : ""
|
return (
|
||||||
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900`
|
<Combobox.Option
|
||||||
}
|
key={label.id}
|
||||||
value={option.value}
|
className={({ active, selected }) =>
|
||||||
>
|
`${active ? "bg-indigo-50" : ""} ${
|
||||||
{issueLabels && (
|
selected ? "bg-indigo-50 font-medium" : ""
|
||||||
<>
|
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900`
|
||||||
<span
|
}
|
||||||
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
value={label.id}
|
||||||
style={{
|
>
|
||||||
backgroundColor: option.color,
|
<span
|
||||||
}}
|
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||||
/>
|
style={{
|
||||||
{option.display}
|
backgroundColor: label?.color ?? "green",
|
||||||
</>
|
}}
|
||||||
)}
|
/>
|
||||||
</Combobox.Option>
|
{label.name}
|
||||||
))
|
</Combobox.Option>
|
||||||
|
);
|
||||||
|
} else
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-50 border-y border-gray-400">
|
||||||
|
<div className="flex select-none font-medium items-center gap-2 truncate p-2 text-gray-900">
|
||||||
|
<RectangleGroupIcon className="h-3 w-3" /> {label.name}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{children.map((child) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={child.id}
|
||||||
|
className={({ active, selected }) =>
|
||||||
|
`${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}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: child?.color ?? "green",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{child.name}
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-gray-500 px-2">No labels found</p>
|
<p className="text-xs text-gray-500 px-2">No labels found</p>
|
||||||
)
|
)
|
||||||
|
@ -38,6 +38,7 @@ import {
|
|||||||
TrashIcon,
|
TrashIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
XMarkIcon,
|
XMarkIcon,
|
||||||
|
RectangleGroupIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
@ -298,30 +299,31 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="basis-1/2">
|
<div className="basis-1/2">
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{watchIssue("labels_list")?.map((label) => {
|
{watchIssue("labels_list")?.map((labelId) => {
|
||||||
const singleLabel = issueLabels?.find((l) => l.id === label);
|
const label = issueLabels?.find((l) => l.id === labelId);
|
||||||
|
|
||||||
if (!singleLabel) return null;
|
if (label)
|
||||||
|
return (
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={singleLabel.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 !== label);
|
|
||||||
submitChanges({
|
|
||||||
labels_list: updatedLabels,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
<span
|
||||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
key={label.id}
|
||||||
style={{ backgroundColor: singleLabel?.color ?? "green" }}
|
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={() => {
|
||||||
{singleLabel.name}
|
const updatedLabels = watchIssue("labels_list")?.filter(
|
||||||
<XMarkIcon className="h-2 w-2 group-hover:text-red-500" />
|
(l) => l !== labelId
|
||||||
</span>
|
);
|
||||||
);
|
submitChanges({
|
||||||
|
labels_list: updatedLabels,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||||
|
style={{ backgroundColor: label?.color ?? "green" }}
|
||||||
|
/>
|
||||||
|
{label.name}
|
||||||
|
<XMarkIcon className="h-2 w-2 group-hover:text-red-500" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@ -336,58 +338,95 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
disabled={isNotAllowed}
|
disabled={isNotAllowed}
|
||||||
>
|
>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<div className="relative">
|
||||||
<Listbox.Label className="sr-only">Label</Listbox.Label>
|
<Listbox.Button
|
||||||
<div className="relative">
|
className={`flex ${
|
||||||
<Listbox.Button
|
isNotAllowed
|
||||||
className={`flex ${
|
? "cursor-not-allowed"
|
||||||
isNotAllowed
|
: "cursor-pointer hover:bg-gray-100"
|
||||||
? "cursor-not-allowed"
|
} items-center gap-2 rounded-2xl border px-2 py-0.5 text-xs`}
|
||||||
: "cursor-pointer hover:bg-gray-100"
|
>
|
||||||
} items-center gap-2 rounded-2xl border px-2 py-0.5 text-xs`}
|
Select Label
|
||||||
>
|
</Listbox.Button>
|
||||||
Select Label
|
|
||||||
</Listbox.Button>
|
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
show={open}
|
show={open}
|
||||||
as={React.Fragment}
|
as={React.Fragment}
|
||||||
leave="transition ease-in duration-100"
|
leave="transition ease-in duration-100"
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-28 w-40 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-28 w-40 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
{issueLabels ? (
|
{issueLabels ? (
|
||||||
issueLabels.length > 0 ? (
|
issueLabels.length > 0 ? (
|
||||||
issueLabels.map((label: IIssueLabels) => (
|
issueLabels.map((label: IIssueLabels) => {
|
||||||
<Listbox.Option
|
const children = issueLabels?.filter(
|
||||||
key={label.id}
|
(l) => l.parent === label.id
|
||||||
className={({ active, selected }) =>
|
);
|
||||||
`${
|
|
||||||
active || selected ? "bg-indigo-50" : ""
|
if (children.length === 0) {
|
||||||
} relative flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900`
|
if (!label.parent)
|
||||||
}
|
return (
|
||||||
value={label.id}
|
<Listbox.Option
|
||||||
>
|
key={label.id}
|
||||||
<span
|
className={({ active, selected }) =>
|
||||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
`${active || selected ? "bg-indigo-50" : ""} ${
|
||||||
style={{ backgroundColor: label.color ?? "green" }}
|
selected ? "font-medium" : ""
|
||||||
/>
|
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900`
|
||||||
{label.name}
|
}
|
||||||
</Listbox.Option>
|
value={label.id}
|
||||||
))
|
>
|
||||||
) : (
|
<span
|
||||||
<div className="text-center">No labels found</div>
|
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||||
)
|
style={{
|
||||||
|
backgroundColor: label?.color ?? "green",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{label.name}
|
||||||
|
</Listbox.Option>
|
||||||
|
);
|
||||||
|
} else
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-50 border-y border-gray-400">
|
||||||
|
<div className="flex select-none font-medium items-center gap-2 truncate p-2 text-gray-900">
|
||||||
|
<RectangleGroupIcon className="h-3 w-3" />{" "}
|
||||||
|
{label.name}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{children.map((child) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={child.id}
|
||||||
|
className={({ active, selected }) =>
|
||||||
|
`${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}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: child?.color ?? "green",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{child.name}
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
<Spinner />
|
<div className="text-center">No labels found</div>
|
||||||
)}
|
)
|
||||||
</div>
|
) : (
|
||||||
</Listbox.Options>
|
<Spinner />
|
||||||
</Transition>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Listbox>
|
</Listbox>
|
||||||
)}
|
)}
|
||||||
|
@ -69,10 +69,10 @@ export const ViewPrioritySelect: React.FC<Props> = ({
|
|||||||
{PRIORITIES?.map((priority) => (
|
{PRIORITIES?.map((priority) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={priority}
|
key={priority}
|
||||||
className={({ active }) =>
|
className={({ active, selected }) =>
|
||||||
`flex cursor-pointer select-none items-center gap-x-2 px-3 py-2 capitalize ${
|
`${active || selected ? "bg-indigo-50" : ""} ${
|
||||||
active ? "bg-indigo-50" : "bg-white"
|
selected ? "font-medium" : ""
|
||||||
}`
|
} flex cursor-pointer select-none items-center gap-x-2 px-3 py-2 capitalize`
|
||||||
}
|
}
|
||||||
value={priority}
|
value={priority}
|
||||||
>
|
>
|
||||||
|
189
apps/app/components/labels/create-update-label-inline.tsx
Normal file
189
apps/app/components/labels/create-update-label-inline.tsx
Normal file
@ -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<React.SetStateAction<boolean>>;
|
||||||
|
isUpdating: boolean;
|
||||||
|
labelToUpdate: IIssueLabels | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultValues: Partial<IIssueLabels> = {
|
||||||
|
name: "",
|
||||||
|
color: "#ff0000",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CreateUpdateLabelInline: React.FC<Props> = ({
|
||||||
|
labelForm,
|
||||||
|
setLabelForm,
|
||||||
|
isUpdating,
|
||||||
|
labelToUpdate,
|
||||||
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
register,
|
||||||
|
reset,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
} = useForm<IIssueLabels>({
|
||||||
|
defaultValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleLabelCreate: SubmitHandler<IIssueLabels> = async (formData) => {
|
||||||
|
if (!workspaceSlug || !projectId || isSubmitting) return;
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.createIssueLabel(workspaceSlug as string, projectId as string, formData)
|
||||||
|
.then((res) => {
|
||||||
|
mutate<IIssueLabels[]>(
|
||||||
|
PROJECT_ISSUE_LABELS(projectId as string),
|
||||||
|
(prevData) => [res, ...(prevData ?? [])],
|
||||||
|
false
|
||||||
|
);
|
||||||
|
reset(defaultValues);
|
||||||
|
setLabelForm(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLabelUpdate: SubmitHandler<IIssueLabels> = 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<IIssueLabels[]>(
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-2 rounded-md border p-3 md:w-2/3 ${
|
||||||
|
labelForm ? "" : "hidden"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="h-8 w-8 flex-shrink-0">
|
||||||
|
<Popover className="relative z-10 flex h-full w-full items-center justify-center rounded-xl bg-gray-200">
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Popover.Button
|
||||||
|
className={`group inline-flex items-center text-base font-medium focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 ${
|
||||||
|
open ? "text-gray-900" : "text-gray-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{watch("color") && watch("color") !== "" && (
|
||||||
|
<span
|
||||||
|
className="h-4 w-4 rounded"
|
||||||
|
style={{
|
||||||
|
backgroundColor: watch("color") ?? "green",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Popover.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="transition ease-out duration-200"
|
||||||
|
enterFrom="opacity-0 translate-y-1"
|
||||||
|
enterTo="opacity-100 translate-y-0"
|
||||||
|
leave="transition ease-in duration-150"
|
||||||
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
|
leaveTo="opacity-0 translate-y-1"
|
||||||
|
>
|
||||||
|
<Popover.Panel className="absolute top-full left-0 z-20 mt-3 w-screen max-w-xs px-2 sm:px-0">
|
||||||
|
<Controller
|
||||||
|
name="color"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<TwitterPicker color={value} onChange={(value) => onChange(value.hex)} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Popover.Panel>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-col justify-center">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="labelName"
|
||||||
|
name="name"
|
||||||
|
register={register}
|
||||||
|
placeholder="Label title"
|
||||||
|
validations={{
|
||||||
|
required: "Label title is required",
|
||||||
|
}}
|
||||||
|
error={errors.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
reset();
|
||||||
|
setLabelForm(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
{isUpdating ? (
|
||||||
|
<Button type="button" onClick={handleSubmit(handleLabelUpdate)} disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? "Updating" : "Update"}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button type="button" onClick={handleSubmit(handleLabelCreate)} disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? "Adding" : "Add"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,2 +1,4 @@
|
|||||||
|
export * from "./create-update-label-inline";
|
||||||
export * from "./labels-list-modal";
|
export * from "./labels-list-modal";
|
||||||
|
export * from "./single-label-group";
|
||||||
export * from "./single-label";
|
export * from "./single-label";
|
||||||
|
136
apps/app/components/labels/single-label-group.tsx
Normal file
136
apps/app/components/labels/single-label-group.tsx
Normal file
@ -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<Props> = ({
|
||||||
|
label,
|
||||||
|
labelChildren,
|
||||||
|
addLabelToGroup,
|
||||||
|
editLabel,
|
||||||
|
handleLabelDelete,
|
||||||
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const removeFromGroup = (label: IIssueLabels) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
mutate<IIssueLabels[]>(
|
||||||
|
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 (
|
||||||
|
<Disclosure as="div" className="rounded-md border p-3 text-gray-900 md:w-2/3" defaultOpen>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between gap-2 cursor-pointer">
|
||||||
|
<Disclosure.Button>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span>
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={`h-4 w-4 text-gray-500 ${!open ? "-rotate-90 transform" : ""}`}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<RectangleGroupIcon className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
<h6 className="text-sm">{label.name}</h6>
|
||||||
|
</div>
|
||||||
|
</Disclosure.Button>
|
||||||
|
<CustomMenu ellipsis>
|
||||||
|
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
|
||||||
|
Add more labels
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem onClick={() => editLabel(label)}>Edit</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem onClick={() => handleLabelDelete(label.id)}>
|
||||||
|
Delete
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
enter="transition duration-100 ease-out"
|
||||||
|
enterFrom="transform opacity-0"
|
||||||
|
enterTo="transform opacity-100"
|
||||||
|
leave="transition duration-75 ease-out"
|
||||||
|
leaveFrom="transform opacity-100"
|
||||||
|
leaveTo="transform opacity-0"
|
||||||
|
>
|
||||||
|
<Disclosure.Panel>
|
||||||
|
<div className="mt-2 ml-4">
|
||||||
|
{labelChildren.map((child) => (
|
||||||
|
<div
|
||||||
|
key={child.id}
|
||||||
|
className="group pl-4 py-1 flex items-center justify-between rounded text-sm hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<h5 className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: child.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{child.name}
|
||||||
|
</h5>
|
||||||
|
<div className="opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto">
|
||||||
|
<CustomMenu ellipsis>
|
||||||
|
<CustomMenu.MenuItem onClick={() => removeFromGroup(child)}>
|
||||||
|
Remove from group
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem onClick={() => editLabel(child)}>
|
||||||
|
Edit
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem onClick={() => handleLabelDelete(child.id)}>
|
||||||
|
Delete
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Disclosure>
|
||||||
|
);
|
||||||
|
};
|
@ -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
|
// ui
|
||||||
import { CustomMenu } from "components/ui";
|
import { CustomMenu } from "components/ui";
|
||||||
// icons
|
|
||||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
|
||||||
// types
|
// types
|
||||||
import { IIssueLabels } from "types";
|
import { IIssueLabels } from "types";
|
||||||
// fetch-keys
|
|
||||||
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label: IIssueLabels;
|
label: IIssueLabels;
|
||||||
issueLabels: IIssueLabels[];
|
addLabelToGroup: (parentLabel: IIssueLabels) => void;
|
||||||
editLabel: (label: IIssueLabels) => void;
|
editLabel: (label: IIssueLabels) => void;
|
||||||
handleLabelDelete: (labelId: string) => void;
|
handleLabelDelete: (labelId: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleLabel: React.FC<Props> = ({
|
export const SingleLabel: React.FC<Props> = ({
|
||||||
label,
|
label,
|
||||||
issueLabels,
|
addLabelToGroup,
|
||||||
editLabel,
|
editLabel,
|
||||||
handleLabelDelete,
|
handleLabelDelete,
|
||||||
}) => {
|
}) => (
|
||||||
const [labelsListModal, setLabelsListModal] = useState(false);
|
<div className="gap-2 space-y-3 divide-y rounded-md border p-3 md:w-2/3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
const router = useRouter();
|
<div className="flex items-center gap-2">
|
||||||
const { workspaceSlug, projectId } = router.query;
|
<span
|
||||||
|
className="h-3 w-3 flex-shrink-0 rounded-full"
|
||||||
const children = issueLabels?.filter((l) => l.parent === label.id);
|
style={{
|
||||||
|
backgroundColor: label.color,
|
||||||
const removeFromGroup = (label: IIssueLabels) => {
|
}}
|
||||||
if (!workspaceSlug || !projectId) return;
|
/>
|
||||||
|
<h6 className="text-sm">{label.name}</h6>
|
||||||
mutate<IIssueLabels[]>(
|
</div>
|
||||||
PROJECT_ISSUE_LABELS(projectId as string),
|
<CustomMenu ellipsis>
|
||||||
(prevData) =>
|
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
|
||||||
prevData?.map((l) => {
|
Convert to group
|
||||||
if (l.id === label.id) return { ...l, parent: null };
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem onClick={() => editLabel(label)}>Edit</CustomMenu.MenuItem>
|
||||||
return l;
|
<CustomMenu.MenuItem onClick={() => handleLabelDelete(label.id)}>
|
||||||
}),
|
Delete
|
||||||
false
|
</CustomMenu.MenuItem>
|
||||||
);
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
issuesService
|
</div>
|
||||||
.patchIssueLabel(workspaceSlug as string, projectId as string, label.id, {
|
);
|
||||||
parent: null,
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
mutate(PROJECT_ISSUE_LABELS(projectId as string));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<LabelsListModal
|
|
||||||
isOpen={labelsListModal}
|
|
||||||
handleClose={() => setLabelsListModal(false)}
|
|
||||||
parent={label}
|
|
||||||
/>
|
|
||||||
{children && children.length === 0 ? (
|
|
||||||
label.parent === "" || !label.parent ? (
|
|
||||||
<div className="gap-2 space-y-3 divide-y rounded-md border p-3 md:w-2/3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span
|
|
||||||
className="h-3 w-3 flex-shrink-0 rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: label.color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<h6 className="text-sm">{label.name}</h6>
|
|
||||||
</div>
|
|
||||||
<CustomMenu ellipsis>
|
|
||||||
<CustomMenu.MenuItem onClick={() => setLabelsListModal(true)}>
|
|
||||||
Convert to group
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
<CustomMenu.MenuItem onClick={() => editLabel(label)}>Edit</CustomMenu.MenuItem>
|
|
||||||
<CustomMenu.MenuItem onClick={() => handleLabelDelete(label.id)}>
|
|
||||||
Delete
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null
|
|
||||||
) : (
|
|
||||||
<Disclosure as="div" className="relative z-20 rounded-md border p-3 text-gray-900 md:w-2/3">
|
|
||||||
{({ open }) => (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center justify-between gap-2 cursor-pointer">
|
|
||||||
<Disclosure.Button>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span>
|
|
||||||
<ChevronDownIcon
|
|
||||||
className={`h-4 w-4 text-gray-500 ${!open ? "-rotate-90 transform" : ""}`}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<h6 className="text-sm">{label.name}</h6>
|
|
||||||
</div>
|
|
||||||
</Disclosure.Button>
|
|
||||||
<CustomMenu ellipsis>
|
|
||||||
<CustomMenu.MenuItem onClick={() => setLabelsListModal(true)}>
|
|
||||||
Add more labels
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
<CustomMenu.MenuItem onClick={() => editLabel(label)}>Edit</CustomMenu.MenuItem>
|
|
||||||
<CustomMenu.MenuItem onClick={() => handleLabelDelete(label.id)}>
|
|
||||||
Delete
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
<Transition
|
|
||||||
show={open}
|
|
||||||
enter="transition duration-100 ease-out"
|
|
||||||
enterFrom="transform opacity-0"
|
|
||||||
enterTo="transform opacity-100"
|
|
||||||
leave="transition duration-75 ease-out"
|
|
||||||
leaveFrom="transform opacity-100"
|
|
||||||
leaveTo="transform opacity-0"
|
|
||||||
>
|
|
||||||
<Disclosure.Panel>
|
|
||||||
<div className="mt-2 ml-4">
|
|
||||||
{children.map((child) => (
|
|
||||||
<div
|
|
||||||
key={child.id}
|
|
||||||
className="group pl-4 py-1 flex items-center justify-between rounded text-sm hover:bg-gray-100"
|
|
||||||
>
|
|
||||||
<h5 className="flex items-center gap-2">
|
|
||||||
<span
|
|
||||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: child.color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{child.name}
|
|
||||||
</h5>
|
|
||||||
<div className="opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto">
|
|
||||||
<CustomMenu ellipsis>
|
|
||||||
<CustomMenu.MenuItem onClick={() => removeFromGroup(child)}>
|
|
||||||
Remove from group
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
<CustomMenu.MenuItem onClick={() => editLabel(child)}>
|
|
||||||
Edit
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
<CustomMenu.MenuItem onClick={() => handleLabelDelete(child.id)}>
|
|
||||||
Delete
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Disclosure.Panel>
|
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Disclosure>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
@ -30,6 +30,8 @@ import {
|
|||||||
} from "components/modules";
|
} from "components/modules";
|
||||||
|
|
||||||
import "react-circular-progressbar/dist/styles.css";
|
import "react-circular-progressbar/dist/styles.css";
|
||||||
|
// components
|
||||||
|
import { SidebarProgressStats } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { CustomDatePicker, Loader } from "components/ui";
|
import { CustomDatePicker, Loader } from "components/ui";
|
||||||
// helpers
|
// helpers
|
||||||
@ -40,7 +42,6 @@ import { groupBy } from "helpers/array.helper";
|
|||||||
import { IIssue, IModule, ModuleIssueResponse } from "types";
|
import { IIssue, IModule, ModuleIssueResponse } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { MODULE_DETAILS } from "constants/fetch-keys";
|
import { MODULE_DETAILS } from "constants/fetch-keys";
|
||||||
import SidebarProgressStats from "components/core/sidebar/sidebar-progress-stats";
|
|
||||||
|
|
||||||
const defaultValues: Partial<IModule> = {
|
const defaultValues: Partial<IModule> = {
|
||||||
lead: "",
|
lead: "",
|
||||||
|
@ -20,17 +20,16 @@ import useToast from "hooks/use-toast";
|
|||||||
// services
|
// services
|
||||||
import cyclesService from "services/cycles.service";
|
import cyclesService from "services/cycles.service";
|
||||||
// components
|
// components
|
||||||
import SidebarProgressStats from "components/core/sidebar/sidebar-progress-stats";
|
import { SidebarProgressStats } from "components/core";
|
||||||
// helpers
|
// helpers
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
import { groupBy } from "helpers/array.helper";
|
import { groupBy } from "helpers/array.helper";
|
||||||
|
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { CycleIssueResponse, ICycle, IIssue } from "types";
|
import { CycleIssueResponse, ICycle, IIssue } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { CYCLE_DETAILS } from "constants/fetch-keys";
|
import { CYCLE_DETAILS } from "constants/fetch-keys";
|
||||||
|
|
||||||
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issues: IIssue[];
|
issues: IIssue[];
|
||||||
cycle: ICycle | undefined;
|
cycle: ICycle | undefined;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
|
|
||||||
// react-hook-form
|
// react-hook-form
|
||||||
@ -15,15 +17,13 @@ import useToast from "hooks/use-toast";
|
|||||||
// ui
|
// ui
|
||||||
import { Button, CustomSelect, Input } from "components/ui";
|
import { Button, CustomSelect, Input } from "components/ui";
|
||||||
// types
|
// types
|
||||||
import type { IState, StateResponse } from "types";
|
import type { IState } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { STATE_LIST } from "constants/fetch-keys";
|
import { STATE_LIST } from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
import { GROUP_CHOICES } from "constants/project";
|
import { GROUP_CHOICES } from "constants/project";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
workspaceSlug?: string;
|
|
||||||
projectId?: string;
|
|
||||||
data: IState | null;
|
data: IState | null;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
selectedGroup: StateGroup | null;
|
selectedGroup: StateGroup | null;
|
||||||
@ -37,13 +37,10 @@ const defaultValues: Partial<IState> = {
|
|||||||
group: "backlog",
|
group: "backlog",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreateUpdateStateInline: React.FC<Props> = ({
|
export const CreateUpdateStateInline: React.FC<Props> = ({ data, onClose, selectedGroup }) => {
|
||||||
workspaceSlug,
|
const router = useRouter();
|
||||||
projectId,
|
const { workspaceSlug, projectId } = router.query;
|
||||||
data,
|
|
||||||
onClose,
|
|
||||||
selectedGroup,
|
|
||||||
}) => {
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -59,16 +56,18 @@ export const CreateUpdateStateInline: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data === null) return;
|
if (!data) return;
|
||||||
|
|
||||||
reset(data);
|
reset(data);
|
||||||
}, [data, reset]);
|
}, [data, reset]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data)
|
if (data) return;
|
||||||
reset({
|
|
||||||
...defaultValues,
|
reset({
|
||||||
group: selectedGroup ?? "backlog",
|
...defaultValues,
|
||||||
});
|
group: selectedGroup ?? "backlog",
|
||||||
|
});
|
||||||
}, [selectedGroup, data, reset]);
|
}, [selectedGroup, data, reset]);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
@ -78,14 +77,15 @@ export const CreateUpdateStateInline: React.FC<Props> = ({
|
|||||||
|
|
||||||
const onSubmit = async (formData: IState) => {
|
const onSubmit = async (formData: IState) => {
|
||||||
if (!workspaceSlug || !projectId || isSubmitting) return;
|
if (!workspaceSlug || !projectId || isSubmitting) return;
|
||||||
|
|
||||||
const payload: IState = {
|
const payload: IState = {
|
||||||
...formData,
|
...formData,
|
||||||
};
|
};
|
||||||
if (!data) {
|
if (!data) {
|
||||||
await stateService
|
await stateService
|
||||||
.createState(workspaceSlug, projectId, { ...payload })
|
.createState(workspaceSlug as string, projectId as string, { ...payload })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
mutate(STATE_LIST(projectId));
|
mutate(STATE_LIST(projectId as string));
|
||||||
handleClose();
|
handleClose();
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -103,11 +103,11 @@ export const CreateUpdateStateInline: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await stateService
|
await stateService
|
||||||
.updateState(workspaceSlug, projectId, data.id, {
|
.updateState(workspaceSlug as string, projectId as string, data.id, {
|
||||||
...payload,
|
...payload,
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
mutate(STATE_LIST(projectId));
|
mutate(STATE_LIST(projectId as string));
|
||||||
handleClose();
|
handleClose();
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./create-update-state-inline";
|
export * from "./create-update-state-inline";
|
||||||
export * from "./create-update-state-modal";
|
export * from "./create-update-state-modal";
|
||||||
export * from "./delete-state-modal";
|
export * from "./delete-state-modal";
|
||||||
|
export * from "./single-state";
|
||||||
|
217
apps/app/components/states/single-state.tsx
Normal file
217
apps/app/components/states/single-state.tsx
Normal file
@ -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<Props> = ({
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className={`group flex items-center justify-between gap-2 border-b bg-gray-50 p-3 ${
|
||||||
|
activeGroup !== currentGroup ? "last:border-0" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className="h-3 w-3 flex-shrink-0 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: state.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<h6 className="text-sm">{addSpaceIfCamelCase(state.name)}</h6>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{index !== 0 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="hidden group-hover:inline-block text-gray-400 hover:text-gray-900"
|
||||||
|
onClick={() => handleMove(state, index, "up")}
|
||||||
|
>
|
||||||
|
<ArrowUpIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{!(index === groupLength - 1) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="hidden group-hover:inline-block text-gray-400 hover:text-gray-900"
|
||||||
|
onClick={() => handleMove(state, index, "down")}
|
||||||
|
>
|
||||||
|
<ArrowDownIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{state.default ? (
|
||||||
|
<span className="text-xs text-gray-400">Default</span>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="hidden group-hover:inline-block text-xs text-gray-400 hover:text-gray-900"
|
||||||
|
onClick={() => handleMakeDefault(state.id)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Set as default
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<Tooltip content="Cannot delete the default state." disabled={!state.default}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${state.default ? "cursor-not-allowed" : ""} grid place-items-center`}
|
||||||
|
onClick={handleDeleteState}
|
||||||
|
disabled={state.default}
|
||||||
|
>
|
||||||
|
<TrashIcon className="h-4 w-4 text-red-400" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<button type="button" className="grid place-items-center" onClick={handleEditState}>
|
||||||
|
<PencilSquareIcon className="h-4 w-4 text-gray-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
66
apps/app/components/ui/tooltip.tsx
Normal file
66
apps/app/components/ui/tooltip.tsx
Normal file
@ -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<Props> = ({
|
||||||
|
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 (
|
||||||
|
<div className="relative inline-block" onMouseEnter={showToolTip} onMouseLeave={hideToolTip}>
|
||||||
|
{children}
|
||||||
|
{active && (
|
||||||
|
<div
|
||||||
|
className={`${className} ${
|
||||||
|
disabled ? "hidden" : ""
|
||||||
|
} absolute p-[6px] text-xs z-20 rounded leading-1 text-white bg-black text-center w-max max-w-[300px]
|
||||||
|
${tooltipStyles[direction]} ${styleConfig}`}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -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<Props> = ({
|
|
||||||
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 (
|
|
||||||
<div className="inline-block relative" onMouseEnter={showToolTip} onMouseLeave={hideToolTip}>
|
|
||||||
{children}
|
|
||||||
{active && (
|
|
||||||
<div
|
|
||||||
className={`absolute p-[6px] text-xs z-20 rounded leading-1 text-white bg-black text-center w-max max-w-[300px]
|
|
||||||
${tooltipStyles[direction]} ${customStyle ? customStyle : ""} ${styleConfig}`}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Tooltip;
|
|
@ -17,7 +17,6 @@ const initialValues: Properties = {
|
|||||||
sub_issue_count: false,
|
sub_issue_count: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: CHECK THIS LOGIC
|
|
||||||
const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => {
|
const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => {
|
||||||
const [properties, setProperties] = useState<Properties>(initialValues);
|
const [properties, setProperties] = useState<Properties>(initialValues);
|
||||||
|
|
||||||
@ -34,6 +33,7 @@ const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!issueProperties || !workspaceSlug || !projectId || !user) return;
|
if (!issueProperties || !workspaceSlug || !projectId || !user) return;
|
||||||
|
|
||||||
setProperties({ ...initialValues, ...issueProperties.properties });
|
setProperties({ ...initialValues, ...issueProperties.properties });
|
||||||
|
|
||||||
if (Object.keys(issueProperties).length === 0)
|
if (Object.keys(issueProperties).length === 0)
|
||||||
@ -53,6 +53,7 @@ const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => {
|
|||||||
if (!workspaceSlug || !user) return;
|
if (!workspaceSlug || !user) return;
|
||||||
|
|
||||||
setProperties((prev) => ({ ...prev, [key]: !prev[key] }));
|
setProperties((prev) => ({ ...prev, [key]: !prev[key] }));
|
||||||
|
|
||||||
if (issueProperties && projectId) {
|
if (issueProperties && projectId) {
|
||||||
mutateIssueProperties(
|
mutateIssueProperties(
|
||||||
(prev) =>
|
(prev) =>
|
||||||
|
@ -13,7 +13,7 @@ import useUser from "hooks/use-user";
|
|||||||
import { Button, Spinner } from "components/ui";
|
import { Button, Spinner } from "components/ui";
|
||||||
// components
|
// components
|
||||||
import { NotAuthorizedView } from "components/core";
|
import { NotAuthorizedView } from "components/core";
|
||||||
import CommandPalette from "components/command-palette";
|
import { CommandPalette } from "components/command-palette";
|
||||||
import { JoinProject } from "components/project";
|
import { JoinProject } from "components/project";
|
||||||
// local components
|
// local components
|
||||||
import Container from "layouts/container";
|
import Container from "layouts/container";
|
||||||
|
@ -4,24 +4,22 @@ import { useRouter } from "next/router";
|
|||||||
|
|
||||||
import useSWR from "swr";
|
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
|
// services
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
import workspaceService from "services/workspace.service";
|
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
// lib
|
// lib
|
||||||
import { requiredAdmin } from "lib/auth";
|
import { requiredAdmin } from "lib/auth";
|
||||||
// layouts
|
// layouts
|
||||||
import AppLayout from "layouts/app-layout";
|
import AppLayout from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
import { SingleLabel } from "components/labels";
|
import {
|
||||||
|
CreateUpdateLabelInline,
|
||||||
|
LabelsListModal,
|
||||||
|
SingleLabel,
|
||||||
|
SingleLabelGroup,
|
||||||
|
} from "components/labels";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input, Loader } from "components/ui";
|
import { Button, Loader } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// icons
|
// icons
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
@ -29,19 +27,21 @@ import { PlusIcon } from "@heroicons/react/24/outline";
|
|||||||
import { IIssueLabels, UserAuth } from "types";
|
import { IIssueLabels, UserAuth } from "types";
|
||||||
import type { NextPageContext, NextPage } from "next";
|
import type { NextPageContext, NextPage } from "next";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS, WORKSPACE_DETAILS } from "constants/fetch-keys";
|
import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||||
|
|
||||||
const defaultValues: Partial<IIssueLabels> = {
|
|
||||||
name: "",
|
|
||||||
color: "#ff0000",
|
|
||||||
};
|
|
||||||
|
|
||||||
const LabelsSettings: NextPage<UserAuth> = (props) => {
|
const LabelsSettings: NextPage<UserAuth> = (props) => {
|
||||||
const { isMember, isOwner, isViewer, isGuest } = props;
|
const { isMember, isOwner, isViewer, isGuest } = props;
|
||||||
|
|
||||||
|
// create/edit label form
|
||||||
const [labelForm, setLabelForm] = useState(false);
|
const [labelForm, setLabelForm] = useState(false);
|
||||||
|
|
||||||
|
// edit label
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
const [labelIdForUpdate, setLabelIdForUpdate] = useState<string | null>(null);
|
const [labelToUpdate, setLabelToUpdate] = useState<IIssueLabels | null>(null);
|
||||||
|
|
||||||
|
// labels list modal
|
||||||
|
const [labelsListModal, setLabelsListModal] = useState(false);
|
||||||
|
const [parentLabel, setParentLabel] = useState<IIssueLabels | undefined>(undefined);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
@ -60,57 +60,20 @@ const LabelsSettings: NextPage<UserAuth> = (props) => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
control,
|
|
||||||
setValue,
|
|
||||||
formState: { errors, isSubmitting },
|
|
||||||
watch,
|
|
||||||
} = useForm<IIssueLabels>({ defaultValues });
|
|
||||||
|
|
||||||
const newLabel = () => {
|
const newLabel = () => {
|
||||||
reset();
|
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
setLabelForm(true);
|
setLabelForm(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addLabelToGroup = (parentLabel: IIssueLabels) => {
|
||||||
|
setLabelsListModal(true);
|
||||||
|
setParentLabel(parentLabel);
|
||||||
|
};
|
||||||
|
|
||||||
const editLabel = (label: IIssueLabels) => {
|
const editLabel = (label: IIssueLabels) => {
|
||||||
setLabelForm(true);
|
setLabelForm(true);
|
||||||
setValue("color", label.color);
|
|
||||||
setValue("name", label.name);
|
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
setLabelIdForUpdate(label.id);
|
setLabelToUpdate(label);
|
||||||
};
|
|
||||||
|
|
||||||
const handleLabelCreate: SubmitHandler<IIssueLabels> = 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<IIssueLabels> = 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);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLabelDelete = (labelId: string) => {
|
const handleLabelDelete = (labelId: string) => {
|
||||||
@ -128,146 +91,85 @@ const LabelsSettings: NextPage<UserAuth> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLayout
|
<>
|
||||||
settingsLayout="project"
|
<LabelsListModal
|
||||||
memberType={{ isMember, isOwner, isViewer, isGuest }}
|
isOpen={labelsListModal}
|
||||||
breadcrumbs={
|
handleClose={() => setLabelsListModal(false)}
|
||||||
<Breadcrumbs>
|
parent={parentLabel}
|
||||||
<BreadcrumbItem
|
/>
|
||||||
title={`${projectDetails?.name ?? "Project"}`}
|
<AppLayout
|
||||||
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
|
settingsLayout="project"
|
||||||
/>
|
memberType={{ isMember, isOwner, isViewer, isGuest }}
|
||||||
<BreadcrumbItem title="Labels Settings" />
|
breadcrumbs={
|
||||||
</Breadcrumbs>
|
<Breadcrumbs>
|
||||||
}
|
<BreadcrumbItem
|
||||||
>
|
title={`${projectDetails?.name ?? "Project"}`}
|
||||||
<section className="space-y-8">
|
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
|
||||||
<div>
|
/>
|
||||||
<h3 className="text-3xl font-bold leading-6 text-gray-900">Labels</h3>
|
<BreadcrumbItem title="Labels Settings" />
|
||||||
<p className="mt-4 text-sm text-gray-500">Manage the labels of this project.</p>
|
</Breadcrumbs>
|
||||||
</div>
|
}
|
||||||
<div className="flex items-center justify-between gap-2 md:w-2/3">
|
>
|
||||||
<h4 className="text-md mb-1 leading-6 text-gray-900">Manage labels</h4>
|
<section className="space-y-8">
|
||||||
<Button theme="secondary" className="flex items-center gap-x-1" onClick={newLabel}>
|
<div>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<h3 className="text-3xl font-bold leading-6 text-gray-900">Labels</h3>
|
||||||
New label
|
<p className="mt-4 text-sm text-gray-500">Manage the labels of this project.</p>
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-5">
|
|
||||||
<div
|
|
||||||
className={`flex items-center gap-2 rounded-md border p-3 md:w-2/3 ${
|
|
||||||
labelForm ? "" : "hidden"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="h-8 w-8 flex-shrink-0">
|
|
||||||
<Popover className="relative flex h-full w-full items-center justify-center rounded-xl bg-gray-200">
|
|
||||||
{({ open }) => (
|
|
||||||
<>
|
|
||||||
<Popover.Button
|
|
||||||
className={`group inline-flex items-center text-base font-medium focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 ${
|
|
||||||
open ? "text-gray-900" : "text-gray-500"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{watch("color") && watch("color") !== "" && (
|
|
||||||
<span
|
|
||||||
className="h-4 w-4 rounded"
|
|
||||||
style={{
|
|
||||||
backgroundColor: watch("color") ?? "green",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Popover.Button>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
as={React.Fragment}
|
|
||||||
enter="transition ease-out duration-200"
|
|
||||||
enterFrom="opacity-0 translate-y-1"
|
|
||||||
enterTo="opacity-100 translate-y-0"
|
|
||||||
leave="transition ease-in duration-150"
|
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
|
||||||
leaveTo="opacity-0 translate-y-1"
|
|
||||||
>
|
|
||||||
<Popover.Panel className="absolute top-full left-0 z-20 mt-3 w-screen max-w-xs px-2 sm:px-0">
|
|
||||||
<Controller
|
|
||||||
name="color"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<TwitterPicker
|
|
||||||
color={value}
|
|
||||||
onChange={(value) => onChange(value.hex)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Popover.Panel>
|
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-col justify-center">
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
id="labelName"
|
|
||||||
name="name"
|
|
||||||
register={register}
|
|
||||||
placeholder="Label title"
|
|
||||||
validations={{
|
|
||||||
required: "Label title is required",
|
|
||||||
}}
|
|
||||||
error={errors.name}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
theme="secondary"
|
|
||||||
onClick={() => {
|
|
||||||
reset();
|
|
||||||
setLabelForm(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
{isUpdating ? (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={handleSubmit(handleLabelUpdate)}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{isSubmitting ? "Updating" : "Update"}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={handleSubmit(handleLabelCreate)}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{isSubmitting ? "Adding" : "Add"}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<>
|
<div className="flex items-center justify-between gap-2 md:w-2/3">
|
||||||
{issueLabels ? (
|
<h4 className="text-md mb-1 leading-6 text-gray-900">Manage labels</h4>
|
||||||
issueLabels.map((label) => (
|
<Button theme="secondary" className="flex items-center gap-x-1" onClick={newLabel}>
|
||||||
<SingleLabel
|
<PlusIcon className="h-4 w-4" />
|
||||||
key={label.id}
|
New label
|
||||||
label={label}
|
</Button>
|
||||||
issueLabels={issueLabels}
|
</div>
|
||||||
editLabel={editLabel}
|
<div className="space-y-5">
|
||||||
handleLabelDelete={handleLabelDelete}
|
<CreateUpdateLabelInline
|
||||||
/>
|
labelForm={labelForm}
|
||||||
))
|
setLabelForm={setLabelForm}
|
||||||
) : (
|
isUpdating={isUpdating}
|
||||||
<Loader className="space-y-5 md:w-2/3">
|
labelToUpdate={labelToUpdate}
|
||||||
<Loader.Item height="40px" />
|
/>
|
||||||
<Loader.Item height="40px" />
|
<>
|
||||||
<Loader.Item height="40px" />
|
{issueLabels ? (
|
||||||
<Loader.Item height="40px" />
|
issueLabels.map((label) => {
|
||||||
</Loader>
|
const children = issueLabels?.filter((l) => l.parent === label.id);
|
||||||
)}
|
|
||||||
</>
|
if (children && children.length === 0) {
|
||||||
</div>
|
if (!label.parent)
|
||||||
</section>
|
return (
|
||||||
</AppLayout>
|
<SingleLabel
|
||||||
|
key={label.id}
|
||||||
|
label={label}
|
||||||
|
addLabelToGroup={() => addLabelToGroup(label)}
|
||||||
|
editLabel={editLabel}
|
||||||
|
handleLabelDelete={handleLabelDelete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else
|
||||||
|
return (
|
||||||
|
<SingleLabelGroup
|
||||||
|
key={label.id}
|
||||||
|
label={label}
|
||||||
|
labelChildren={children}
|
||||||
|
addLabelToGroup={addLabelToGroup}
|
||||||
|
editLabel={editLabel}
|
||||||
|
handleLabelDelete={handleLabelDelete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<Loader className="space-y-5 md:w-2/3">
|
||||||
|
<Loader.Item height="40px" />
|
||||||
|
<Loader.Item height="40px" />
|
||||||
|
<Loader.Item height="40px" />
|
||||||
|
<Loader.Item height="40px" />
|
||||||
|
</Loader>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</AppLayout>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,22 +4,26 @@ import { useRouter } from "next/router";
|
|||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// lib
|
||||||
|
import { requiredAdmin } from "lib/auth";
|
||||||
// services
|
// services
|
||||||
import stateService from "services/state.service";
|
import stateService from "services/state.service";
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
// lib
|
|
||||||
import { requiredAdmin } from "lib/auth";
|
|
||||||
// layouts
|
// layouts
|
||||||
import AppLayout from "layouts/app-layout";
|
import AppLayout from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
import { CreateUpdateStateInline, DeleteStateModal, StateGroup } from "components/states";
|
import {
|
||||||
|
CreateUpdateStateInline,
|
||||||
|
DeleteStateModal,
|
||||||
|
SingleState,
|
||||||
|
StateGroup,
|
||||||
|
} from "components/states";
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "components/ui";
|
import { Loader } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// icons
|
// icons
|
||||||
import { PencilSquareIcon, PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
|
||||||
import { getStatesList, orderStateGroups } from "helpers/state.helper";
|
import { getStatesList, orderStateGroups } from "helpers/state.helper";
|
||||||
// types
|
// types
|
||||||
import { UserAuth } from "types";
|
import { UserAuth } from "types";
|
||||||
@ -34,9 +38,8 @@ const StatesSettings: NextPage<UserAuth> = (props) => {
|
|||||||
const [selectedState, setSelectedState] = useState<string | null>(null);
|
const [selectedState, setSelectedState] = useState<string | null>(null);
|
||||||
const [selectDeleteState, setSelectDeleteState] = useState<string | null>(null);
|
const [selectDeleteState, setSelectDeleteState] = useState<string | null>(null);
|
||||||
|
|
||||||
const {
|
const router = useRouter();
|
||||||
query: { workspaceSlug, projectId },
|
const { workspaceSlug, projectId } = router.query;
|
||||||
} = useRouter();
|
|
||||||
|
|
||||||
const { data: projectDetails } = useSWR(
|
const { data: projectDetails } = useSWR(
|
||||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||||
@ -99,54 +102,33 @@ const StatesSettings: NextPage<UserAuth> = (props) => {
|
|||||||
<div className="space-y-1 rounded-xl border p-1 md:w-2/3">
|
<div className="space-y-1 rounded-xl border p-1 md:w-2/3">
|
||||||
{key === activeGroup && (
|
{key === activeGroup && (
|
||||||
<CreateUpdateStateInline
|
<CreateUpdateStateInline
|
||||||
projectId={projectDetails.id}
|
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setActiveGroup(null);
|
setActiveGroup(null);
|
||||||
setSelectedState(null);
|
setSelectedState(null);
|
||||||
}}
|
}}
|
||||||
workspaceSlug={workspaceSlug as string}
|
|
||||||
data={null}
|
data={null}
|
||||||
selectedGroup={key as keyof StateGroup}
|
selectedGroup={key as keyof StateGroup}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{orderedStateGroups[key].map((state) =>
|
{orderedStateGroups[key].map((state, index) =>
|
||||||
state.id !== selectedState ? (
|
state.id !== selectedState ? (
|
||||||
<div
|
<SingleState
|
||||||
key={state.id}
|
key={state.id}
|
||||||
className={`flex items-center justify-between gap-2 border-b bg-gray-50 p-3 ${
|
index={index}
|
||||||
activeGroup !== key ? "last:border-0" : ""
|
currentGroup={key}
|
||||||
}`}
|
state={state}
|
||||||
>
|
statesList={statesList}
|
||||||
<div className="flex items-center gap-2">
|
activeGroup={activeGroup}
|
||||||
<div
|
handleEditState={() => setSelectedState(state.id)}
|
||||||
className="h-3 w-3 flex-shrink-0 rounded-full"
|
handleDeleteState={() => setSelectDeleteState(state.id)}
|
||||||
style={{
|
/>
|
||||||
backgroundColor: state.color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<h6 className="text-sm">{addSpaceIfCamelCase(state.name)}</h6>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setSelectDeleteState(state.id)}
|
|
||||||
>
|
|
||||||
<TrashIcon className="h-4 w-4 text-red-400" />
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={() => setSelectedState(state.id)}>
|
|
||||||
<PencilSquareIcon className="h-4 w-4 text-gray-400" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="border-b last:border-b-0" key={state.id}>
|
<div className="border-b last:border-b-0" key={state.id}>
|
||||||
<CreateUpdateStateInline
|
<CreateUpdateStateInline
|
||||||
projectId={projectDetails.id}
|
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setActiveGroup(null);
|
setActiveGroup(null);
|
||||||
setSelectedState(null);
|
setSelectedState(null);
|
||||||
}}
|
}}
|
||||||
workspaceSlug={workspaceSlug as string}
|
|
||||||
data={
|
data={
|
||||||
statesList?.find((state) => state.id === selectedState) ?? null
|
statesList?.find((state) => state.id === selectedState) ?? null
|
||||||
}
|
}
|
||||||
|
19
apps/app/types/state.d.ts
vendored
19
apps/app/types/state.d.ts
vendored
@ -1,17 +1,18 @@
|
|||||||
export interface IState {
|
export interface IState {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly created_at: Date;
|
|
||||||
readonly updated_at: Date;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
color: string;
|
color: string;
|
||||||
readonly slug: string;
|
readonly created_at: Date;
|
||||||
readonly created_by: string;
|
readonly created_by: string;
|
||||||
readonly updated_by: string;
|
default: boolean;
|
||||||
project: string;
|
description: string;
|
||||||
workspace: string;
|
|
||||||
sequence: number;
|
|
||||||
group: "backlog" | "unstarted" | "started" | "completed" | "cancelled";
|
group: "backlog" | "unstarted" | "started" | "completed" | "cancelled";
|
||||||
|
name: string;
|
||||||
|
project: string;
|
||||||
|
sequence: number;
|
||||||
|
readonly slug: string;
|
||||||
|
readonly updated_at: Date;
|
||||||
|
readonly updated_by: string;
|
||||||
|
workspace: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StateResponse {
|
export interface StateResponse {
|
||||||
|
Loading…
Reference in New Issue
Block a user