mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
feat: all functionalities in cycle, crud for labels in settings
This commit is contained in:
parent
93552f190d
commit
259213851f
@ -22,7 +22,7 @@ import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
|||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
parentId: string;
|
parent: IIssue | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormInput = {
|
type FormInput = {
|
||||||
@ -30,7 +30,7 @@ type FormInput = {
|
|||||||
cycleId: string;
|
cycleId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddAsSubIssue: React.FC<Props> = ({ isOpen, setIsOpen, parentId }) => {
|
const AddAsSubIssue: React.FC<Props> = ({ isOpen, setIsOpen, parent }) => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
const { activeWorkspace, activeProject, issues } = useUser();
|
const { activeWorkspace, activeProject, issues } = useUser();
|
||||||
@ -54,7 +54,7 @@ const AddAsSubIssue: React.FC<Props> = ({ isOpen, setIsOpen, parentId }) => {
|
|||||||
const addAsSubIssue = (issueId: string) => {
|
const addAsSubIssue = (issueId: string) => {
|
||||||
if (activeWorkspace && activeProject) {
|
if (activeWorkspace && activeProject) {
|
||||||
issuesServices
|
issuesServices
|
||||||
.patchIssue(activeWorkspace.slug, activeProject.id, issueId, { parent: parentId })
|
.patchIssue(activeWorkspace.slug, activeProject.id, issueId, { parent: parent?.id })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
mutate<IssueResponse>(
|
mutate<IssueResponse>(
|
||||||
PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id),
|
PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id),
|
||||||
@ -127,8 +127,9 @@ const AddAsSubIssue: React.FC<Props> = ({ isOpen, setIsOpen, parentId }) => {
|
|||||||
<ul className="text-sm text-gray-700">
|
<ul className="text-sm text-gray-700">
|
||||||
{filteredIssues.map((issue) => {
|
{filteredIssues.map((issue) => {
|
||||||
if (
|
if (
|
||||||
(issue.parent === "" || issue.parent === null) &&
|
(issue.parent === "" || issue.parent === null) && // issue does not have any other parent
|
||||||
issue.id !== parentId
|
issue.id !== parent?.id && // issue is not itself
|
||||||
|
issue.id !== parent?.parent // issue is not it's parent
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<Combobox.Option
|
<Combobox.Option
|
||||||
|
@ -4,40 +4,35 @@ import { useRouter } from "next/router";
|
|||||||
// swr
|
// swr
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
// react hook form
|
// react hook form
|
||||||
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||||
|
// services
|
||||||
|
import issuesServices from "lib/services/issues.services";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
import useTheme from "lib/hooks/useTheme";
|
import useTheme from "lib/hooks/useTheme";
|
||||||
import useToast from "lib/hooks/useToast";
|
import useToast from "lib/hooks/useToast";
|
||||||
// icons
|
// icons
|
||||||
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
|
|
||||||
import {
|
import {
|
||||||
FolderIcon,
|
FolderIcon,
|
||||||
RectangleStackIcon,
|
RectangleStackIcon,
|
||||||
ClipboardDocumentListIcon,
|
ClipboardDocumentListIcon,
|
||||||
ArrowPathIcon,
|
MagnifyingGlassIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
// commons
|
|
||||||
import { classNames, copyTextToClipboard } from "constants/common";
|
|
||||||
// components
|
// components
|
||||||
import ShortcutsModal from "components/command-palette/shortcuts";
|
import ShortcutsModal from "components/command-palette/shortcuts";
|
||||||
import CreateProjectModal from "components/project/CreateProjectModal";
|
import CreateProjectModal from "components/project/CreateProjectModal";
|
||||||
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
|
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
|
||||||
import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal";
|
import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal";
|
||||||
|
// ui
|
||||||
|
import { Button } from "ui";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IProject, IssueResponse } from "types";
|
import { IIssue, IssueResponse } from "types";
|
||||||
import { Button, SearchListbox } from "ui";
|
|
||||||
import issuesServices from "lib/services/issues.services";
|
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import { PROJECTS_LIST, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
||||||
|
// constants
|
||||||
type ItemType = {
|
import { classNames, copyTextToClipboard } from "constants/common";
|
||||||
name: string;
|
|
||||||
url?: string;
|
|
||||||
onClick?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type FormInput = {
|
type FormInput = {
|
||||||
issue_ids: string[];
|
issue_ids: string[];
|
||||||
@ -45,8 +40,6 @@ type FormInput = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CommandPalette: React.FC = () => {
|
const CommandPalette: React.FC = () => {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
const [isPaletteOpen, setIsPaletteOpen] = useState(false);
|
const [isPaletteOpen, setIsPaletteOpen] = useState(false);
|
||||||
@ -55,7 +48,9 @@ const CommandPalette: React.FC = () => {
|
|||||||
const [isShortcutsModalOpen, setIsShortcutsModalOpen] = useState(false);
|
const [isShortcutsModalOpen, setIsShortcutsModalOpen] = useState(false);
|
||||||
const [isCreateCycleModalOpen, setIsCreateCycleModalOpen] = useState(false);
|
const [isCreateCycleModalOpen, setIsCreateCycleModalOpen] = useState(false);
|
||||||
|
|
||||||
const { activeWorkspace, activeProject, issues, cycles } = useUser();
|
const { activeWorkspace, activeProject, issues } = useUser();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const { toggleCollapsed } = useTheme();
|
const { toggleCollapsed } = useTheme();
|
||||||
|
|
||||||
@ -67,14 +62,7 @@ const CommandPalette: React.FC = () => {
|
|||||||
: issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ??
|
: issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ??
|
||||||
[];
|
[];
|
||||||
|
|
||||||
const {
|
const { register, handleSubmit, reset } = useForm<FormInput>();
|
||||||
register,
|
|
||||||
formState: { errors, isSubmitting },
|
|
||||||
handleSubmit,
|
|
||||||
control,
|
|
||||||
reset,
|
|
||||||
setError,
|
|
||||||
} = useForm<FormInput>();
|
|
||||||
|
|
||||||
const quickActions = [
|
const quickActions = [
|
||||||
{
|
{
|
||||||
@ -186,37 +174,6 @@ const CommandPalette: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddToCycle: SubmitHandler<FormInput> = (data) => {
|
|
||||||
if (!data.issue_ids || data.issue_ids.length === 0) {
|
|
||||||
setToastAlert({
|
|
||||||
title: "Error",
|
|
||||||
type: "error",
|
|
||||||
message: "Please select atleast one issue",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.cycleId) {
|
|
||||||
setToastAlert({
|
|
||||||
title: "Error",
|
|
||||||
type: "error",
|
|
||||||
message: "Please select a cycle",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeWorkspace && activeProject) {
|
|
||||||
issuesServices
|
|
||||||
.bulkAddIssuesToCycle(activeWorkspace.slug, activeProject.id, data.cycleId, data)
|
|
||||||
.then((res) => {
|
|
||||||
console.log(res);
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener("keydown", handleKeyDown);
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
@ -269,14 +226,7 @@ const CommandPalette: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white bg-opacity-80 shadow-2xl ring-1 ring-black ring-opacity-5 backdrop-blur backdrop-filter transition-all">
|
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white bg-opacity-80 shadow-2xl ring-1 ring-black ring-opacity-5 backdrop-blur backdrop-filter transition-all">
|
||||||
<form>
|
<form>
|
||||||
<Combobox
|
<Combobox>
|
||||||
// onChange={(item: ItemType) => {
|
|
||||||
// const { url, onClick } = item;
|
|
||||||
// if (url) router.push(url);
|
|
||||||
// else if (onClick) onClick();
|
|
||||||
// handleCommandPaletteClose();
|
|
||||||
// }}
|
|
||||||
>
|
|
||||||
<div className="relative m-1">
|
<div className="relative m-1">
|
||||||
<MagnifyingGlassIcon
|
<MagnifyingGlassIcon
|
||||||
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
|
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
|
||||||
@ -305,42 +255,53 @@ const CommandPalette: React.FC = () => {
|
|||||||
{filteredIssues.map((issue) => (
|
{filteredIssues.map((issue) => (
|
||||||
<Combobox.Option
|
<Combobox.Option
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
|
as="label"
|
||||||
|
htmlFor={`issue-${issue.id}`}
|
||||||
value={{
|
value={{
|
||||||
name: issue.name,
|
name: issue.name,
|
||||||
url: `/projects/${issue.project}/issues/${issue.id}`,
|
url: `/projects/${issue.project}/issues/${issue.id}`,
|
||||||
}}
|
}}
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
classNames(
|
classNames(
|
||||||
"flex cursor-pointer select-none items-center rounded-md px-3 py-2",
|
"flex items-center justify-between cursor-pointer select-none rounded-md px-3 py-2",
|
||||||
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
|
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{({ active }) => (
|
{({ active }) => (
|
||||||
<>
|
<>
|
||||||
{/* <FolderIcon
|
<div className="flex items-center gap-2">
|
||||||
className={classNames(
|
<input
|
||||||
"h-6 w-6 flex-none text-gray-900 text-opacity-40",
|
type="checkbox"
|
||||||
active ? "text-opacity-100" : ""
|
{...register("issue_ids")}
|
||||||
)}
|
id={`issue-${issue.id}`}
|
||||||
aria-hidden="true"
|
value={issue.id}
|
||||||
/> */}
|
/>
|
||||||
<input
|
<span
|
||||||
type="checkbox"
|
className={`h-1.5 w-1.5 block rounded-full`}
|
||||||
{...register("issue_ids")}
|
style={{
|
||||||
id={`issue-${issue.id}`}
|
backgroundColor: issue.state_detail.color,
|
||||||
value={issue.id}
|
}}
|
||||||
/>
|
/>
|
||||||
<label
|
<span className="text-xs text-gray-500">
|
||||||
htmlFor={`issue-${issue.id}`}
|
{activeProject?.identifier}-{issue.sequence_id}
|
||||||
className="ml-3 flex-auto truncate"
|
|
||||||
>
|
|
||||||
{issue.name}
|
|
||||||
</label>
|
|
||||||
{active && (
|
|
||||||
<span className="ml-3 flex-none text-gray-500">
|
|
||||||
Jump to...
|
|
||||||
</span>
|
</span>
|
||||||
|
{issue.name}
|
||||||
|
</div>
|
||||||
|
{active && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
router.push(
|
||||||
|
`/projects/${activeProject?.id}/issues/${issue.id}`
|
||||||
|
);
|
||||||
|
handleCommandPaletteClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="justify-self-end flex-none text-gray-500">
|
||||||
|
Jump to...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -405,31 +366,9 @@ const CommandPalette: React.FC = () => {
|
|||||||
</Combobox>
|
</Combobox>
|
||||||
|
|
||||||
<div className="flex justify-between items-center gap-2 p-3">
|
<div className="flex justify-between items-center gap-2 p-3">
|
||||||
<div className="flex items-center gap-2">
|
<Button onClick={handleSubmit(handleDelete)} theme="danger" size="sm">
|
||||||
<Controller
|
Delete selected
|
||||||
control={control}
|
</Button>
|
||||||
name="cycleId"
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<SearchListbox
|
|
||||||
title="Cycle"
|
|
||||||
optionsFontsize="sm"
|
|
||||||
options={cycles?.map((cycle) => {
|
|
||||||
return { value: cycle.id, display: cycle.name };
|
|
||||||
})}
|
|
||||||
multiple={false}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
icon={<ArrowPathIcon className="h-4 w-4 text-gray-400" />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button onClick={handleSubmit(handleAddToCycle)} size="sm">
|
|
||||||
Add to Cycle
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSubmit(handleDelete)} theme="danger" size="sm">
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<Button type="button" size="sm" onClick={handleCommandPaletteClose}>
|
<Button type="button" size="sm" onClick={handleCommandPaletteClose}>
|
||||||
Close
|
Close
|
||||||
|
@ -45,6 +45,7 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
|
|||||||
if (onChange) onChange(editorData);
|
if (onChange) onChange(editorData);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// function handleChange(state: EditorState, editor: LexicalEditor) {
|
// function handleChange(state: EditorState, editor: LexicalEditor) {
|
||||||
// state.read(() => {
|
// state.read(() => {
|
||||||
// onChange(state.toJSON());
|
// onChange(state.toJSON());
|
||||||
|
208
apps/app/components/project/cycles/CycleIssuesListModal.tsx
Normal file
208
apps/app/components/project/cycles/CycleIssuesListModal.tsx
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
// react
|
||||||
|
import React, { useState } from "react";
|
||||||
|
// react-hook-form
|
||||||
|
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
// headless ui
|
||||||
|
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||||
|
// ui
|
||||||
|
import { Button } from "ui";
|
||||||
|
// services
|
||||||
|
import issuesServices from "lib/services/issues.services";
|
||||||
|
// hooks
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
import useToast from "lib/hooks/useToast";
|
||||||
|
// icons
|
||||||
|
import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import { IIssue, IssueResponse } from "types";
|
||||||
|
// constants
|
||||||
|
import { classNames } from "constants/common";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleClose: () => void;
|
||||||
|
issues: IssueResponse | undefined;
|
||||||
|
cycleId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormInput = {
|
||||||
|
issue_ids: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const CycleIssuesListModal: React.FC<Props> = ({
|
||||||
|
isOpen,
|
||||||
|
handleClose: onClose,
|
||||||
|
issues,
|
||||||
|
cycleId,
|
||||||
|
}) => {
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
|
const { activeWorkspace, activeProject } = useUser();
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose();
|
||||||
|
setQuery("");
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
const { handleSubmit, reset, control } = useForm<FormInput>({
|
||||||
|
defaultValues: {
|
||||||
|
issue_ids: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAddToCycle: SubmitHandler<FormInput> = (data) => {
|
||||||
|
if (!data.issue_ids || data.issue_ids.length === 0) {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Error",
|
||||||
|
type: "error",
|
||||||
|
message: "Please select atleast one issue",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeWorkspace && activeProject) {
|
||||||
|
issuesServices
|
||||||
|
.bulkAddIssuesToCycle(activeWorkspace.slug, activeProject.id, cycleId, data)
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredIssues: IIssue[] =
|
||||||
|
query === ""
|
||||||
|
? issues?.results ?? []
|
||||||
|
: issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear>
|
||||||
|
<Dialog as="div" className="relative z-10" onClose={handleClose}>
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-gray-500 bg-opacity-25 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white bg-opacity-80 shadow-2xl ring-1 ring-black ring-opacity-5 backdrop-blur backdrop-filter transition-all">
|
||||||
|
<form>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="issue_ids"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Combobox as="div" {...field} multiple>
|
||||||
|
<div className="relative m-1">
|
||||||
|
<MagnifyingGlassIcon
|
||||||
|
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<Combobox.Input
|
||||||
|
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm outline-none"
|
||||||
|
placeholder="Search..."
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Combobox.Options
|
||||||
|
static
|
||||||
|
className="max-h-80 scroll-py-2 divide-y divide-gray-500 divide-opacity-10 overflow-y-auto"
|
||||||
|
>
|
||||||
|
{filteredIssues.length > 0 && (
|
||||||
|
<li className="p-2">
|
||||||
|
{query === "" && (
|
||||||
|
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-gray-900">
|
||||||
|
Select issues to add to cycle
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
<ul className="text-sm text-gray-700">
|
||||||
|
{filteredIssues.map((issue) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={issue.id}
|
||||||
|
as="label"
|
||||||
|
htmlFor={`issue-${issue.id}`}
|
||||||
|
value={issue.id}
|
||||||
|
className={({ active }) =>
|
||||||
|
classNames(
|
||||||
|
"flex items-center gap-2 cursor-pointer select-none w-full rounded-md px-3 py-2",
|
||||||
|
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ selected }) => (
|
||||||
|
<>
|
||||||
|
<input type="checkbox" checked={selected} readOnly />
|
||||||
|
<span
|
||||||
|
className={`h-1.5 w-1.5 block rounded-full`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: issue.state_detail.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{activeProject?.identifier}-{issue.sequence_id}
|
||||||
|
</span>
|
||||||
|
{issue.name}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</Combobox.Options>
|
||||||
|
|
||||||
|
{query !== "" && filteredIssues.length === 0 && (
|
||||||
|
<div className="py-14 px-6 text-center sm:px-14">
|
||||||
|
<RectangleStackIcon
|
||||||
|
className="mx-auto h-6 w-6 text-gray-900 text-opacity-40"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<p className="mt-4 text-sm text-gray-900">
|
||||||
|
We couldn{"'"}t find any issue with that term. Please try again.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Combobox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end items-center gap-2 p-3">
|
||||||
|
<Button type="button" theme="danger" size="sm" onClick={handleClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="button" size="sm" onClick={handleSubmit(handleAddToCycle)}>
|
||||||
|
Add to Cycle
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CycleIssuesListModal;
|
@ -1,258 +1,314 @@
|
|||||||
import React from "react";
|
// react
|
||||||
|
import React, { useState } from "react";
|
||||||
// next
|
// next
|
||||||
import { useRouter } from "next/router";
|
import Link from "next/link";
|
||||||
// swr
|
// swr
|
||||||
import useSWR from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Disclosure, Transition, Menu, Listbox } from "@headlessui/react";
|
import { Disclosure, Transition, Menu } from "@headlessui/react";
|
||||||
// fetch keys
|
|
||||||
import { PROJECT_ISSUES_LIST, CYCLE_ISSUES } from "constants/fetch-keys";
|
|
||||||
// services
|
// services
|
||||||
import issuesServices from "lib/services/issues.services";
|
|
||||||
import cycleServices from "lib/services/cycles.services";
|
import cycleServices from "lib/services/cycles.services";
|
||||||
// commons
|
// hooks
|
||||||
import { classNames, renderShortNumericDateFormat } from "constants/common";
|
import useUser from "lib/hooks/useUser";
|
||||||
|
// components
|
||||||
|
import CycleIssuesListModal from "./CycleIssuesListModal";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner } from "ui";
|
import { Spinner } from "ui";
|
||||||
// icons
|
// icons
|
||||||
import { PlusIcon, EllipsisHorizontalIcon, ChevronDownIcon } from "@heroicons/react/20/solid";
|
import { PlusIcon, EllipsisHorizontalIcon, ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||||
// types
|
// types
|
||||||
import type { ICycle, SprintViewProps as Props, SprintIssueResponse, IssueResponse } from "types";
|
import type { CycleViewProps as Props, CycleIssueResponse, IssueResponse } from "types";
|
||||||
|
// fetch keys
|
||||||
|
import { CYCLE_ISSUES } from "constants/fetch-keys";
|
||||||
|
// constants
|
||||||
|
import { renderShortNumericDateFormat } from "constants/common";
|
||||||
|
import issuesServices from "lib/services/issues.services";
|
||||||
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
|
import { Draggable } from "react-beautiful-dnd";
|
||||||
|
|
||||||
const SprintView: React.FC<Props> = ({
|
const CycleView: React.FC<Props> = ({
|
||||||
sprint,
|
cycle,
|
||||||
selectSprint,
|
selectSprint,
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
projectId,
|
projectId,
|
||||||
openIssueModal,
|
openIssueModal,
|
||||||
addIssueToSprint,
|
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
|
||||||
|
|
||||||
const { data: sprintIssues } = useSWR<SprintIssueResponse[]>(CYCLE_ISSUES(sprint.id), () =>
|
const { activeWorkspace, activeProject, issues } = useUser();
|
||||||
cycleServices.getCycleIssues(workspaceSlug, projectId, sprint.id)
|
|
||||||
|
const { data: cycleIssues } = useSWR<CycleIssueResponse[]>(CYCLE_ISSUES(cycle.id), () =>
|
||||||
|
cycleServices.getCycleIssues(workspaceSlug, projectId, cycle.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: projectIssues } = useSWR<IssueResponse>(
|
const removeIssueFromCycle = (cycleId: string, bridgeId: string) => {
|
||||||
projectId && workspaceSlug ? PROJECT_ISSUES_LIST(workspaceSlug, projectId) : null,
|
if (activeWorkspace && activeProject && cycleIssues) {
|
||||||
workspaceSlug ? () => issuesServices.getIssues(workspaceSlug, projectId) : null
|
mutate<CycleIssueResponse[]>(
|
||||||
);
|
CYCLE_ISSUES(cycleId),
|
||||||
|
(prevData) => prevData?.filter((p) => p.id !== bridgeId),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
issuesServices
|
||||||
|
.removeIssueFromCycle(activeWorkspace.slug, activeProject.id, cycleId, bridgeId)
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col gap-y-4 pb-5 relative">
|
<>
|
||||||
<Disclosure defaultOpen>
|
<CycleIssuesListModal
|
||||||
|
isOpen={cycleIssuesListModal}
|
||||||
|
handleClose={() => setCycleIssuesListModal(false)}
|
||||||
|
issues={issues}
|
||||||
|
cycleId={cycle.id}
|
||||||
|
/>
|
||||||
|
<Disclosure as="div" defaultOpen>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<div className="bg-gray-50 py-5 px-5 rounded">
|
<div className="bg-white px-4 py-2 rounded-lg space-y-3">
|
||||||
<div className="w-full h-full space-y-6 overflow-auto pb-10">
|
<div className="flex items-center">
|
||||||
<div className="w-full flex items-center">
|
<Disclosure.Button className="w-full">
|
||||||
<Disclosure.Button className="w-full">
|
<div className="flex items-center gap-x-2">
|
||||||
<div className="flex items-center gap-x-2">
|
<span>
|
||||||
|
<ChevronDownIcon
|
||||||
|
width={22}
|
||||||
|
className={`text-gray-500 ${!open ? "transform -rotate-90" : ""}`}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<h2 className="font-medium leading-5">{cycle.name}</h2>
|
||||||
|
<p className="flex gap-2 text-xs text-gray-500">
|
||||||
<span>
|
<span>
|
||||||
<ChevronDownIcon
|
{cycle.status === "started"
|
||||||
width={22}
|
? cycle.start_date
|
||||||
className={`text-gray-500 ${!open ? "transform -rotate-90" : ""}`}
|
? `${renderShortNumericDateFormat(cycle.start_date)} - `
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<h2 className="text-xl">{sprint.name}</h2>
|
|
||||||
<p className="font-light text-gray-500">
|
|
||||||
{sprint.status === "started"
|
|
||||||
? sprint.start_date
|
|
||||||
? `${renderShortNumericDateFormat(sprint.start_date)} - `
|
|
||||||
: ""
|
: ""
|
||||||
: sprint.status}
|
: cycle.status}
|
||||||
{sprint.end_date ? renderShortNumericDateFormat(sprint.end_date) : ""}
|
</span>
|
||||||
</p>
|
<span>
|
||||||
</div>
|
{cycle.end_date ? renderShortNumericDateFormat(cycle.end_date) : ""}
|
||||||
</Disclosure.Button>
|
</span>
|
||||||
|
</p>
|
||||||
<div className="relative">
|
|
||||||
<Menu>
|
|
||||||
<Menu.Button>
|
|
||||||
<EllipsisHorizontalIcon width="16" height="16" />
|
|
||||||
</Menu.Button>
|
|
||||||
<Menu.Items className="absolute z-20 w-28 bg-white rounded border cursor-pointer -left-24">
|
|
||||||
<Menu.Item>
|
|
||||||
<div className="hover:bg-gray-100 border-b last:border-0">
|
|
||||||
<button
|
|
||||||
className="w-full text-left py-2 pl-2"
|
|
||||||
type="button"
|
|
||||||
onClick={() => selectSprint({ ...sprint, actionType: "edit" })}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item>
|
|
||||||
<div className="hover:bg-gray-100 border-b last:border-0">
|
|
||||||
<button
|
|
||||||
className="w-full text-left py-2 pl-2"
|
|
||||||
type="button"
|
|
||||||
onClick={() => selectSprint({ ...sprint, actionType: "delete" })}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Items>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Disclosure.Button>
|
||||||
|
|
||||||
|
<Menu as="div" className="relative inline-block">
|
||||||
|
<Menu.Button className="grid place-items-center rounded p-1 hover:bg-gray-100 focus:outline-none">
|
||||||
|
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||||
|
</Menu.Button>
|
||||||
|
<Menu.Items className="absolute origin-top-right right-0 mt-1 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
||||||
|
<Menu.Item>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||||
|
onClick={() => selectSprint({ ...cycle, actionType: "edit" })}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||||
|
onClick={() => selectSprint({ ...cycle, actionType: "delete" })}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Items>
|
||||||
|
</Menu>
|
||||||
|
</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>
|
||||||
|
<StrictModeDroppable droppableId={cycle.id}>
|
||||||
|
{(provided) => (
|
||||||
|
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||||
|
{cycleIssues ? (
|
||||||
|
cycleIssues.length > 0 ? (
|
||||||
|
cycleIssues.map((issue, index) => (
|
||||||
|
<Draggable
|
||||||
|
key={issue.id}
|
||||||
|
draggableId={`${issue.id},${issue.issue}`} // bridge id, issue id
|
||||||
|
index={index}
|
||||||
|
>
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<div
|
||||||
|
className={`group p-2 hover:bg-gray-100 text-sm rounded flex items-center justify-between ${
|
||||||
|
snapshot.isDragging
|
||||||
|
? "bg-gray-100 shadow-lg border border-theme"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 rotate-90 outline-none`}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
>
|
||||||
|
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600" />
|
||||||
|
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600 mt-[-0.7rem]" />
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
className={`h-1.5 w-1.5 block rounded-full`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: issue.issue_details.state_detail.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
href={`/projects/${projectId}/issues/${issue.issue_details.id}`}
|
||||||
|
>
|
||||||
|
<a className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{activeProject?.identifier}-
|
||||||
|
{issue.issue_details.sequence_id}
|
||||||
|
</span>
|
||||||
|
{issue.issue_details.name}
|
||||||
|
{/* {cycle.id} */}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className="text-black rounded-md px-2 py-0.5 text-sm"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${issue.issue_details.state_detail?.color}20`,
|
||||||
|
border: `2px solid ${issue.issue_details.state_detail?.color}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{issue.issue_details.state_detail?.name}
|
||||||
|
</span>
|
||||||
|
<Menu as="div" className="relative">
|
||||||
|
<Menu.Button
|
||||||
|
as="button"
|
||||||
|
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none`}
|
||||||
|
>
|
||||||
|
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||||
|
</Menu.Button>
|
||||||
|
<Menu.Items className="absolute origin-top-right right-0.5 mt-1 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
||||||
|
<Menu.Item>
|
||||||
|
<button
|
||||||
|
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
openIssueModal(cycle.id, issue.issue_details, "edit")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<div className="hover:bg-gray-100 border-b last:border-0">
|
||||||
|
<button
|
||||||
|
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
removeIssueFromCycle(issue.cycle, issue.id)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Remove from cycle
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<div className="hover:bg-gray-100 border-b last:border-0">
|
||||||
|
<button
|
||||||
|
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
openIssueModal(
|
||||||
|
cycle.id,
|
||||||
|
issue.issue_details,
|
||||||
|
"delete"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Delete permanently
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Items>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-500">This cycle has no issue.</p>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{provided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</StrictModeDroppable>
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</Transition>
|
||||||
|
<Menu as="div" className="relative inline-block">
|
||||||
|
<Menu.Button className="flex items-center gap-1 px-2 py-1 rounded hover:bg-gray-100 text-xs font-medium">
|
||||||
|
<PlusIcon className="h-3 w-3" />
|
||||||
|
Add issue
|
||||||
|
</Menu.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
show={open}
|
as={React.Fragment}
|
||||||
enter="transition duration-100 ease-out"
|
enter="transition ease-out duration-100"
|
||||||
enterFrom="transform opacity-0"
|
enterFrom="transform opacity-0 scale-95"
|
||||||
enterTo="transform opacity-100"
|
enterTo="transform opacity-100 scale-100"
|
||||||
leave="transition duration-75 ease-out"
|
leave="transition ease-in duration-75"
|
||||||
leaveFrom="transform opacity-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Disclosure.Panel>
|
<Menu.Items className="absolute left-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
||||||
<div className="space-y-3">
|
<div className="p-1">
|
||||||
{sprintIssues ? (
|
<Menu.Item as="div">
|
||||||
sprintIssues.length > 0 ? (
|
{(active) => (
|
||||||
sprintIssues.map((issue) => (
|
<button
|
||||||
<div
|
type="button"
|
||||||
key={issue.id}
|
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||||
className="p-4 bg-white border border-gray-200 rounded flex items-center justify-between"
|
onClick={() => openIssueModal(cycle.id)}
|
||||||
>
|
>
|
||||||
<button
|
Create new
|
||||||
type="button"
|
</button>
|
||||||
onClick={() =>
|
)}
|
||||||
router.push(
|
</Menu.Item>
|
||||||
`/projects/${projectId}/issues/${issue.issue_details.id}`
|
<Menu.Item as="div">
|
||||||
)
|
{(active) => (
|
||||||
}
|
<button
|
||||||
>
|
type="button"
|
||||||
<p>{issue.issue_details.name}</p>
|
className="p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap"
|
||||||
</button>
|
onClick={() => setCycleIssuesListModal(true)}
|
||||||
<div className="flex items-center gap-x-4">
|
>
|
||||||
<span
|
Add an existing issue
|
||||||
className="text-black rounded px-2 py-0.5 text-sm border"
|
</button>
|
||||||
style={{
|
)}
|
||||||
backgroundColor: `${issue.issue_details.state_detail?.color}20`,
|
</Menu.Item>
|
||||||
borderColor: issue.issue_details.state_detail?.color,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{issue.issue_details.state_detail?.name}
|
|
||||||
</span>
|
|
||||||
<div className="relative">
|
|
||||||
<Menu>
|
|
||||||
<Menu.Button>
|
|
||||||
<EllipsisHorizontalIcon width="16" height="16" />
|
|
||||||
</Menu.Button>
|
|
||||||
<Menu.Items className="absolute z-20 w-28 bg-white rounded border cursor-pointer -left-24">
|
|
||||||
<Menu.Item>
|
|
||||||
<div className="hover:bg-gray-100 border-b last:border-0">
|
|
||||||
<button
|
|
||||||
className="w-full text-left py-2 pl-2"
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
openIssueModal(sprint.id, issue.issue_details, "edit")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item>
|
|
||||||
<div className="hover:bg-gray-100 border-b last:border-0">
|
|
||||||
<button
|
|
||||||
className="w-full text-left py-2 pl-2"
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
openIssueModal(sprint.id, issue.issue_details, "delete")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Items>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-gray-500">This cycle has no issues.</p>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div className="w-full h-full flex items-center justify-center">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Disclosure.Panel>
|
</Menu.Items>
|
||||||
</Transition>
|
</Transition>
|
||||||
<div className="flex flex-col gap-y-2">
|
</Menu>
|
||||||
<button
|
|
||||||
className="text-indigo-600 flex items-center gap-x-2"
|
|
||||||
onClick={() => openIssueModal(sprint.id)}
|
|
||||||
>
|
|
||||||
<div className="bg-theme text-white rounded-full p-0.5">
|
|
||||||
<PlusIcon width="18" height="18" />
|
|
||||||
</div>
|
|
||||||
<p>Add Issue</p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="ml-1">
|
|
||||||
<Menu as="div" className="inline-block text-left">
|
|
||||||
<div>
|
|
||||||
<Menu.Button className="inline-flex w-full items-center justify-center rounded-md text-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-gray-100">
|
|
||||||
<div className="text-indigo-600 flex items-center gap-x-2">
|
|
||||||
<p>Add Existing Issue</p>
|
|
||||||
</div>
|
|
||||||
<ChevronDownIcon
|
|
||||||
className="-mr-1 ml-2 h-5 w-5 text-indigo-600"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</Menu.Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
as={React.Fragment}
|
|
||||||
enter="transition ease-out duration-100"
|
|
||||||
enterFrom="transform opacity-0 scale-95"
|
|
||||||
enterTo="transform opacity-100 scale-100"
|
|
||||||
leave="transition ease-in duration-75"
|
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
|
||||||
leaveTo="transform opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<Menu.Items className="absolute left-5 z-20 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
|
||||||
<div className="py-1">
|
|
||||||
{projectIssues?.results.map((issue) => (
|
|
||||||
<Menu.Item
|
|
||||||
key={issue.id}
|
|
||||||
as="div"
|
|
||||||
onClick={() => {
|
|
||||||
addIssueToSprint(sprint.id, issue.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ active }) => (
|
|
||||||
<p
|
|
||||||
className={classNames(
|
|
||||||
active ? "bg-gray-100 text-gray-900" : "text-gray-700",
|
|
||||||
"block px-4 py-2 text-sm"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{issue.name}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Menu.Items>
|
|
||||||
</Transition>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Disclosure>
|
</Disclosure>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SprintView;
|
export default CycleView;
|
||||||
|
@ -75,7 +75,7 @@ const SingleBoard: React.FC<Props> = ({
|
|||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<div
|
<div
|
||||||
className={`rounded flex-shrink-0 h-full ${
|
className={`rounded flex-shrink-0 h-full ${
|
||||||
snapshot.isDragging ? "border-indigo-600 shadow-lg" : ""
|
snapshot.isDragging ? "border-theme shadow-lg" : ""
|
||||||
} ${!show ? "" : "w-80 bg-gray-50 border"}`}
|
} ${!show ? "" : "w-80 bg-gray-50 border"}`}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
@ -196,7 +196,7 @@ const SingleBoard: React.FC<Props> = ({
|
|||||||
className="px-2 py-3 space-y-1.5 select-none"
|
className="px-2 py-3 space-y-1.5 select-none"
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
>
|
>
|
||||||
<span className="group-hover:text-theme break-all">
|
<span className="group-hover:text-theme text-sm break-all">
|
||||||
{childIssue.name}
|
{childIssue.name}
|
||||||
</span>
|
</span>
|
||||||
{Object.keys(properties).map(
|
{Object.keys(properties).map(
|
||||||
@ -277,6 +277,7 @@ const SingleBoard: React.FC<Props> = ({
|
|||||||
<>{addSpaceIfCamelCase(childIssue["state_detail"].name)}</>
|
<>{addSpaceIfCamelCase(childIssue["state_detail"].name)}</>
|
||||||
)}
|
)}
|
||||||
{key === "priority" && <>{childIssue.priority}</>}
|
{key === "priority" && <>{childIssue.priority}</>}
|
||||||
|
{/* {key === "description" && <>{childIssue.description}</>} */}
|
||||||
{key === "assignee" ? (
|
{key === "assignee" ? (
|
||||||
<div className="flex items-center gap-1 text-xs">
|
<div className="flex items-center gap-1 text-xs">
|
||||||
{childIssue?.assignee_details?.length > 0 ? (
|
{childIssue?.assignee_details?.length > 0 ? (
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
// react hook form
|
// react hook form
|
||||||
import { Controller, Control } from "react-hook-form";
|
import { Controller, Control } from "react-hook-form";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
// types
|
// types
|
||||||
import type { IIssue } from "types";
|
import type { IIssue, IssueResponse } from "types";
|
||||||
// icons
|
// icons
|
||||||
import { UserIcon } from "@heroicons/react/24/outline";
|
import { UserIcon } from "@heroicons/react/24/outline";
|
||||||
// components
|
// components
|
||||||
@ -12,33 +12,23 @@ import IssuesListModal from "components/project/issues/IssuesListModal";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
control: Control<IIssue, any>;
|
control: Control<IIssue, any>;
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
issues: IssueResponse | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SelectParent: React.FC<Props> = ({ control }) => {
|
const SelectParent: React.FC<Props> = ({ control, isOpen, setIsOpen, issues }) => {
|
||||||
const [isIssueListModalOpen, setIsIssueListModalOpen] = useState(false);
|
|
||||||
|
|
||||||
const { issues } = useUser();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="parent"
|
name="parent"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<>
|
<IssuesListModal
|
||||||
<IssuesListModal
|
isOpen={isOpen}
|
||||||
isOpen={isIssueListModalOpen}
|
handleClose={() => setIsOpen(false)}
|
||||||
handleClose={() => setIsIssueListModalOpen(false)}
|
onChange={onChange}
|
||||||
onChange={onChange}
|
issues={issues}
|
||||||
issues={issues}
|
/>
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap"
|
|
||||||
onClick={() => setIsIssueListModalOpen(true)}
|
|
||||||
>
|
|
||||||
{value ? issues?.results.find((i) => i.id === value)?.name : "Select Parent Issue"}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -21,38 +21,36 @@ const SelectState: React.FC<Props> = ({ control, setIsOpen }) => {
|
|||||||
const { states } = useUser();
|
const { states } = useUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Controller
|
||||||
<Controller
|
control={control}
|
||||||
control={control}
|
name="state"
|
||||||
name="state"
|
render={({ field: { value, onChange } }) => (
|
||||||
render={({ field: { value, onChange } }) => (
|
<CustomListbox
|
||||||
<CustomListbox
|
title="State"
|
||||||
title="State"
|
options={states?.map((state) => {
|
||||||
options={states?.map((state) => {
|
return { value: state.id, display: state.name };
|
||||||
return { value: state.id, display: state.name };
|
})}
|
||||||
})}
|
value={value}
|
||||||
value={value}
|
optionsFontsize="sm"
|
||||||
optionsFontsize="sm"
|
onChange={onChange}
|
||||||
onChange={onChange}
|
icon={<Squares2X2Icon className="h-4 w-4 text-gray-400" />}
|
||||||
icon={<Squares2X2Icon className="h-4 w-4 text-gray-400" />}
|
footerOption={
|
||||||
footerOption={
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className="select-none relative py-2 pl-3 pr-9 flex items-center gap-x-2 text-gray-400 hover:text-gray-500"
|
||||||
className="select-none relative py-2 pl-3 pr-9 flex items-center gap-x-2 text-gray-400 hover:text-gray-500"
|
onClick={() => setIsOpen(true)}
|
||||||
onClick={() => setIsOpen(true)}
|
>
|
||||||
>
|
<span>
|
||||||
<span>
|
<PlusIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||||
<PlusIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
</span>
|
||||||
</span>
|
<span>
|
||||||
<span>
|
<span className="block truncate">Create state</span>
|
||||||
<span className="block truncate">Create state</span>
|
</span>
|
||||||
</span>
|
</button>
|
||||||
</button>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
/>
|
||||||
></Controller>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,14 +14,14 @@ import {
|
|||||||
USER_ISSUE,
|
USER_ISSUE,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
// headless
|
// headless
|
||||||
import { Dialog, Menu, Popover, Transition } from "@headlessui/react";
|
import { Dialog, Menu, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import issuesServices from "lib/services/issues.services";
|
import issuesServices from "lib/services/issues.services";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
import useToast from "lib/hooks/useToast";
|
import useToast from "lib/hooks/useToast";
|
||||||
// ui
|
// ui
|
||||||
import { Button, CustomListbox, Input, TextArea } from "ui";
|
import { Button, Input, TextArea } from "ui";
|
||||||
// commons
|
// commons
|
||||||
import { renderDateFormat, cosineSimilarity } from "constants/common";
|
import { renderDateFormat, cosineSimilarity } from "constants/common";
|
||||||
// components
|
// components
|
||||||
@ -36,7 +36,7 @@ import CreateUpdateStateModal from "components/project/issues/BoardView/state/Cr
|
|||||||
import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal";
|
import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import type { IIssue, IssueResponse, SprintIssueResponse } from "types";
|
import type { IIssue, IssueResponse, CycleIssueResponse } from "types";
|
||||||
import { EllipsisHorizontalIcon } from "@heroicons/react/24/outline";
|
import { EllipsisHorizontalIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -53,8 +53,8 @@ const defaultValues: Partial<IIssue> = {
|
|||||||
name: "",
|
name: "",
|
||||||
// description: "",
|
// description: "",
|
||||||
state: "",
|
state: "",
|
||||||
sprints: "",
|
sprints: null,
|
||||||
priority: "",
|
priority: null,
|
||||||
labels_list: [],
|
labels_list: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -68,6 +68,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [isCycleModalOpen, setIsCycleModalOpen] = useState(false);
|
const [isCycleModalOpen, setIsCycleModalOpen] = useState(false);
|
||||||
const [isStateModalOpen, setIsStateModalOpen] = useState(false);
|
const [isStateModalOpen, setIsStateModalOpen] = useState(false);
|
||||||
|
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
|
||||||
|
|
||||||
const [mostSimilarIssue, setMostSimilarIssue] = useState<string | undefined>();
|
const [mostSimilarIssue, setMostSimilarIssue] = useState<string | undefined>();
|
||||||
|
|
||||||
@ -90,13 +91,6 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
reset(defaultValues);
|
|
||||||
clearTimeout(timeout);
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { activeWorkspace, activeProject, user, issues } = useUser();
|
const { activeWorkspace, activeProject, user, issues } = useUser();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -109,11 +103,17 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
setError,
|
setError,
|
||||||
control,
|
control,
|
||||||
watch,
|
watch,
|
||||||
setValue,
|
|
||||||
} = useForm<IIssue>({
|
} = useForm<IIssue>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
reset(defaultValues);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
const addIssueToSprint = async (issueId: string, sprintId: string, issueDetail: IIssue) => {
|
const addIssueToSprint = async (issueId: string, sprintId: string, issueDetail: IIssue) => {
|
||||||
if (!activeWorkspace || !activeProject) return;
|
if (!activeWorkspace || !activeProject) return;
|
||||||
await issuesServices
|
await issuesServices
|
||||||
@ -121,8 +121,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
issue: issueId,
|
issue: issueId,
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
console.log("add to sprint", res);
|
mutate<CycleIssueResponse[]>(
|
||||||
mutate<SprintIssueResponse[]>(
|
|
||||||
CYCLE_ISSUES(sprintId),
|
CYCLE_ISSUES(sprintId),
|
||||||
(prevData) => {
|
(prevData) => {
|
||||||
const targetResponse = prevData?.find((t) => t.cycle === sprintId);
|
const targetResponse = prevData?.find((t) => t.cycle === sprintId);
|
||||||
@ -135,7 +134,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
{
|
{
|
||||||
cycle: sprintId,
|
cycle: sprintId,
|
||||||
issue_details: issueDetail,
|
issue_details: issueDetail,
|
||||||
} as SprintIssueResponse,
|
} as CycleIssueResponse,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -183,17 +182,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
.createIssues(activeWorkspace.slug, activeProject.id, payload)
|
.createIssues(activeWorkspace.slug, activeProject.id, payload)
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
console.log(res);
|
console.log(res);
|
||||||
mutate<IssueResponse>(
|
mutate<IssueResponse>(PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id));
|
||||||
PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id),
|
|
||||||
(prevData) => {
|
|
||||||
return {
|
|
||||||
...(prevData as IssueResponse),
|
|
||||||
results: [res, ...(prevData?.results ?? [])],
|
|
||||||
count: (prevData?.count ?? 0) + 1,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
if (formData.sprints && formData.sprints !== null) {
|
if (formData.sprints && formData.sprints !== null) {
|
||||||
await addIssueToSprint(res.id, formData.sprints, formData);
|
await addIssueToSprint(res.id, formData.sprints, formData);
|
||||||
@ -206,13 +195,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
message: `Issue ${data ? "updated" : "created"} successfully`,
|
message: `Issue ${data ? "updated" : "created"} successfully`,
|
||||||
});
|
});
|
||||||
if (formData.assignees_list.some((assignee) => assignee === user?.id)) {
|
if (formData.assignees_list.some((assignee) => assignee === user?.id)) {
|
||||||
mutate<IIssue[]>(
|
mutate<IIssue[]>(USER_ISSUE);
|
||||||
USER_ISSUE,
|
|
||||||
(prevData) => {
|
|
||||||
return [res, ...(prevData ?? [])];
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -392,21 +375,21 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{/* <TextArea
|
<TextArea
|
||||||
id="description"
|
id="description"
|
||||||
name="description"
|
name="description"
|
||||||
label="Description"
|
label="Description"
|
||||||
placeholder="Enter description"
|
placeholder="Enter description"
|
||||||
error={errors.description}
|
error={errors.description}
|
||||||
register={register}
|
register={register}
|
||||||
/> */}
|
/>
|
||||||
<Controller
|
{/* <Controller
|
||||||
name="description"
|
name="description"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<RichTextEditor {...field} id="issueDescriptionEditor" />
|
<RichTextEditor {...field} id="issueDescriptionEditor" />
|
||||||
)}
|
)}
|
||||||
/>
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
@ -426,9 +409,15 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
<SelectPriority control={control} />
|
<SelectPriority control={control} />
|
||||||
<SelectAssignee control={control} />
|
<SelectAssignee control={control} />
|
||||||
<SelectLabels control={control} />
|
<SelectLabels control={control} />
|
||||||
|
<SelectParent
|
||||||
|
control={control}
|
||||||
|
isOpen={parentIssueListModalOpen}
|
||||||
|
setIsOpen={setParentIssueListModalOpen}
|
||||||
|
issues={issues}
|
||||||
|
/>
|
||||||
<Menu as="div" className="relative inline-block">
|
<Menu as="div" className="relative inline-block">
|
||||||
<Menu.Button className="grid relative place-items-center rounded p-1 hover:bg-gray-100 focus:outline-none">
|
<Menu.Button className="grid place-items-center p-1 hover:bg-gray-100 border rounded-md shadow-sm cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm duration-300">
|
||||||
<EllipsisHorizontalIcon className="h-4 w-4" />
|
<EllipsisHorizontalIcon className="h-5 w-5" />
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
@ -443,7 +432,18 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
<Menu.Items className="origin-top-right absolute right-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
|
<Menu.Items className="origin-top-right absolute right-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
<Menu.Item as="div">
|
<Menu.Item as="div">
|
||||||
{(active) => <SelectParent control={control} />}
|
<button
|
||||||
|
type="button"
|
||||||
|
className="p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap "
|
||||||
|
onClick={() => setParentIssueListModalOpen(true)}
|
||||||
|
>
|
||||||
|
{watch("parent") && watch("parent") !== ""
|
||||||
|
? `${activeProject?.identifier}-${
|
||||||
|
issues?.results.find((i) => i.id === watch("parent"))
|
||||||
|
?.sequence_id
|
||||||
|
}`
|
||||||
|
: "Select Parent Issue"}
|
||||||
|
</button>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Items>
|
</Menu.Items>
|
||||||
|
@ -7,6 +7,7 @@ import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/out
|
|||||||
// types
|
// types
|
||||||
import { IIssue, IssueResponse } from "types";
|
import { IIssue, IssueResponse } from "types";
|
||||||
import { classNames } from "constants/common";
|
import { classNames } from "constants/common";
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -18,6 +19,8 @@ type Props = {
|
|||||||
const IssuesListModal: React.FC<Props> = ({ isOpen, handleClose: onClose, onChange, issues }) => {
|
const IssuesListModal: React.FC<Props> = ({ isOpen, handleClose: onClose, onChange, issues }) => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
|
const { activeProject } = useUser();
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
onClose();
|
onClose();
|
||||||
setQuery("");
|
setQuery("");
|
||||||
@ -102,6 +105,9 @@ const IssuesListModal: React.FC<Props> = ({ isOpen, handleClose: onClose, onChan
|
|||||||
backgroundColor: issue.state_detail.color,
|
backgroundColor: issue.state_detail.color,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{activeProject?.identifier}-{issue.sequence_id}
|
||||||
|
</span>{" "}
|
||||||
{issue.name}
|
{issue.name}
|
||||||
</Combobox.Option>
|
</Combobox.Option>
|
||||||
))}
|
))}
|
||||||
|
@ -185,7 +185,7 @@ const ListView: React.FC<Props> = ({
|
|||||||
id={`descriptionViewer-${issue.id}`}
|
id={`descriptionViewer-${issue.id}`}
|
||||||
value={issue.description}
|
value={issue.description}
|
||||||
/> */}
|
/> */}
|
||||||
{/* {issue.description} */}
|
{issue.description}
|
||||||
</td>
|
</td>
|
||||||
) : (key as keyof Properties) === "priority" ? (
|
) : (key as keyof Properties) === "priority" ? (
|
||||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
|
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
|
||||||
@ -218,7 +218,7 @@ const ListView: React.FC<Props> = ({
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
<Listbox.Options className="fixed z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||||
{PRIORITIES?.map((priority) => (
|
{PRIORITIES?.map((priority) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={priority}
|
key={priority}
|
||||||
@ -290,14 +290,14 @@ const ListView: React.FC<Props> = ({
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
<Listbox.Options className="fixed z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||||
{people?.map((person) => (
|
{people?.map((person) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={person.id}
|
key={person.id}
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
classNames(
|
classNames(
|
||||||
active ? "bg-indigo-50" : "bg-white",
|
active ? "bg-indigo-50" : "bg-white",
|
||||||
"cursor-pointer select-none px-3 py-2"
|
"cursor-pointer select-none p-2"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
value={person.member.id}
|
value={person.member.id}
|
||||||
@ -305,15 +305,15 @@ const ListView: React.FC<Props> = ({
|
|||||||
<div
|
<div
|
||||||
className={`flex items-center gap-x-1 ${
|
className={`flex items-center gap-x-1 ${
|
||||||
assignees.includes(
|
assignees.includes(
|
||||||
person.member.first_name
|
person.member.email
|
||||||
)
|
)
|
||||||
? "font-medium"
|
? "font-medium"
|
||||||
: "font-normal"
|
: "text-gray-500"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{person.member.avatar &&
|
{person.member.avatar &&
|
||||||
person.member.avatar !== "" ? (
|
person.member.avatar !== "" ? (
|
||||||
<div className="relative w-4 h-4">
|
<div className="relative h-4 w-4">
|
||||||
<Image
|
<Image
|
||||||
src={person.member.avatar}
|
src={person.member.avatar}
|
||||||
alt="avatar"
|
alt="avatar"
|
||||||
@ -323,11 +323,11 @@ const ListView: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p>
|
<span className="h-4 w-4 grid place-items-center bg-gray-700 text-white rounded-full">
|
||||||
{person.member.first_name.charAt(0)}
|
{person.member.first_name.charAt(0)}
|
||||||
</p>
|
</span>
|
||||||
)}
|
)}
|
||||||
<p>{person.member.first_name}</p>
|
{person.member.first_name}
|
||||||
</div>
|
</div>
|
||||||
</Listbox.Option>
|
</Listbox.Option>
|
||||||
))}
|
))}
|
||||||
@ -375,18 +375,24 @@ const ListView: React.FC<Props> = ({
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
<Listbox.Options className="fixed z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||||
{states?.map((state) => (
|
{states?.map((state) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={state.id}
|
key={state.id}
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
classNames(
|
classNames(
|
||||||
active ? "bg-indigo-50" : "bg-white",
|
active ? "bg-indigo-50" : "bg-white",
|
||||||
"cursor-pointer select-none px-3 py-2"
|
"flex items-center gap-2 cursor-pointer select-none p-2"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
value={state.id}
|
value={state.id}
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
|
className={`h-1.5 w-1.5 block rounded-full`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: state.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{addSpaceIfCamelCase(state.name)}
|
{addSpaceIfCamelCase(state.name)}
|
||||||
</Listbox.Option>
|
</Listbox.Option>
|
||||||
))}
|
))}
|
||||||
|
@ -106,7 +106,6 @@ const IssueActivitySection: React.FC<Props> = ({ issueActivities, states, issues
|
|||||||
: activity.old_value ?? "None"}
|
: activity.old_value ?? "None"}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{console.log(activity)}
|
|
||||||
<span className="text-gray-500">To: </span>
|
<span className="text-gray-500">To: </span>
|
||||||
{activity.field === "state"
|
{activity.field === "state"
|
||||||
? activity.new_value
|
? activity.new_value
|
||||||
|
@ -73,7 +73,7 @@ const ChangeStateDropdown: React.FC<Props> = ({ issue, updateIssues }) => {
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
<Listbox.Options className="fixed z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||||
{states?.map((state) => (
|
{states?.map((state) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={state.id}
|
key={state.id}
|
||||||
|
197
apps/app/components/project/settings/ControlSettings.tsx
Normal file
197
apps/app/components/project/settings/ControlSettings.tsx
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
// react
|
||||||
|
import React from "react";
|
||||||
|
// swr
|
||||||
|
import useSWR from "swr";
|
||||||
|
// react-hook-form
|
||||||
|
import { Control, Controller } from "react-hook-form";
|
||||||
|
// services
|
||||||
|
import workspaceService from "lib/services/workspace.service";
|
||||||
|
// hooks
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
// headless ui
|
||||||
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
|
// ui
|
||||||
|
import { Button } from "ui";
|
||||||
|
// icons
|
||||||
|
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import { IProject, WorkspaceMember } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
control: Control<IProject, any>;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ControlSettings: React.FC<Props> = ({ control, isSubmitting }) => {
|
||||||
|
const { activeWorkspace } = useUser();
|
||||||
|
|
||||||
|
const { data: people } = useSWR<WorkspaceMember[]>(
|
||||||
|
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
||||||
|
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="space-y-5">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium leading-6 text-gray-900">Control</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">Set the control for the project.</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between gap-3">
|
||||||
|
<div className="w-full md:w-1/2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="project_lead"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Listbox value={value} onChange={onChange}>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Listbox.Label>
|
||||||
|
<div className="text-gray-500 mb-2">Project Lead</div>
|
||||||
|
</Listbox.Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Listbox.Button className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||||
|
<span className="block truncate">
|
||||||
|
{people?.find((person) => person.member.id === value)?.member
|
||||||
|
.first_name ?? "Select Lead"}
|
||||||
|
</span>
|
||||||
|
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
|
<ChevronDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</Listbox.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
as={React.Fragment}
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Listbox.Options className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
||||||
|
{people?.map((person) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={person.id}
|
||||||
|
className={({ active }) =>
|
||||||
|
`${
|
||||||
|
active ? "text-white bg-theme" : "text-gray-900"
|
||||||
|
} cursor-default select-none relative py-2 pl-3 pr-9`
|
||||||
|
}
|
||||||
|
value={person.member.id}
|
||||||
|
>
|
||||||
|
{({ selected, active }) => (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
selected ? "font-semibold" : "font-normal"
|
||||||
|
} block truncate`}
|
||||||
|
>
|
||||||
|
{person.member.first_name}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{selected ? (
|
||||||
|
<span
|
||||||
|
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
|
||||||
|
active ? "text-white" : "text-indigo-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-full md:w-1/2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="default_assignee"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<Listbox value={value} onChange={onChange}>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Listbox.Label>
|
||||||
|
<div className="text-gray-500 mb-2">Default Assignee</div>
|
||||||
|
</Listbox.Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Listbox.Button className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||||
|
<span className="block truncate">
|
||||||
|
{people?.find((p) => p.member.id === value)?.member.first_name ??
|
||||||
|
"Select Default Assignee"}
|
||||||
|
</span>
|
||||||
|
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
|
<ChevronDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</Listbox.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
as={React.Fragment}
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Listbox.Options className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
||||||
|
{people?.map((person) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={person.id}
|
||||||
|
className={({ active }) =>
|
||||||
|
`${
|
||||||
|
active ? "text-white bg-theme" : "text-gray-900"
|
||||||
|
} cursor-default select-none relative py-2 pl-3 pr-9`
|
||||||
|
}
|
||||||
|
value={person.member.id}
|
||||||
|
>
|
||||||
|
{({ selected, active }) => (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
selected ? "font-semibold" : "font-normal"
|
||||||
|
} block truncate`}
|
||||||
|
>
|
||||||
|
{person.member.first_name}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{selected ? (
|
||||||
|
<span
|
||||||
|
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
|
||||||
|
active ? "text-white" : "text-indigo-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? "Updating Project..." : "Update Project"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ControlSettings;
|
119
apps/app/components/project/settings/GeneralSettings.tsx
Normal file
119
apps/app/components/project/settings/GeneralSettings.tsx
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
// react
|
||||||
|
import { useCallback } from "react";
|
||||||
|
// react-hook-form
|
||||||
|
import { UseFormRegister, UseFormSetError } from "react-hook-form";
|
||||||
|
// services
|
||||||
|
import projectServices from "lib/services/project.service";
|
||||||
|
// hooks
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
// ui
|
||||||
|
import { Input, Select, TextArea } from "ui";
|
||||||
|
// types
|
||||||
|
import { IProject } from "types";
|
||||||
|
// constants
|
||||||
|
import { debounce } from "constants/common";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
register: UseFormRegister<IProject>;
|
||||||
|
errors: any;
|
||||||
|
setError: UseFormSetError<IProject>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
||||||
|
|
||||||
|
const GeneralSettings: React.FC<Props> = ({ register, errors, setError }) => {
|
||||||
|
const { activeWorkspace } = useUser();
|
||||||
|
|
||||||
|
const checkIdentifier = (slug: string, value: string) => {
|
||||||
|
projectServices.checkProjectIdentifierAvailability(slug, value).then((response) => {
|
||||||
|
console.log(response);
|
||||||
|
if (response.exists) setError("identifier", { message: "Identifier already exists" });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
const checkIdentifierAvailability = useCallback(debounce(checkIdentifier, 1500), []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="space-y-5">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium leading-6 text-gray-900">General</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
|
This information will be displayed to every member of the project.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 gap-3">
|
||||||
|
<div className="col-span-2">
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
error={errors.name}
|
||||||
|
register={register}
|
||||||
|
placeholder="Project Name"
|
||||||
|
label="Name"
|
||||||
|
validations={{
|
||||||
|
required: "Name is required",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
name="network"
|
||||||
|
id="network"
|
||||||
|
options={Object.keys(NETWORK_CHOICES).map((key) => ({
|
||||||
|
value: key,
|
||||||
|
label: NETWORK_CHOICES[key as keyof typeof NETWORK_CHOICES],
|
||||||
|
}))}
|
||||||
|
label="Network"
|
||||||
|
register={register}
|
||||||
|
validations={{
|
||||||
|
required: "Network is required",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
id="identifier"
|
||||||
|
name="identifier"
|
||||||
|
error={errors.identifier}
|
||||||
|
register={register}
|
||||||
|
placeholder="Enter identifier"
|
||||||
|
label="Identifier"
|
||||||
|
onChange={(e: any) => {
|
||||||
|
if (!activeWorkspace || !e.target.value) return;
|
||||||
|
checkIdentifierAvailability(activeWorkspace.slug, e.target.value);
|
||||||
|
}}
|
||||||
|
validations={{
|
||||||
|
required: "Identifier is required",
|
||||||
|
minLength: {
|
||||||
|
value: 1,
|
||||||
|
message: "Identifier must at least be of 1 character",
|
||||||
|
},
|
||||||
|
maxLength: {
|
||||||
|
value: 9,
|
||||||
|
message: "Identifier must at most be of 9 characters",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TextArea
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
error={errors.description}
|
||||||
|
register={register}
|
||||||
|
label="Description"
|
||||||
|
placeholder="Enter project description"
|
||||||
|
validations={{
|
||||||
|
required: "Description is required",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GeneralSettings;
|
275
apps/app/components/project/settings/LabelsSettings.tsx
Normal file
275
apps/app/components/project/settings/LabelsSettings.tsx
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
// react
|
||||||
|
import React, { useState } from "react";
|
||||||
|
// swr
|
||||||
|
import useSWR from "swr";
|
||||||
|
// react-hook-form
|
||||||
|
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
// react-color
|
||||||
|
import { TwitterPicker } from "react-color";
|
||||||
|
// services
|
||||||
|
import issuesServices from "lib/services/issues.services";
|
||||||
|
// hooks
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
// headless ui
|
||||||
|
import { Popover, Transition, Menu } from "@headlessui/react";
|
||||||
|
// ui
|
||||||
|
import { Button, Input, Spinner } from "ui";
|
||||||
|
// icons
|
||||||
|
import {
|
||||||
|
ChevronDownIcon,
|
||||||
|
EllipsisHorizontalIcon,
|
||||||
|
PencilIcon,
|
||||||
|
PlusIcon,
|
||||||
|
RectangleGroupIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import { IIssueLabels } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
const defaultValues: Partial<IIssueLabels> = {
|
||||||
|
name: "",
|
||||||
|
colour: "#ff0000",
|
||||||
|
};
|
||||||
|
|
||||||
|
const LabelsSettings: React.FC = () => {
|
||||||
|
const [newLabelForm, setNewLabelForm] = useState(false);
|
||||||
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
const [labelIdForUpdate, setLabelidForUpdate] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { activeWorkspace, activeProject } = useUser();
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
control,
|
||||||
|
setValue,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
watch,
|
||||||
|
} = useForm<IIssueLabels>({ defaultValues });
|
||||||
|
|
||||||
|
const { data: issueLabels, mutate } = useSWR<IIssueLabels[]>(
|
||||||
|
activeProject && activeWorkspace ? PROJECT_ISSUE_LABELS(activeProject.id) : null,
|
||||||
|
activeProject && activeWorkspace
|
||||||
|
? () => issuesServices.getIssueLabels(activeWorkspace.slug, activeProject.id)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNewLabel: SubmitHandler<IIssueLabels> = (formData) => {
|
||||||
|
if (!activeWorkspace || !activeProject || isSubmitting) return;
|
||||||
|
issuesServices
|
||||||
|
.createIssueLabel(activeWorkspace.slug, activeProject.id, formData)
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
reset(defaultValues);
|
||||||
|
mutate((prevData) => [...(prevData ?? []), res], false);
|
||||||
|
setNewLabelForm(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLabelUpdate: SubmitHandler<IIssueLabels> = (formData) => {
|
||||||
|
if (!activeWorkspace || !activeProject || isSubmitting) return;
|
||||||
|
issuesServices
|
||||||
|
.patchIssueLabel(activeWorkspace.slug, activeProject.id, labelIdForUpdate ?? "", formData)
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
reset(defaultValues);
|
||||||
|
mutate(
|
||||||
|
(prevData) =>
|
||||||
|
prevData?.map((p) => (p.id === labelIdForUpdate ? { ...p, ...formData } : p)),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
setNewLabelForm(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLabelDelete = (labelId: string) => {
|
||||||
|
if (activeWorkspace && activeProject) {
|
||||||
|
mutate((prevData) => prevData?.filter((p) => p.id !== labelId), false);
|
||||||
|
issuesServices
|
||||||
|
.deleteIssueLabel(activeWorkspace.slug, activeProject.id, labelId)
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLabelChildren = (labelId: string) => {
|
||||||
|
return issueLabels?.filter((l) => l.parent === labelId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="space-y-5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium leading-6 text-gray-900">Labels</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">Manage the labels of this project.</p>
|
||||||
|
</div>
|
||||||
|
<Button className="flex items-center gap-x-1" onClick={() => setNewLabelForm(true)}>
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
New label
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-5">
|
||||||
|
<form
|
||||||
|
className={`bg-white px-4 py-2 flex items-center gap-2 ${newLabelForm ? "" : "hidden"}`}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Popover className="relative">
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Popover.Button
|
||||||
|
className={`bg-white flex items-center gap-1 rounded-md p-1 outline-none focus:ring-2 focus:ring-indigo-500`}
|
||||||
|
>
|
||||||
|
{watch("colour") && watch("colour") !== "" && (
|
||||||
|
<span
|
||||||
|
className="w-6 h-6 rounded"
|
||||||
|
style={{
|
||||||
|
backgroundColor: watch("colour") ?? "green",
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
)}
|
||||||
|
<ChevronDownIcon className="h-4 w-4" />
|
||||||
|
</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 z-20 transform left-0 mt-1 px-2 max-w-xs sm:px-0">
|
||||||
|
<Controller
|
||||||
|
name="colour"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<TwitterPicker
|
||||||
|
color={value}
|
||||||
|
onChange={(value) => onChange(value.hex)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Popover.Panel>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="labelName"
|
||||||
|
name="name"
|
||||||
|
register={register}
|
||||||
|
placeholder="Lable title"
|
||||||
|
/>
|
||||||
|
<Button type="button" theme="secondary" onClick={() => setNewLabelForm(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
{isUpdating ? (
|
||||||
|
<Button type="button" onClick={handleSubmit(handleLabelUpdate)}>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button type="button" onClick={handleSubmit(handleNewLabel)}>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
{issueLabels ? (
|
||||||
|
issueLabels.map((label) => {
|
||||||
|
const children = getLabelChildren(label.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={label.id}>
|
||||||
|
{children && children.length === 0 ? (
|
||||||
|
<div className="bg-white p-2 flex items-center justify-between text-gray-900 rounded-md">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={`h-1.5 w-1.5 rounded-full`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: label.colour,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p className="text-sm">{label.name}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Menu as="div" className="relative">
|
||||||
|
<Menu.Button
|
||||||
|
as="button"
|
||||||
|
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-100 duration-300 outline-none`}
|
||||||
|
>
|
||||||
|
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||||
|
</Menu.Button>
|
||||||
|
<Menu.Items className="absolute origin-top-right right-0.5 mt-1 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
||||||
|
<Menu.Item>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||||
|
onClick={() => {
|
||||||
|
setNewLabelForm(true);
|
||||||
|
setValue("colour", label.colour);
|
||||||
|
setValue("name", label.name);
|
||||||
|
setIsUpdating(true);
|
||||||
|
setLabelidForUpdate(label.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<div className="hover:bg-gray-100 border-b last:border-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||||
|
onClick={() => handleLabelDelete(label.id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Items>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-white p-4 text-gray-900 rounded-md">
|
||||||
|
<h3 className="font-medium leading-5 flex items-center gap-2">
|
||||||
|
<RectangleGroupIcon className="h-5 w-5" />
|
||||||
|
This is the label group title
|
||||||
|
</h3>
|
||||||
|
<div className="pl-5 mt-4">
|
||||||
|
<div className="group text-sm flex justify-between items-center p-2 hover:bg-gray-100 rounded">
|
||||||
|
<h5 className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 bg-red-600 rounded-full"></div>
|
||||||
|
This is the label title
|
||||||
|
</h5>
|
||||||
|
<button type="button" className="hidden group-hover:block">
|
||||||
|
<PencilIcon className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-center py-4">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LabelsSettings;
|
78
apps/app/components/project/settings/StatesSettings.tsx
Normal file
78
apps/app/components/project/settings/StatesSettings.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
// react
|
||||||
|
import { useState } from "react";
|
||||||
|
// hooks
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
// components
|
||||||
|
import CreateUpdateStateModal from "components/project/issues/BoardView/state/CreateUpdateStateModal";
|
||||||
|
// ui
|
||||||
|
import { Button } from "ui";
|
||||||
|
// icons
|
||||||
|
import { PencilSquareIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||||
|
// constants
|
||||||
|
import { addSpaceIfCamelCase } from "constants/common";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
projectId: string | string[] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatesSettings: React.FC<Props> = ({ projectId }) => {
|
||||||
|
const [isCreateStateModal, setIsCreateStateModal] = useState(false);
|
||||||
|
const [selectedState, setSelectedState] = useState<string | undefined>();
|
||||||
|
|
||||||
|
const { states } = useUser();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CreateUpdateStateModal
|
||||||
|
isOpen={isCreateStateModal || Boolean(selectedState)}
|
||||||
|
handleClose={() => {
|
||||||
|
setSelectedState(undefined);
|
||||||
|
setIsCreateStateModal(false);
|
||||||
|
}}
|
||||||
|
projectId={projectId as string}
|
||||||
|
data={selectedState ? states?.find((state) => state.id === selectedState) : undefined}
|
||||||
|
/>
|
||||||
|
<section className="space-y-5">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium leading-6 text-gray-900">State</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">Manage the state of this project.</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between gap-3">
|
||||||
|
<div className="w-full space-y-5">
|
||||||
|
{states?.map((state) => (
|
||||||
|
<div
|
||||||
|
key={state.id}
|
||||||
|
className="bg-white px-4 py-2 rounded flex justify-between items-center"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: state.color,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<h4>{addSpaceIfCamelCase(state.name)}</h4>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="button" onClick={() => setSelectedState(state.id)}>
|
||||||
|
<PencilSquareIcon className="h-5 w-5 text-gray-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-x-1"
|
||||||
|
onClick={() => setIsCreateStateModal(true)}
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
<span>Add State</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatesSettings;
|
@ -100,6 +100,8 @@ export const ISSUE_ACTIVITIES = (workspaceSlug: string, projectId: string, issue
|
|||||||
|
|
||||||
export const ISSUE_LABELS = (workspaceSlug: string, projectId: string) =>
|
export const ISSUE_LABELS = (workspaceSlug: string, projectId: string) =>
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`;
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`;
|
||||||
|
export const ISSUE_LABEL_DETAILS = (workspaceSlug: string, projectId: string, labelId: string) =>
|
||||||
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/${labelId}/`;
|
||||||
|
|
||||||
export const FILTER_STATE_ISSUES = (workspaceSlug: string, projectId: string, state: string) =>
|
export const FILTER_STATE_ISSUES = (workspaceSlug: string, projectId: string, state: string) =>
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/?state=${state}`;
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/?state=${state}`;
|
||||||
@ -122,3 +124,10 @@ export const CYCLES_ENDPOINT = (workspaceSlug: string, projectId: string) =>
|
|||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`;
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`;
|
||||||
export const CYCLE_DETAIL = (workspaceSlug: string, projectId: string, cycleId: string) =>
|
export const CYCLE_DETAIL = (workspaceSlug: string, projectId: string, cycleId: string) =>
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/`;
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/`;
|
||||||
|
export const REMOVE_ISSUE_FROM_CYCLE = (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
cycleId: string,
|
||||||
|
bridgeId: string
|
||||||
|
) =>
|
||||||
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/${bridgeId}/`;
|
||||||
|
@ -87,7 +87,7 @@ export const ToastContextProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
removeAlert(id);
|
removeAlert(id);
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
}, 5000);
|
}, 3000);
|
||||||
},
|
},
|
||||||
[removeAlert]
|
[removeAlert]
|
||||||
);
|
);
|
||||||
|
@ -211,7 +211,7 @@ const Sidebar: React.FC = () => {
|
|||||||
<div className="px-2">
|
<div className="px-2">
|
||||||
<div
|
<div
|
||||||
className={`relative ${
|
className={`relative ${
|
||||||
sidebarCollapse ? "flex" : "grid grid-cols-5 gap-1 items-center"
|
sidebarCollapse ? "flex" : "grid grid-cols-5 gap-2 items-center"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Menu as="div" className="col-span-4 inline-block text-left w-full">
|
<Menu as="div" className="col-span-4 inline-block text-left w-full">
|
||||||
@ -224,7 +224,7 @@ const Sidebar: React.FC = () => {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex gap-x-1 items-center">
|
<div className="flex gap-x-1 items-center">
|
||||||
<div className="h-5 w-5 p-4 flex items-center justify-center bg-gray-500 text-white rounded uppercase relative">
|
<div className="h-5 w-5 p-4 flex items-center justify-center bg-gray-700 text-white rounded uppercase relative">
|
||||||
{activeWorkspace?.logo && activeWorkspace.logo !== "" ? (
|
{activeWorkspace?.logo && activeWorkspace.logo !== "" ? (
|
||||||
<Image
|
<Image
|
||||||
src={activeWorkspace.logo}
|
src={activeWorkspace.logo}
|
||||||
@ -259,7 +259,7 @@ const Sidebar: React.FC = () => {
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="origin-top-left fixed max-w-[15rem] ml-2 left-0 mt-2 w-full rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
|
<Menu.Items className="origin-top-left fixed max-w-[15rem] ml-2 left-0 mt-2 w-full rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-20">
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
{workspaces ? (
|
{workspaces ? (
|
||||||
<>
|
<>
|
||||||
@ -319,16 +319,15 @@ const Sidebar: React.FC = () => {
|
|||||||
</Transition>
|
</Transition>
|
||||||
</Menu>
|
</Menu>
|
||||||
{!sidebarCollapse && (
|
{!sidebarCollapse && (
|
||||||
<Menu as="div" className="inline-block text-left w-full">
|
<Menu as="div" className="inline-block text-left flex-shrink-0 w-full">
|
||||||
<div className="h-10 w-10">
|
<div className="h-10 w-10">
|
||||||
<Menu.Button className="grid relative place-items-center h-full w-full rounded-md shadow-sm px-2 py-2 bg-white text-gray-700 hover:bg-gray-50 focus:outline-none">
|
<Menu.Button className="grid relative place-items-center h-full w-full rounded-md shadow-sm bg-white text-gray-700 hover:bg-gray-50 focus:outline-none">
|
||||||
{user?.avatar && user.avatar !== "" ? (
|
{user?.avatar && user.avatar !== "" ? (
|
||||||
<Image
|
<Image
|
||||||
src={user.avatar}
|
src={user.avatar}
|
||||||
alt="User Avatar"
|
alt="User Avatar"
|
||||||
layout="fill"
|
layout="fill"
|
||||||
objectFit="cover"
|
className="rounded-md"
|
||||||
className="rounded-full"
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<UserIcon className="h-5 w-5" />
|
<UserIcon className="h-5 w-5" />
|
||||||
@ -345,7 +344,7 @@ const Sidebar: React.FC = () => {
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="origin-top-right absolute left-0 mt-2 w-full rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
|
<Menu.Items className="origin-top-right absolute left-0 mt-2 w-full rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-20">
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
{userLinks.map((item) => (
|
{userLinks.map((item) => (
|
||||||
<Menu.Item key={item.name} as="div">
|
<Menu.Item key={item.name} as="div">
|
||||||
@ -462,7 +461,7 @@ const Sidebar: React.FC = () => {
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="origin-top-right absolute right-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
|
<Menu.Items className="origin-top-right absolute right-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-20">
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
<Menu.Item as="div">
|
<Menu.Item as="div">
|
||||||
{(active) => (
|
{(active) => (
|
||||||
@ -553,24 +552,29 @@ const Sidebar: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="px-2 py-2 bg-gray-50 w-full self-baseline flex items-center gap-x-2">
|
<div
|
||||||
<Tooltip content="Click to toggle sidebar" position="right">
|
className={`px-2 py-2 bg-gray-50 w-full self-baseline flex items-center ${
|
||||||
<button
|
sidebarCollapse ? "flex-col-reverse" : ""
|
||||||
type="button"
|
}`}
|
||||||
className={`flex items-center gap-3 px-2 py-2 text-xs font-medium rounded-md text-gray-500 hover:bg-gray-100 hover:text-gray-900 focus:bg-gray-100 focus:text-gray-900 outline-none ${
|
>
|
||||||
sidebarCollapse ? "justify-center w-full" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => toggleCollapsed()}
|
|
||||||
>
|
|
||||||
<ArrowLongLeftIcon
|
|
||||||
className={`h-4 w-4 text-gray-500 group-hover:text-gray-900 flex-shrink-0 duration-300 ${
|
|
||||||
sidebarCollapse ? "rotate-180" : ""
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
className={`flex items-center gap-3 px-2 py-2 text-xs font-medium rounded-md text-gray-500 hover:bg-gray-100 hover:text-gray-900 outline-none ${
|
||||||
|
sidebarCollapse ? "justify-center w-full" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => toggleCollapsed()}
|
||||||
|
>
|
||||||
|
<ArrowLongLeftIcon
|
||||||
|
className={`h-4 w-4 text-gray-500 group-hover:text-gray-900 flex-shrink-0 duration-300 ${
|
||||||
|
sidebarCollapse ? "rotate-180" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`flex items-center gap-3 px-2 py-2 text-xs font-medium rounded-md text-gray-500 hover:bg-gray-100 hover:text-gray-900 outline-none ${
|
||||||
|
sidebarCollapse ? "justify-center w-full" : ""
|
||||||
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const e = new KeyboardEvent("keydown", {
|
const e = new KeyboardEvent("keydown", {
|
||||||
ctrlKey: true,
|
ctrlKey: true,
|
||||||
|
@ -10,8 +10,8 @@ class ProjectCycleServices extends APIService {
|
|||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||||
}
|
}
|
||||||
|
|
||||||
async createCycle(workspace_slug: string, projectId: string, data: any): Promise<any> {
|
async createCycle(workspaceSlug: string, projectId: string, data: any): Promise<any> {
|
||||||
return this.post(CYCLES_ENDPOINT(workspace_slug, projectId), data)
|
return this.post(CYCLES_ENDPOINT(workspaceSlug, projectId), data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -20,8 +20,8 @@ class ProjectCycleServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCycles(workspace_slug: string, projectId: string): Promise<any> {
|
async getCycles(workspaceSlug: string, projectId: string): Promise<any> {
|
||||||
return this.get(CYCLES_ENDPOINT(workspace_slug, projectId))
|
return this.get(CYCLES_ENDPOINT(workspaceSlug, projectId))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -30,18 +30,8 @@ class ProjectCycleServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCycleIssues(workspace_slug: string, projectId: string, cycleId: string): Promise<any> {
|
async getCycleIssues(workspaceSlug: string, projectId: string, cycleId: string): Promise<any> {
|
||||||
return this.get(CYCLE_DETAIL(workspace_slug, projectId, cycleId))
|
return this.get(CYCLE_DETAIL(workspaceSlug, projectId, cycleId))
|
||||||
.then((response) => {
|
|
||||||
return response?.data;
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
throw error?.response?.data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCycle(workspace_slug: string, projectId: string, cycleId: string): Promise<any> {
|
|
||||||
return this.get(CYCLE_DETAIL(workspace_slug, projectId, cycleId))
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -51,13 +41,13 @@ class ProjectCycleServices extends APIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateCycle(
|
async updateCycle(
|
||||||
workspace_slug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
cycleId: string,
|
cycleId: string,
|
||||||
data: any
|
data: any
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.put(
|
return this.put(
|
||||||
CYCLE_DETAIL(workspace_slug, projectId, cycleId).replace("cycle-issues/", ""),
|
CYCLE_DETAIL(workspaceSlug, projectId, cycleId).replace("cycle-issues/", ""),
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -68,10 +58,8 @@ class ProjectCycleServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteCycle(workspace_slug: string, projectId: string, cycleId: string): Promise<any> {
|
async deleteCycle(workspaceSlug: string, projectId: string, cycleId: string): Promise<any> {
|
||||||
return this.delete(
|
return this.delete(CYCLE_DETAIL(workspaceSlug, projectId, cycleId).replace("cycle-issues/", ""))
|
||||||
CYCLE_DETAIL(workspace_slug, projectId, cycleId).replace("cycle-issues/", "")
|
|
||||||
)
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
|
@ -10,6 +10,8 @@ import {
|
|||||||
ISSUE_LABELS,
|
ISSUE_LABELS,
|
||||||
BULK_DELETE_ISSUES,
|
BULK_DELETE_ISSUES,
|
||||||
BULK_ADD_ISSUES_TO_CYCLE,
|
BULK_ADD_ISSUES_TO_CYCLE,
|
||||||
|
REMOVE_ISSUE_FROM_CYCLE,
|
||||||
|
ISSUE_LABEL_DETAILS,
|
||||||
} from "constants/api-routes";
|
} from "constants/api-routes";
|
||||||
// services
|
// services
|
||||||
import APIService from "lib/services/api.service";
|
import APIService from "lib/services/api.service";
|
||||||
@ -103,6 +105,21 @@ class ProjectIssuesServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeIssueFromCycle(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
cycleId: string,
|
||||||
|
bridgeId: string
|
||||||
|
) {
|
||||||
|
return this.delete(REMOVE_ISSUE_FROM_CYCLE(workspaceSlug, projectId, cycleId, bridgeId))
|
||||||
|
.then((response) => {
|
||||||
|
return response?.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async createIssueProperties(workspaceSlug: string, projectId: string, data: any): Promise<any> {
|
async createIssueProperties(workspaceSlug: string, projectId: string, data: any): Promise<any> {
|
||||||
return this.post(ISSUE_PROPERTIES_ENDPOINT(workspaceSlug, projectId), data)
|
return this.post(ISSUE_PROPERTIES_ENDPOINT(workspaceSlug, projectId), data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -198,6 +215,31 @@ class ProjectIssuesServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async patchIssueLabel(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
labelId: string,
|
||||||
|
data: any
|
||||||
|
): Promise<any> {
|
||||||
|
return this.patch(ISSUE_LABEL_DETAILS(workspaceSlug, projectId, labelId), data)
|
||||||
|
.then((response) => {
|
||||||
|
return response?.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteIssueLabel(workspaceSlug: string, projectId: string, labelId: string): Promise<any> {
|
||||||
|
return this.delete(ISSUE_LABEL_DETAILS(workspaceSlug, projectId, labelId))
|
||||||
|
.then((response) => {
|
||||||
|
return response?.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async updateIssue(
|
async updateIssue(
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
|
|
||||||
const RichTextEditor = dynamic(() => import("../components/lexical/editor"), {
|
|
||||||
ssr: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const LexicalViewer = dynamic(() => import("../components/lexical/viewer"), {
|
|
||||||
ssr: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const Home = () => {
|
|
||||||
const [value, setValue] = React.useState("");
|
|
||||||
const onChange: any = (value: any) => {
|
|
||||||
console.log(value);
|
|
||||||
setValue(value);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<RichTextEditor onChange={onChange} value={value} id="editor" />
|
|
||||||
<LexicalViewer id="institution_viewer" value={value} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Home;
|
|
@ -1,5 +1,5 @@
|
|||||||
// react
|
// react
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
// next
|
// next
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
// swr
|
// swr
|
||||||
@ -22,15 +22,18 @@ import issuesServices from "lib/services/issues.services";
|
|||||||
// components
|
// components
|
||||||
import ChangeStateDropdown from "components/project/issues/my-issues/ChangeStateDropdown";
|
import ChangeStateDropdown from "components/project/issues/my-issues/ChangeStateDropdown";
|
||||||
// icons
|
// icons
|
||||||
import { PlusIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
|
import { ChevronDownIcon, PlusIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
const MyIssues: NextPage = () => {
|
const MyIssues: NextPage = () => {
|
||||||
const { user } = useUser();
|
const [selectedWorkspace, setSelectedWorkspace] = useState<string | null>(null);
|
||||||
|
|
||||||
const { data: myIssues, mutate: mutateMyIssue } = useSWR<IIssue[]>(
|
const { user, workspaces } = useUser();
|
||||||
|
|
||||||
|
const { data: myIssues, mutate: mutateMyIssues } = useSWR<IIssue[]>(
|
||||||
user ? USER_ISSUE : null,
|
user ? USER_ISSUE : null,
|
||||||
user ? () => userService.userIssues() : null
|
user ? () => userService.userIssues() : null
|
||||||
);
|
);
|
||||||
@ -41,7 +44,7 @@ const MyIssues: NextPage = () => {
|
|||||||
issueId: string,
|
issueId: string,
|
||||||
issue: Partial<IIssue>
|
issue: Partial<IIssue>
|
||||||
) => {
|
) => {
|
||||||
mutateMyIssue((prevData) => {
|
mutateMyIssues((prevData) => {
|
||||||
return prevData?.map((prevIssue) => {
|
return prevData?.map((prevIssue) => {
|
||||||
if (prevIssue.id === issueId) {
|
if (prevIssue.id === issueId) {
|
||||||
return {
|
return {
|
||||||
@ -66,6 +69,10 @@ const MyIssues: NextPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleWorkspaceChange = (workspaceId: string | null) => {
|
||||||
|
setSelectedWorkspace(workspaceId);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
<div className="w-full h-full flex flex-col space-y-5">
|
<div className="w-full h-full flex flex-col space-y-5">
|
||||||
@ -79,6 +86,63 @@ const MyIssues: NextPage = () => {
|
|||||||
<div className="flex items-center justify-between cursor-pointer w-full">
|
<div className="flex items-center justify-between cursor-pointer w-full">
|
||||||
<h2 className="text-2xl font-medium">My Issues</h2>
|
<h2 className="text-2xl font-medium">My Issues</h2>
|
||||||
<div className="flex items-center gap-x-3">
|
<div className="flex items-center gap-x-3">
|
||||||
|
<Menu as="div" className="relative inline-block w-40">
|
||||||
|
<div className="w-full">
|
||||||
|
<Menu.Button className="inline-flex justify-between items-center w-full rounded-md shadow-sm p-2 border border-gray-300 text-xs font-semibold text-gray-700 hover:bg-gray-100 focus:outline-none">
|
||||||
|
<span className="flex gap-x-1 items-center">
|
||||||
|
{workspaces?.find((w) => w.id === selectedWorkspace)?.name ??
|
||||||
|
"All workspaces"}
|
||||||
|
</span>
|
||||||
|
<div className="flex-grow flex justify-end">
|
||||||
|
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
</Menu.Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items className="origin-top-left absolute left-0 mt-2 w-full rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
|
||||||
|
<div className="p-1">
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${
|
||||||
|
active ? "bg-theme text-white" : "text-gray-900"
|
||||||
|
} group flex w-full items-center rounded-md p-2 text-xs`}
|
||||||
|
onClick={() => handleWorkspaceChange(null)}
|
||||||
|
>
|
||||||
|
All workspaces
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
{workspaces &&
|
||||||
|
workspaces.map((workspace) => (
|
||||||
|
<Menu.Item key={workspace.id}>
|
||||||
|
{({ active }) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${
|
||||||
|
active ? "bg-theme text-white" : "text-gray-900"
|
||||||
|
} group flex w-full items-center rounded-md p-2 text-xs`}
|
||||||
|
onClick={() => handleWorkspaceChange(workspace.id)}
|
||||||
|
>
|
||||||
|
{workspace.name}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
<HeaderButton
|
<HeaderButton
|
||||||
Icon={PlusIcon}
|
Icon={PlusIcon}
|
||||||
label="Add Issue"
|
label="Add Issue"
|
||||||
@ -132,36 +196,42 @@ const MyIssues: NextPage = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white">
|
<tbody className="bg-white">
|
||||||
{myIssues.map((myIssue, index) => (
|
{myIssues
|
||||||
<tr
|
.filter((i) =>
|
||||||
key={myIssue.id}
|
selectedWorkspace ? i.workspace === selectedWorkspace : true
|
||||||
className={classNames(
|
)
|
||||||
index === 0 ? "border-gray-300" : "border-gray-200",
|
.map((myIssue, index) => (
|
||||||
"border-t text-sm text-gray-900"
|
<tr
|
||||||
)}
|
key={myIssue.id}
|
||||||
>
|
className={classNames(
|
||||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 hover:text-theme max-w-[15rem] duration-300">
|
index === 0 ? "border-gray-300" : "border-gray-200",
|
||||||
<Link href={`/projects/${myIssue.project}/issues/${myIssue.id}`}>
|
"border-t text-sm text-gray-900"
|
||||||
<a>{myIssue.name}</a>
|
)}
|
||||||
</Link>
|
>
|
||||||
</td>
|
<td className="px-3 py-4 text-sm font-medium text-gray-900 hover:text-theme max-w-[15rem] duration-300">
|
||||||
<td className="px-3 py-4 max-w-[15rem] truncate">
|
<Link
|
||||||
{myIssue.description}
|
href={`/projects/${myIssue.project}/issues/${myIssue.id}`}
|
||||||
</td>
|
>
|
||||||
<td className="px-3 py-4">
|
<a>{myIssue.name}</a>
|
||||||
{myIssue.project_detail?.name}
|
</Link>
|
||||||
<br />
|
</td>
|
||||||
<span className="text-xs">{`(${myIssue.project_detail?.identifier}-${myIssue.sequence_id})`}</span>
|
<td className="px-3 py-4 max-w-[15rem] truncate">
|
||||||
</td>
|
{/* {myIssue.description} */}
|
||||||
<td className="px-3 py-4 capitalize">{myIssue.priority}</td>
|
</td>
|
||||||
<td className="relative px-3 py-4">
|
<td className="px-3 py-4">
|
||||||
<ChangeStateDropdown
|
{myIssue.project_detail?.name}
|
||||||
issue={myIssue}
|
<br />
|
||||||
updateIssues={updateMyIssues}
|
<span className="text-xs">{`(${myIssue.project_detail?.identifier}-${myIssue.sequence_id})`}</span>
|
||||||
/>
|
</td>
|
||||||
</td>
|
<td className="px-3 py-4 capitalize">{myIssue.priority}</td>
|
||||||
</tr>
|
<td className="relative px-3 py-4">
|
||||||
))}
|
<ChangeStateDropdown
|
||||||
|
issue={myIssue}
|
||||||
|
updateIssues={updateMyIssues}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,7 +25,8 @@ import { BreadcrumbItem, Breadcrumbs, HeaderButton, Spinner, EmptySpace, EmptySp
|
|||||||
import { PlusIcon } from "@heroicons/react/20/solid";
|
import { PlusIcon } from "@heroicons/react/20/solid";
|
||||||
import { ArrowPathIcon } from "@heroicons/react/24/outline";
|
import { ArrowPathIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import { IIssue, ICycle, SelectSprintType, SelectIssue } from "types";
|
import { IIssue, ICycle, SelectSprintType, SelectIssue, CycleIssueResponse } from "types";
|
||||||
|
import { DragDropContext, DropResult } from "react-beautiful-dnd";
|
||||||
|
|
||||||
const ProjectSprints: NextPage = () => {
|
const ProjectSprints: NextPage = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@ -35,7 +36,7 @@ const ProjectSprints: NextPage = () => {
|
|||||||
const [selectedIssues, setSelectedIssues] = useState<SelectIssue>();
|
const [selectedIssues, setSelectedIssues] = useState<SelectIssue>();
|
||||||
const [deleteIssue, setDeleteIssue] = useState<string | undefined>();
|
const [deleteIssue, setDeleteIssue] = useState<string | undefined>();
|
||||||
|
|
||||||
const { activeWorkspace, activeProject } = useUser();
|
const { activeWorkspace, activeProject, issues } = useUser();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@ -80,6 +81,75 @@ const ProjectSprints: NextPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDragEnd = (result: DropResult) => {
|
||||||
|
if (!result.destination) return;
|
||||||
|
const { source, destination } = result;
|
||||||
|
|
||||||
|
if (source.droppableId === destination.droppableId) return;
|
||||||
|
|
||||||
|
if (activeWorkspace && activeProject) {
|
||||||
|
// remove issue from the source cycle
|
||||||
|
mutate<CycleIssueResponse[]>(
|
||||||
|
CYCLE_ISSUES(source.droppableId),
|
||||||
|
(prevData) => prevData?.filter((p) => p.id !== result.draggableId.split(",")[0]),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
// add issue to the destination cycle
|
||||||
|
mutate(CYCLE_ISSUES(destination.droppableId));
|
||||||
|
|
||||||
|
// mutate<CycleIssueResponse[]>(
|
||||||
|
// CYCLE_ISSUES(destination.droppableId),
|
||||||
|
// (prevData) => {
|
||||||
|
// const issueDetails = issues?.results.find(
|
||||||
|
// (i) => i.id === result.draggableId.split(",")[1]
|
||||||
|
// );
|
||||||
|
// const targetResponse = prevData?.find((t) => t.cycle === destination.droppableId);
|
||||||
|
// console.log(issueDetails, targetResponse, prevData);
|
||||||
|
// if (targetResponse) {
|
||||||
|
// console.log("if");
|
||||||
|
// targetResponse.issue_details = issueDetails as IIssue;
|
||||||
|
// return prevData;
|
||||||
|
// } else {
|
||||||
|
// console.log("else");
|
||||||
|
// return [
|
||||||
|
// ...(prevData ?? []),
|
||||||
|
// {
|
||||||
|
// cycle: destination.droppableId,
|
||||||
|
// issue_details: issueDetails,
|
||||||
|
// } as CycleIssueResponse,
|
||||||
|
// ];
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// false
|
||||||
|
// );
|
||||||
|
|
||||||
|
issuesServices
|
||||||
|
.removeIssueFromCycle(
|
||||||
|
activeWorkspace.slug,
|
||||||
|
activeProject.id,
|
||||||
|
source.droppableId,
|
||||||
|
result.draggableId.split(",")[0]
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
issuesServices
|
||||||
|
.addIssueToSprint(activeWorkspace.slug, activeProject.id, destination.droppableId, {
|
||||||
|
issue: result.draggableId.split(",")[1],
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// console.log(result);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) return;
|
if (isOpen) return;
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@ -142,18 +212,20 @@ const ProjectSprints: NextPage = () => {
|
|||||||
<h2 className="text-2xl font-medium">Project Cycle</h2>
|
<h2 className="text-2xl font-medium">Project Cycle</h2>
|
||||||
<HeaderButton Icon={PlusIcon} label="Add Cycle" onClick={() => setIsOpen(true)} />
|
<HeaderButton Icon={PlusIcon} label="Add Cycle" onClick={() => setIsOpen(true)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full w-full">
|
<div className="space-y-5">
|
||||||
{cycles.map((cycle) => (
|
<DragDropContext onDragEnd={handleDragEnd}>
|
||||||
<CycleView
|
{cycles.map((cycle) => (
|
||||||
key={cycle.id}
|
<CycleView
|
||||||
sprint={cycle}
|
key={cycle.id}
|
||||||
selectSprint={setSelectedSprint}
|
cycle={cycle}
|
||||||
projectId={projectId as string}
|
selectSprint={setSelectedSprint}
|
||||||
workspaceSlug={activeWorkspace?.slug as string}
|
projectId={projectId as string}
|
||||||
openIssueModal={openIssueModal}
|
workspaceSlug={activeWorkspace?.slug as string}
|
||||||
addIssueToSprint={addIssueToSprint}
|
openIssueModal={openIssueModal}
|
||||||
/>
|
addIssueToSprint={addIssueToSprint}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -54,7 +54,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
|
|
||||||
const { issueId, projectId } = router.query;
|
const { issueId, projectId } = router.query;
|
||||||
|
|
||||||
const { activeWorkspace, activeProject, issues, mutateIssues } = useUser();
|
const { activeWorkspace, activeProject, issues, mutateIssues, states } = useUser();
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [isAddAsSubIssueOpen, setIsAddAsSubIssueOpen] = useState(false);
|
const [isAddAsSubIssueOpen, setIsAddAsSubIssueOpen] = useState(false);
|
||||||
@ -125,13 +125,6 @@ const IssueDetail: NextPage = () => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: states } = useSWR<IState[]>(
|
|
||||||
activeWorkspace && activeProject ? STATE_LIST(activeProject.id) : null,
|
|
||||||
activeWorkspace && activeProject
|
|
||||||
? () => stateServices.getStates(activeWorkspace.slug, activeProject.id)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const submitChanges = useCallback(
|
const submitChanges = useCallback(
|
||||||
(formData: Partial<IIssue>) => {
|
(formData: Partial<IIssue>) => {
|
||||||
if (!activeWorkspace || !activeProject || !issueId) return;
|
if (!activeWorkspace || !activeProject || !issueId) return;
|
||||||
@ -188,8 +181,11 @@ const IssueDetail: NextPage = () => {
|
|||||||
const nextIssue = issues?.results[issues?.results.findIndex((issue) => issue.id === issueId) + 1];
|
const nextIssue = issues?.results[issues?.results.findIndex((issue) => issue.id === issueId) + 1];
|
||||||
|
|
||||||
const subIssues = (issues && issues.results.filter((i) => i.parent === issueDetail?.id)) ?? [];
|
const subIssues = (issues && issues.results.filter((i) => i.parent === issueDetail?.id)) ?? [];
|
||||||
|
const siblingIssues =
|
||||||
|
issueDetail &&
|
||||||
|
issues?.results.filter((i) => i.parent === issueDetail.parent && i.id !== issueDetail.id);
|
||||||
|
|
||||||
const handleRemove = (issueId: string) => {
|
const handleSubIssueRemove = (issueId: string) => {
|
||||||
if (activeWorkspace && activeProject) {
|
if (activeWorkspace && activeProject) {
|
||||||
issuesServices
|
issuesServices
|
||||||
.patchIssue(activeWorkspace.slug, activeProject.id, issueId, { parent: null })
|
.patchIssue(activeWorkspace.slug, activeProject.id, issueId, { parent: null })
|
||||||
@ -224,7 +220,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
<AddAsSubIssue
|
<AddAsSubIssue
|
||||||
isOpen={isAddAsSubIssueOpen}
|
isOpen={isAddAsSubIssueOpen}
|
||||||
setIsOpen={setIsAddAsSubIssueOpen}
|
setIsOpen={setIsAddAsSubIssueOpen}
|
||||||
parentId={issueDetail?.id ?? ""}
|
parent={issueDetail}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center justify-between w-full mb-5">
|
<div className="flex items-center justify-between w-full mb-5">
|
||||||
@ -266,6 +262,66 @@ const IssueDetail: NextPage = () => {
|
|||||||
<div className="grid grid-cols-4 gap-5">
|
<div className="grid grid-cols-4 gap-5">
|
||||||
<div className="col-span-3 space-y-5">
|
<div className="col-span-3 space-y-5">
|
||||||
<div className="bg-secondary rounded-lg p-4">
|
<div className="bg-secondary rounded-lg p-4">
|
||||||
|
{issueDetail.parent !== null && issueDetail.parent !== "" ? (
|
||||||
|
<div className="bg-gray-100 flex items-center gap-2 p-2 text-xs rounded mb-5 w-min whitespace-nowrap">
|
||||||
|
<Link href={`/projects/${activeProject.id}/issues/${issueDetail.parent}`}>
|
||||||
|
<a className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={`h-1.5 w-1.5 block rounded-full`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: issueDetail.state_detail.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="flex-shrink-0 text-gray-600">
|
||||||
|
{activeProject.identifier}-{issueDetail.sequence_id}
|
||||||
|
</span>
|
||||||
|
<span className="font-medium truncate">
|
||||||
|
{issues?.results
|
||||||
|
.find((i) => i.id === issueDetail.parent)
|
||||||
|
?.name.substring(0, 50)}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
<Menu as="div" className="relative inline-block">
|
||||||
|
<Menu.Button className="grid relative place-items-center hover:bg-gray-200 rounded p-1 focus:outline-none">
|
||||||
|
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||||
|
</Menu.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items className="absolute left-0 mt-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
|
||||||
|
<div className="p-1">
|
||||||
|
{siblingIssues && siblingIssues.length > 0 ? (
|
||||||
|
siblingIssues.map((issue) => (
|
||||||
|
<Menu.Item as="div" key={issue.id}>
|
||||||
|
<Link href={`/projects/${activeProject.id}/issues/${issue.id}`}>
|
||||||
|
<a className="flex items-center gap-2 p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap">
|
||||||
|
{activeProject.identifier}-{issue.sequence_id}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Menu.Item
|
||||||
|
as="div"
|
||||||
|
className="flex items-center gap-2 p-2 text-left text-gray-900 text-xs whitespace-nowrap"
|
||||||
|
>
|
||||||
|
No other sub-issues
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div>
|
<div>
|
||||||
<TextArea
|
<TextArea
|
||||||
id="name"
|
id="name"
|
||||||
@ -280,7 +336,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
mode="transparent"
|
mode="transparent"
|
||||||
className="text-xl font-medium"
|
className="text-xl font-medium"
|
||||||
/>
|
/>
|
||||||
{/* <TextArea
|
<TextArea
|
||||||
id="description"
|
id="description"
|
||||||
name="description"
|
name="description"
|
||||||
error={errors.description}
|
error={errors.description}
|
||||||
@ -293,8 +349,8 @@ const IssueDetail: NextPage = () => {
|
|||||||
placeholder="Enter issue description"
|
placeholder="Enter issue description"
|
||||||
mode="transparent"
|
mode="transparent"
|
||||||
register={register}
|
register={register}
|
||||||
/> */}
|
/>
|
||||||
<Controller
|
{/* <Controller
|
||||||
name="description"
|
name="description"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
@ -304,7 +360,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
value={JSON.parse(issueDetail.description)}
|
value={JSON.parse(issueDetail.description)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/> */}
|
||||||
{/* <LexicalViewer id="descriptionViewer" value={JSON.parse(issueDetail.description)} /> */}
|
{/* <LexicalViewer id="descriptionViewer" value={JSON.parse(issueDetail.description)} /> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
@ -332,7 +388,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-3 w-3" />
|
<PlusIcon className="h-3 w-3" />
|
||||||
Add new
|
Create new
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Menu as="div" className="relative inline-block">
|
<Menu as="div" className="relative inline-block">
|
||||||
@ -379,57 +435,59 @@ const IssueDetail: NextPage = () => {
|
|||||||
>
|
>
|
||||||
<Disclosure.Panel className="flex flex-col gap-y-1 mt-3">
|
<Disclosure.Panel className="flex flex-col gap-y-1 mt-3">
|
||||||
{subIssues.map((subIssue) => (
|
{subIssues.map((subIssue) => (
|
||||||
<Link
|
<div
|
||||||
key={subIssue.id}
|
key={subIssue.id}
|
||||||
href={`/projects/${activeProject.id}/issues/${subIssue.id}`}
|
className="flex justify-between items-center gap-2 p-2 hover:bg-gray-100"
|
||||||
>
|
>
|
||||||
<a className="p-2 flex justify-between items-center rounded text-xs hover:bg-gray-100">
|
<Link href={`/projects/${activeProject.id}/issues/${subIssue.id}`}>
|
||||||
<div className="flex items-center gap-2">
|
<a className="flex items-center gap-2 rounded text-xs">
|
||||||
<span
|
<span
|
||||||
className={`h-1.5 w-1.5 block rounded-full`}
|
className={`h-1.5 w-1.5 block rounded-full`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: subIssue.state_detail.color,
|
backgroundColor: subIssue.state_detail.color,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="text-gray-600">
|
<span className="flex-shrink-0 text-gray-600">
|
||||||
{activeProject.identifier}-{subIssue.sequence_id}
|
{activeProject.identifier}-{subIssue.sequence_id}
|
||||||
</span>
|
</span>
|
||||||
<span className="font-medium">{subIssue.name}</span>
|
<span className="font-medium max-w-sm break-all">
|
||||||
</div>
|
{subIssue.name}
|
||||||
<div>
|
</span>
|
||||||
<Menu as="div" className="relative inline-block">
|
</a>
|
||||||
<Menu.Button className="grid relative place-items-center focus:outline-none">
|
</Link>
|
||||||
<EllipsisHorizontalIcon className="h-4 w-4" />
|
<div>
|
||||||
</Menu.Button>
|
<Menu as="div" className="relative inline-block">
|
||||||
|
<Menu.Button className="grid relative place-items-center focus:outline-none">
|
||||||
|
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||||
|
</Menu.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
as={React.Fragment}
|
as={React.Fragment}
|
||||||
enter="transition ease-out duration-100"
|
enter="transition ease-out duration-100"
|
||||||
enterFrom="transform opacity-0 scale-95"
|
enterFrom="transform opacity-0 scale-95"
|
||||||
enterTo="transform opacity-100 scale-100"
|
enterTo="transform opacity-100 scale-100"
|
||||||
leave="transition ease-in duration-75"
|
leave="transition ease-in duration-75"
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="origin-top-right absolute right-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
|
<Menu.Items className="origin-top-right absolute right-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
<Menu.Item as="div">
|
<Menu.Item as="div">
|
||||||
{(active) => (
|
{(active) => (
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2 p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap"
|
className="flex items-center gap-2 p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap"
|
||||||
onClick={() => handleRemove(subIssue.id)}
|
onClick={() => handleSubIssueRemove(subIssue.id)}
|
||||||
>
|
>
|
||||||
Remove as sub-issue
|
Remove as sub-issue
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Items>
|
</Menu.Items>
|
||||||
</Transition>
|
</Transition>
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
</Link>
|
|
||||||
))}
|
))}
|
||||||
</Disclosure.Panel>
|
</Disclosure.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
@ -452,7 +510,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="origin-top-right absolute left-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
<Menu.Items className="absolute origin-top-right left-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
<Menu.Item as="div">
|
<Menu.Item as="div">
|
||||||
{(active) => (
|
{(active) => (
|
||||||
@ -467,7 +525,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add new
|
Create new
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
@ -1,52 +1,55 @@
|
|||||||
import React, { useEffect, useCallback, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
// next
|
// next
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
// swr
|
// swr
|
||||||
import { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
// swr
|
|
||||||
import useSWR from "swr";
|
|
||||||
// react hook form
|
// react hook form
|
||||||
import { useForm, Controller } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Listbox, Tab, Transition } from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
// layouts
|
// layouts
|
||||||
import AdminLayout from "layouts/AdminLayout";
|
import AdminLayout from "layouts/AdminLayout";
|
||||||
// service
|
// service
|
||||||
import projectServices from "lib/services/project.service";
|
import projectServices from "lib/services/project.service";
|
||||||
import workspaceService from "lib/services/workspace.service";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
import useToast from "lib/hooks/useToast";
|
import useToast from "lib/hooks/useToast";
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import { PROJECT_DETAILS, PROJECTS_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
import { PROJECT_DETAILS, PROJECTS_LIST } from "constants/fetch-keys";
|
||||||
// commons
|
|
||||||
import { addSpaceIfCamelCase, debounce } from "constants/common";
|
|
||||||
// components
|
|
||||||
import CreateUpdateStateModal from "components/project/issues/BoardView/state/CreateUpdateStateModal";
|
|
||||||
// ui
|
// ui
|
||||||
import { Spinner, Button, Input, TextArea, Select } from "ui";
|
import { Spinner } from "ui";
|
||||||
import { Breadcrumbs, BreadcrumbItem } from "ui/Breadcrumbs";
|
import { Breadcrumbs, BreadcrumbItem } from "ui/Breadcrumbs";
|
||||||
// icons
|
|
||||||
import {
|
|
||||||
ChevronDownIcon,
|
|
||||||
CheckIcon,
|
|
||||||
PlusIcon,
|
|
||||||
PencilSquareIcon,
|
|
||||||
RectangleGroupIcon,
|
|
||||||
PencilIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
// types
|
// types
|
||||||
import type { IProject, IWorkspace, WorkspaceMember } from "types";
|
import type { IProject, IWorkspace } from "types";
|
||||||
|
|
||||||
const defaultValues: Partial<IProject> = {
|
const defaultValues: Partial<IProject> = {
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
|
||||||
|
|
||||||
const ProjectSettings: NextPage = () => {
|
const ProjectSettings: NextPage = () => {
|
||||||
|
const GeneralSettings = dynamic(() => import("components/project/settings/GeneralSettings"), {
|
||||||
|
loading: () => <p>Loading...</p>,
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ControlSettings = dynamic(() => import("components/project/settings/ControlSettings"), {
|
||||||
|
loading: () => <p>Loading...</p>,
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const StatesSettings = dynamic(() => import("components/project/settings/StatesSettings"), {
|
||||||
|
loading: () => <p>Loading...</p>,
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const LabelsSettings = dynamic(() => import("components/project/settings/LabelsSettings"), {
|
||||||
|
loading: () => <p>Loading...</p>,
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@ -58,15 +61,11 @@ const ProjectSettings: NextPage = () => {
|
|||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isCreateStateModalOpen, setIsCreateStateModalOpen] = useState(false);
|
|
||||||
const [selectedState, setSelectedState] = useState<string | undefined>();
|
|
||||||
const [newGroupForm, setNewGroupForm] = useState(false);
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { projectId } = router.query;
|
const { projectId } = router.query;
|
||||||
|
|
||||||
const { activeWorkspace, activeProject, states } = useUser();
|
const { activeWorkspace, activeProject } = useUser();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -77,11 +76,6 @@ const ProjectSettings: NextPage = () => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: people } = useSWR<WorkspaceMember[]>(
|
|
||||||
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
|
||||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
projectDetails &&
|
projectDetails &&
|
||||||
reset({
|
reset({
|
||||||
@ -130,28 +124,9 @@ const ProjectSettings: NextPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkIdentifier = (slug: string, value: string) => {
|
|
||||||
projectServices.checkProjectIdentifierAvailability(slug, value).then((response) => {
|
|
||||||
console.log(response);
|
|
||||||
if (response.exists) setError("identifier", { message: "Identifier already exists" });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
const checkIdentifierAvailability = useCallback(debounce(checkIdentifier, 1500), []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
<div className="space-y-5 mb-5">
|
<div className="space-y-5 mb-5">
|
||||||
<CreateUpdateStateModal
|
|
||||||
isOpen={isCreateStateModalOpen || Boolean(selectedState)}
|
|
||||||
handleClose={() => {
|
|
||||||
setSelectedState(undefined);
|
|
||||||
setIsCreateStateModalOpen(false);
|
|
||||||
}}
|
|
||||||
projectId={projectId as string}
|
|
||||||
data={selectedState ? states?.find((state) => state.id === selectedState) : undefined}
|
|
||||||
/>
|
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<BreadcrumbItem title="Projects" link="/projects" />
|
<BreadcrumbItem title="Projects" link="/projects" />
|
||||||
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Settings`} />
|
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Settings`} />
|
||||||
@ -159,373 +134,38 @@ const ProjectSettings: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
{projectDetails ? (
|
{projectDetails ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="mt-3">
|
<Tab.Group>
|
||||||
<Tab.Group>
|
<Tab.List className="flex items-center gap-x-4 gap-y-2 flex-wrap mb-8">
|
||||||
<Tab.List className="flex items-center gap-x-4 gap-y-2 flex-wrap">
|
{["General", "Control", "States", "Labels"].map((tab, index) => (
|
||||||
{["General", "Control", "States", "Labels"].map((tab, index) => (
|
<Tab
|
||||||
<Tab
|
key={index}
|
||||||
key={index}
|
className={({ selected }) =>
|
||||||
className={({ selected }) =>
|
`px-6 py-2 border-2 border-theme hover:bg-theme hover:text-white text-xs font-medium rounded-md outline-none duration-300 ${
|
||||||
`px-6 py-2 border-2 border-theme hover:bg-theme hover:text-white text-xs font-medium rounded-md duration-300 ${
|
selected ? "bg-theme text-white" : ""
|
||||||
selected ? "bg-theme text-white" : ""
|
}`
|
||||||
}`
|
}
|
||||||
}
|
>
|
||||||
>
|
{tab}
|
||||||
{tab}
|
</Tab>
|
||||||
</Tab>
|
))}
|
||||||
))}
|
</Tab.List>
|
||||||
</Tab.List>
|
<Tab.Panels>
|
||||||
<Tab.Panels className="mt-8">
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<section className="space-y-5">
|
<GeneralSettings register={register} errors={errors} setError={setError} />
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900">General</h3>
|
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
|
||||||
This information will be displayed to every member of the project.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4 gap-3">
|
|
||||||
<div className="col-span-2">
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
error={errors.name}
|
|
||||||
register={register}
|
|
||||||
placeholder="Project Name"
|
|
||||||
label="Name"
|
|
||||||
validations={{
|
|
||||||
required: "Name is required",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Select
|
|
||||||
name="network"
|
|
||||||
id="network"
|
|
||||||
options={Object.keys(NETWORK_CHOICES).map((key) => ({
|
|
||||||
value: key,
|
|
||||||
label: NETWORK_CHOICES[key as keyof typeof NETWORK_CHOICES],
|
|
||||||
}))}
|
|
||||||
label="Network"
|
|
||||||
register={register}
|
|
||||||
validations={{
|
|
||||||
required: "Network is required",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Input
|
|
||||||
id="identifier"
|
|
||||||
name="identifier"
|
|
||||||
error={errors.identifier}
|
|
||||||
register={register}
|
|
||||||
placeholder="Enter identifier"
|
|
||||||
label="Identifier"
|
|
||||||
onChange={(e: any) => {
|
|
||||||
if (!activeWorkspace || !e.target.value) return;
|
|
||||||
checkIdentifierAvailability(activeWorkspace.slug, e.target.value);
|
|
||||||
}}
|
|
||||||
validations={{
|
|
||||||
required: "Identifier is required",
|
|
||||||
minLength: {
|
|
||||||
value: 1,
|
|
||||||
message: "Identifier must at least be of 1 character",
|
|
||||||
},
|
|
||||||
maxLength: {
|
|
||||||
value: 9,
|
|
||||||
message: "Identifier must at most be of 9 characters",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<TextArea
|
|
||||||
id="description"
|
|
||||||
name="description"
|
|
||||||
error={errors.description}
|
|
||||||
register={register}
|
|
||||||
label="Description"
|
|
||||||
placeholder="Enter project description"
|
|
||||||
validations={{
|
|
||||||
required: "Description is required",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<section className="space-y-5">
|
<ControlSettings control={control} isSubmitting={isSubmitting} />
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900">Control</h3>
|
|
||||||
<p className="mt-1 text-sm text-gray-500">Set the control for the project.</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between gap-3">
|
|
||||||
<div className="w-full md:w-1/2">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="project_lead"
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<Listbox value={value} onChange={onChange}>
|
|
||||||
{({ open }) => (
|
|
||||||
<>
|
|
||||||
<Listbox.Label>
|
|
||||||
<div className="text-gray-500 mb-2">Project Lead</div>
|
|
||||||
</Listbox.Label>
|
|
||||||
<div className="relative">
|
|
||||||
<Listbox.Button className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
|
||||||
<span className="block truncate">
|
|
||||||
{people?.find((person) => person.member.id === value)
|
|
||||||
?.member.first_name ?? "Select Lead"}
|
|
||||||
</span>
|
|
||||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
|
||||||
<ChevronDownIcon
|
|
||||||
className="h-5 w-5 text-gray-400"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</Listbox.Button>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
show={open}
|
|
||||||
as={React.Fragment}
|
|
||||||
leave="transition ease-in duration-100"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<Listbox.Options className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
|
||||||
{people?.map((person) => (
|
|
||||||
<Listbox.Option
|
|
||||||
key={person.id}
|
|
||||||
className={({ active }) =>
|
|
||||||
`${
|
|
||||||
active ? "text-white bg-theme" : "text-gray-900"
|
|
||||||
} cursor-default select-none relative py-2 pl-3 pr-9`
|
|
||||||
}
|
|
||||||
value={person.member.id}
|
|
||||||
>
|
|
||||||
{({ selected, active }) => (
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
className={`${
|
|
||||||
selected ? "font-semibold" : "font-normal"
|
|
||||||
} block truncate`}
|
|
||||||
>
|
|
||||||
{person.member.first_name}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{selected ? (
|
|
||||||
<span
|
|
||||||
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
|
|
||||||
active ? "text-white" : "text-indigo-600"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<CheckIcon
|
|
||||||
className="h-5 w-5"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Listbox.Option>
|
|
||||||
))}
|
|
||||||
</Listbox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Listbox>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-full md:w-1/2">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="default_assignee"
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<Listbox value={value} onChange={onChange}>
|
|
||||||
{({ open }) => (
|
|
||||||
<>
|
|
||||||
<Listbox.Label>
|
|
||||||
<div className="text-gray-500 mb-2">Default Assignee</div>
|
|
||||||
</Listbox.Label>
|
|
||||||
<div className="relative">
|
|
||||||
<Listbox.Button className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
|
||||||
<span className="block truncate">
|
|
||||||
{people?.find((p) => p.member.id === value)?.member
|
|
||||||
.first_name ?? "Select Default Assignee"}
|
|
||||||
</span>
|
|
||||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
|
||||||
<ChevronDownIcon
|
|
||||||
className="h-5 w-5 text-gray-400"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</Listbox.Button>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
show={open}
|
|
||||||
as={React.Fragment}
|
|
||||||
leave="transition ease-in duration-100"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<Listbox.Options className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
|
||||||
{people?.map((person) => (
|
|
||||||
<Listbox.Option
|
|
||||||
key={person.id}
|
|
||||||
className={({ active }) =>
|
|
||||||
`${
|
|
||||||
active ? "text-white bg-theme" : "text-gray-900"
|
|
||||||
} cursor-default select-none relative py-2 pl-3 pr-9`
|
|
||||||
}
|
|
||||||
value={person.member.id}
|
|
||||||
>
|
|
||||||
{({ selected, active }) => (
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
className={`${
|
|
||||||
selected ? "font-semibold" : "font-normal"
|
|
||||||
} block truncate`}
|
|
||||||
>
|
|
||||||
{person.member.first_name}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{selected ? (
|
|
||||||
<span
|
|
||||||
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
|
|
||||||
active ? "text-white" : "text-indigo-600"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<CheckIcon
|
|
||||||
className="h-5 w-5"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Listbox.Option>
|
|
||||||
))}
|
|
||||||
</Listbox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Listbox>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? "Updating Project..." : "Update Project"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel>
|
</form>
|
||||||
<section className="space-y-5">
|
<Tab.Panel>
|
||||||
<div>
|
<StatesSettings projectId={projectId} />
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900">State</h3>
|
</Tab.Panel>
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
<Tab.Panel>
|
||||||
Manage the state of this project.
|
<LabelsSettings />
|
||||||
</p>
|
</Tab.Panel>
|
||||||
</div>
|
</Tab.Panels>
|
||||||
<div className="flex justify-between gap-3">
|
</Tab.Group>
|
||||||
<div className="w-full space-y-5">
|
|
||||||
{states?.map((state) => (
|
|
||||||
<div
|
|
||||||
key={state.id}
|
|
||||||
className="bg-white px-4 py-2 rounded flex justify-between items-center"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-x-2">
|
|
||||||
<div
|
|
||||||
className="w-3 h-3 rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: state.color,
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
<h4>{addSpaceIfCamelCase(state.name)}</h4>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button type="button" onClick={() => setSelectedState(state.id)}>
|
|
||||||
<PencilSquareIcon className="h-5 w-5 text-gray-400" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="flex items-center gap-x-1"
|
|
||||||
onClick={() => setIsCreateStateModalOpen(true)}
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
<span>Add State</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel>
|
|
||||||
<section className="space-y-5">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900">Labels</h3>
|
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
|
||||||
Manage the labels of this project.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
className="flex items-center gap-x-1"
|
|
||||||
onClick={() => setNewGroupForm(true)}
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
New group
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-5">
|
|
||||||
<div
|
|
||||||
className={`bg-white px-4 py-2 flex items-center gap-2 ${
|
|
||||||
newGroupForm ? "" : "hidden"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Input type="text" name="groupName" />
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
theme="secondary"
|
|
||||||
onClick={() => setNewGroupForm(false)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="button">Save</Button>
|
|
||||||
</div>
|
|
||||||
{["", ""].map((group, index) => (
|
|
||||||
<div key={index} className="bg-white p-4 text-gray-900 rounded-md">
|
|
||||||
<h3 className="font-medium leading-5 flex items-center gap-2">
|
|
||||||
<RectangleGroupIcon className="h-5 w-5" />
|
|
||||||
This is the label group title
|
|
||||||
</h3>
|
|
||||||
<div className="pl-5 mt-4">
|
|
||||||
<div className="group text-sm flex justify-between items-center p-2 hover:bg-gray-100 rounded">
|
|
||||||
<h5 className="flex items-center gap-2">
|
|
||||||
<div className="w-2 h-2 bg-red-600 rounded-full"></div>
|
|
||||||
This is the label title
|
|
||||||
</h5>
|
|
||||||
<div className="hidden group-hover:block">
|
|
||||||
<PencilIcon className="h-3 w-3" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Tab.Panel>
|
|
||||||
</Tab.Panels>
|
|
||||||
</Tab.Group>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-full flex justify-center items-center">
|
<div className="h-full w-full flex justify-center items-center">
|
||||||
|
6
apps/app/types/sprints.d.ts
vendored
6
apps/app/types/sprints.d.ts
vendored
@ -17,7 +17,7 @@ export interface ICycle {
|
|||||||
issue: string;
|
issue: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SprintIssueResponse {
|
export interface CycleIssueResponse {
|
||||||
id: string;
|
id: string;
|
||||||
issue_details: IIssue;
|
issue_details: IIssue;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
@ -30,8 +30,8 @@ export interface SprintIssueResponse {
|
|||||||
cycle: string;
|
cycle: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SprintViewProps = {
|
export type CycleViewProps = {
|
||||||
sprint: ICycle;
|
cycle: ICycle;
|
||||||
selectSprint: React.Dispatch<React.SetStateAction<SelectSprintType>>;
|
selectSprint: React.Dispatch<React.SetStateAction<SelectSprintType>>;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
|
@ -90,8 +90,6 @@ const CustomListbox: React.FC<Props> = ({
|
|||||||
: optionsFontsize === "2xl"
|
: optionsFontsize === "2xl"
|
||||||
? "text-2xl"
|
? "text-2xl"
|
||||||
: ""
|
: ""
|
||||||
} ${
|
|
||||||
className || ""
|
|
||||||
} rounded-md py-1 ring-1 ring-black ring-opacity-5 focus:outline-none z-10`}
|
} rounded-md py-1 ring-1 ring-black ring-opacity-5 focus:outline-none z-10`}
|
||||||
>
|
>
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
|
@ -6,7 +6,7 @@ const Select: React.FC<Props> = ({
|
|||||||
id,
|
id,
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
className,
|
className = "",
|
||||||
name,
|
name,
|
||||||
register,
|
register,
|
||||||
disabled,
|
disabled,
|
||||||
@ -27,7 +27,7 @@ const Select: React.FC<Props> = ({
|
|||||||
value={value}
|
value={value}
|
||||||
{...(register && register(name, validations))}
|
{...(register && register(name, validations))}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="mt-1 block w-full px-3 py-2 text-base border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md bg-transparent"
|
className={`mt-1 block w-full px-3 py-2 text-base border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md bg-transparent ${className}`}
|
||||||
>
|
>
|
||||||
{options.map((option, index) => (
|
{options.map((option, index) => (
|
||||||
<option value={option.value} key={index}>
|
<option value={option.value} key={index}>
|
||||||
|
Loading…
Reference in New Issue
Block a user