feat: all functionalities in cycle, crud for labels in settings

This commit is contained in:
Aaryan Khandelwal 2022-12-06 20:08:28 +05:30
parent 93552f190d
commit 259213851f
30 changed files with 1784 additions and 1055 deletions

View file

@ -25,7 +25,8 @@ import { BreadcrumbItem, Breadcrumbs, HeaderButton, Spinner, EmptySpace, EmptySp
import { PlusIcon } from "@heroicons/react/20/solid";
import { ArrowPathIcon } from "@heroicons/react/24/outline";
// 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 [isOpen, setIsOpen] = useState(false);
@ -35,7 +36,7 @@ const ProjectSprints: NextPage = () => {
const [selectedIssues, setSelectedIssues] = useState<SelectIssue>();
const [deleteIssue, setDeleteIssue] = useState<string | undefined>();
const { activeWorkspace, activeProject } = useUser();
const { activeWorkspace, activeProject, issues } = useUser();
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(() => {
if (isOpen) return;
const timer = setTimeout(() => {
@ -142,18 +212,20 @@ const ProjectSprints: NextPage = () => {
<h2 className="text-2xl font-medium">Project Cycle</h2>
<HeaderButton Icon={PlusIcon} label="Add Cycle" onClick={() => setIsOpen(true)} />
</div>
<div className="h-full w-full">
{cycles.map((cycle) => (
<CycleView
key={cycle.id}
sprint={cycle}
selectSprint={setSelectedSprint}
projectId={projectId as string}
workspaceSlug={activeWorkspace?.slug as string}
openIssueModal={openIssueModal}
addIssueToSprint={addIssueToSprint}
/>
))}
<div className="space-y-5">
<DragDropContext onDragEnd={handleDragEnd}>
{cycles.map((cycle) => (
<CycleView
key={cycle.id}
cycle={cycle}
selectSprint={setSelectedSprint}
projectId={projectId as string}
workspaceSlug={activeWorkspace?.slug as string}
openIssueModal={openIssueModal}
addIssueToSprint={addIssueToSprint}
/>
))}
</DragDropContext>
</div>
</div>
) : (

View file

@ -54,7 +54,7 @@ const IssueDetail: NextPage = () => {
const { issueId, projectId } = router.query;
const { activeWorkspace, activeProject, issues, mutateIssues } = useUser();
const { activeWorkspace, activeProject, issues, mutateIssues, states } = useUser();
const [isOpen, setIsOpen] = useState(false);
const [isAddAsSubIssueOpen, setIsAddAsSubIssueOpen] = useState(false);
@ -125,13 +125,6 @@ const IssueDetail: NextPage = () => {
: 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(
(formData: Partial<IIssue>) => {
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 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) {
issuesServices
.patchIssue(activeWorkspace.slug, activeProject.id, issueId, { parent: null })
@ -224,7 +220,7 @@ const IssueDetail: NextPage = () => {
<AddAsSubIssue
isOpen={isAddAsSubIssueOpen}
setIsOpen={setIsAddAsSubIssueOpen}
parentId={issueDetail?.id ?? ""}
parent={issueDetail}
/>
<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="col-span-3 space-y-5">
<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>
<TextArea
id="name"
@ -280,7 +336,7 @@ const IssueDetail: NextPage = () => {
mode="transparent"
className="text-xl font-medium"
/>
{/* <TextArea
<TextArea
id="description"
name="description"
error={errors.description}
@ -293,8 +349,8 @@ const IssueDetail: NextPage = () => {
placeholder="Enter issue description"
mode="transparent"
register={register}
/> */}
<Controller
/>
{/* <Controller
name="description"
control={control}
render={({ field }) => (
@ -304,7 +360,7 @@ const IssueDetail: NextPage = () => {
value={JSON.parse(issueDetail.description)}
/>
)}
/>
/> */}
{/* <LexicalViewer id="descriptionViewer" value={JSON.parse(issueDetail.description)} /> */}
</div>
<div className="mt-2">
@ -332,7 +388,7 @@ const IssueDetail: NextPage = () => {
}}
>
<PlusIcon className="h-3 w-3" />
Add new
Create new
</button>
<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">
{subIssues.map((subIssue) => (
<Link
<div
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">
<div className="flex items-center gap-2">
<Link href={`/projects/${activeProject.id}/issues/${subIssue.id}`}>
<a className="flex items-center gap-2 rounded text-xs">
<span
className={`h-1.5 w-1.5 block rounded-full`}
style={{
backgroundColor: subIssue.state_detail.color,
}}
/>
<span className="text-gray-600">
<span className="flex-shrink-0 text-gray-600">
{activeProject.identifier}-{subIssue.sequence_id}
</span>
<span className="font-medium">{subIssue.name}</span>
</div>
<div>
<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>
<span className="font-medium max-w-sm break-all">
{subIssue.name}
</span>
</a>
</Link>
<div>
<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
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-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">
<Menu.Item as="div">
{(active) => (
<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"
onClick={() => handleRemove(subIssue.id)}
>
Remove as sub-issue
</button>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
</a>
</Link>
<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-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">
<Menu.Item as="div">
{(active) => (
<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"
onClick={() => handleSubIssueRemove(subIssue.id)}
>
Remove as sub-issue
</button>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
</div>
))}
</Disclosure.Panel>
</Transition>
@ -452,7 +510,7 @@ const IssueDetail: NextPage = () => {
leaveFrom="transform opacity-100 scale-100"
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">
<Menu.Item as="div">
{(active) => (
@ -467,7 +525,7 @@ const IssueDetail: NextPage = () => {
});
}}
>
Add new
Create new
</button>
)}
</Menu.Item>

View file

@ -1,52 +1,55 @@
import React, { useEffect, useCallback, useState } from "react";
import React, { useEffect, useState } from "react";
// next
import type { NextPage } from "next";
import { useRouter } from "next/router";
import dynamic from "next/dynamic";
// swr
import { mutate } from "swr";
// swr
import useSWR from "swr";
import useSWR, { mutate } from "swr";
// react hook form
import { useForm, Controller } from "react-hook-form";
import { useForm } from "react-hook-form";
// headless ui
import { Listbox, Tab, Transition } from "@headlessui/react";
import { Tab } from "@headlessui/react";
// layouts
import AdminLayout from "layouts/AdminLayout";
// service
import projectServices from "lib/services/project.service";
import workspaceService from "lib/services/workspace.service";
// hooks
import useUser from "lib/hooks/useUser";
import useToast from "lib/hooks/useToast";
// fetch keys
import { PROJECT_DETAILS, PROJECTS_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys";
// commons
import { addSpaceIfCamelCase, debounce } from "constants/common";
// components
import CreateUpdateStateModal from "components/project/issues/BoardView/state/CreateUpdateStateModal";
import { PROJECT_DETAILS, PROJECTS_LIST } from "constants/fetch-keys";
// ui
import { Spinner, Button, Input, TextArea, Select } from "ui";
import { Spinner } from "ui";
import { Breadcrumbs, BreadcrumbItem } from "ui/Breadcrumbs";
// icons
import {
ChevronDownIcon,
CheckIcon,
PlusIcon,
PencilSquareIcon,
RectangleGroupIcon,
PencilIcon,
} from "@heroicons/react/24/outline";
// types
import type { IProject, IWorkspace, WorkspaceMember } from "types";
import type { IProject, IWorkspace } from "types";
const defaultValues: Partial<IProject> = {
name: "",
description: "",
};
const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
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 {
register,
handleSubmit,
@ -58,15 +61,11 @@ const ProjectSettings: NextPage = () => {
defaultValues,
});
const [isCreateStateModalOpen, setIsCreateStateModalOpen] = useState(false);
const [selectedState, setSelectedState] = useState<string | undefined>();
const [newGroupForm, setNewGroupForm] = useState(false);
const router = useRouter();
const { projectId } = router.query;
const { activeWorkspace, activeProject, states } = useUser();
const { activeWorkspace, activeProject } = useUser();
const { setToastAlert } = useToast();
@ -77,11 +76,6 @@ const ProjectSettings: NextPage = () => {
: null
);
const { data: people } = useSWR<WorkspaceMember[]>(
activeWorkspace ? WORKSPACE_MEMBERS : null,
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
);
useEffect(() => {
projectDetails &&
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 (
<AdminLayout>
<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>
<BreadcrumbItem title="Projects" link="/projects" />
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Settings`} />
@ -159,373 +134,38 @@ const ProjectSettings: NextPage = () => {
</div>
{projectDetails ? (
<div className="space-y-3">
<form onSubmit={handleSubmit(onSubmit)} className="mt-3">
<Tab.Group>
<Tab.List className="flex items-center gap-x-4 gap-y-2 flex-wrap">
{["General", "Control", "States", "Labels"].map((tab, index) => (
<Tab
key={index}
className={({ selected }) =>
`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" : ""
}`
}
>
{tab}
</Tab>
))}
</Tab.List>
<Tab.Panels className="mt-8">
<Tab.Group>
<Tab.List className="flex items-center gap-x-4 gap-y-2 flex-wrap mb-8">
{["General", "Control", "States", "Labels"].map((tab, index) => (
<Tab
key={index}
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 ${
selected ? "bg-theme text-white" : ""
}`
}
>
{tab}
</Tab>
))}
</Tab.List>
<Tab.Panels>
<form onSubmit={handleSubmit(onSubmit)}>
<Tab.Panel>
<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>
<GeneralSettings register={register} errors={errors} setError={setError} />
</Tab.Panel>
<Tab.Panel>
<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>
<ControlSettings control={control} isSubmitting={isSubmitting} />
</Tab.Panel>
<Tab.Panel>
<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={() => 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>
</form>
<Tab.Panel>
<StatesSettings projectId={projectId} />
</Tab.Panel>
<Tab.Panel>
<LabelsSettings />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
) : (
<div className="h-full w-full flex justify-center items-center">