feat: create/update state according to group from project settings pages

refractor: followed naming convension and made components easy to use
This commit is contained in:
Dakshesh Jain 2022-12-13 18:45:23 +05:30
parent b5a33d8f4d
commit c9b1a2590a
5 changed files with 296 additions and 62 deletions

View File

@ -18,11 +18,11 @@ import { Button } from "ui";
import type { IState } from "types";
type Props = {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
data?: IState;
onClose: () => void;
data: IState | null;
};
const ConfirmStateDeletion: React.FC<Props> = ({ isOpen, setIsOpen, data }) => {
const ConfirmStateDeletion: React.FC<Props> = ({ isOpen, onClose, data }) => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const { activeWorkspace } = useUser();
@ -30,7 +30,7 @@ const ConfirmStateDeletion: React.FC<Props> = ({ isOpen, setIsOpen, data }) => {
const cancelButtonRef = useRef(null);
const handleClose = () => {
setIsOpen(false);
onClose();
setIsDeleteLoading(false);
};
@ -53,10 +53,6 @@ const ConfirmStateDeletion: React.FC<Props> = ({ isOpen, setIsOpen, data }) => {
});
};
useEffect(() => {
data && setIsOpen(true);
}, [data, setIsOpen]);
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog

View File

@ -11,10 +11,12 @@ import { Dialog, Popover, Transition } from "@headlessui/react";
import stateService from "lib/services/state.service";
// fetch keys
import { STATE_LIST } from "constants/fetch-keys";
// constants
import { GROUP_CHOICES } from "constants/";
// hooks
import useUser from "lib/hooks/useUser";
// ui
import { Button, Input, TextArea } from "ui";
import { Button, Input, Select, TextArea } from "ui";
// icons
import { ChevronDownIcon } from "@heroicons/react/24/outline";
@ -31,6 +33,7 @@ const defaultValues: Partial<IState> = {
name: "",
description: "",
color: "#000000",
group: "backlog",
};
const CreateUpdateStateModal: React.FC<Props> = ({ isOpen, data, projectId, handleClose }) => {
@ -161,6 +164,22 @@ const CreateUpdateStateModal: React.FC<Props> = ({ isOpen, data, projectId, hand
}}
/>
</div>
<div>
<Select
id="group"
label="Group"
name="group"
error={errors.group}
register={register}
validations={{
required: "Group is required",
}}
options={Object.keys(GROUP_CHOICES).map((key) => ({
value: key,
label: GROUP_CHOICES[key as keyof typeof GROUP_CHOICES],
}))}
/>
</div>
<div>
<Popover className="relative">
{({ open }) => (

View File

@ -32,7 +32,7 @@ import SelectProject from "./SelectProject";
import SelectPriority from "./SelectPriority";
import SelectAssignee from "./SelectAssignee";
import SelectParent from "./SelectParentIssue";
import CreateUpdateStateModal from "components/project/issues/BoardView/state/CreateUpdateStateModal";
import CreateUpdateStateModal from "components/project/issues/BoardView/state/create-update-state-modal";
import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal";
// types

View File

@ -1,74 +1,285 @@
// react
import { useState } from "react";
import React, { useEffect, useState } from "react";
// swr
import { mutate } from "swr";
// react hook form
import { useForm, Controller } from "react-hook-form";
// react color
import { TwitterPicker } from "react-color";
// headless
import { Popover, Transition } from "@headlessui/react";
// 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";
import { STATE_LIST } from "constants/fetch-keys";
// services
import stateService from "lib/services/state.service";
// components
import ConfirmStateDeletion from "components/project/issues/BoardView/state/confirm-state-delete";
// ui
import { Button, Input } from "ui";
// icons
import { PencilSquareIcon, PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
// constants
import { addSpaceIfCamelCase, groupBy } from "constants/common";
// types
import type { IState } from "types";
type Props = {
projectId: string | string[] | undefined;
};
const StatesSettings: React.FC<Props> = ({ projectId }) => {
const [isCreateStateModal, setIsCreateStateModal] = useState(false);
const [selectedState, setSelectedState] = useState<string | undefined>();
type CreateUpdateStateProps = {
workspaceSlug?: string;
projectId?: string;
data: IState | null;
onClose: () => void;
selectedGroup: StateGroup | null;
};
const { states } = useUser();
type StateGroup = "backlog" | "unstarted" | "started" | "completed" | "cancelled" | null;
const CreateUpdateState: React.FC<CreateUpdateStateProps> = ({
workspaceSlug,
projectId,
data,
onClose,
selectedGroup,
}) => {
const {
register,
handleSubmit,
formState: { errors },
setError,
watch,
reset,
control,
} = useForm<IState>({
defaultValues: {
name: "",
color: "#000000",
group: "backlog",
},
});
const handleClose = () => {
onClose();
reset({ name: "", color: "#000000", group: "backlog" });
};
const onSubmit = async (formData: IState) => {
if (!workspaceSlug || !projectId) return;
const payload: IState = {
...formData,
};
if (!data) {
await stateService
.createState(workspaceSlug, projectId, { ...payload, group: selectedGroup })
.then((res) => {
mutate<IState[]>(STATE_LIST(projectId), (prevData) => [...(prevData ?? []), res], false);
handleClose();
})
.catch((err) => {
Object.keys(err).map((key) => {
setError(key as keyof IState, {
message: err[key].join(", "),
});
});
});
} else {
await stateService
.updateState(workspaceSlug, projectId, data.id, {
...payload,
group: selectedGroup ?? "backlog",
})
.then((res) => {
mutate<IState[]>(
STATE_LIST(projectId),
(prevData) => {
const newData = prevData?.map((item) => {
if (item.id === res.id) {
return res;
}
return item;
});
return newData;
},
false
);
handleClose();
})
.catch((err) => {
Object.keys(err).map((key) => {
setError(key as keyof IState, {
message: err[key].join(", "),
});
});
});
}
};
useEffect(() => {
if (data === null) return;
reset(data);
}, [data]);
return (
<div className="flex items-center gap-x-2 p-2 bg-white">
<div className="w-8 h-8 border shrink-0">
<Popover className="relative w-full h-full flex justify-center items-center">
{({ open }) => (
<>
<Popover.Button
className={`group bg-white rounded-md inline-flex items-center text-base font-medium hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 ${
open ? "text-gray-900" : "text-gray-500"
}`}
>
{watch("color") && watch("color") !== "" && (
<span
className="w-4 h-4 rounded"
style={{
backgroundColor: watch("color") ?? "green",
}}
></span>
)}
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute top-full z-50 left-0 mt-3 px-2 w-screen max-w-xs sm:px-0">
<Controller
name="color"
control={control}
render={({ field: { value, onChange } }) => (
<TwitterPicker color={value} onChange={(value) => onChange(value.hex)} />
)}
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</div>
<Input
id="name"
name="name"
register={register}
placeholder="Enter state name"
validations={{
required: "Name is required",
}}
error={errors.name}
autoComplete="off"
/>
<Input
id="description"
name="description"
register={register}
placeholder="Enter state description"
error={errors.description}
autoComplete="off"
/>
<Button theme="secondary" onClick={handleClose}>
Cancel
</Button>
<Button theme="primary" onClick={handleSubmit(onSubmit)}>
Save
</Button>
</div>
);
};
const StatesSettings: React.FC<Props> = ({ projectId }) => {
const [activeGroup, setActiveGroup] = useState<StateGroup>(null);
const [selectedState, setSelectedState] = useState<string | null>(null);
const [selectDeleteState, setSelectDeleteState] = useState<string | null>(null);
const { states, activeWorkspace } = useUser();
const groupedStates: {
[key: string]: Array<IState>;
} = groupBy(states ?? [], "group");
return (
<>
<CreateUpdateStateModal
isOpen={isCreateStateModal || Boolean(selectedState)}
handleClose={() => {
setSelectedState(undefined);
setIsCreateStateModal(false);
}}
projectId={projectId as string}
data={selectedState ? states?.find((state) => state.id === selectedState) : undefined}
<ConfirmStateDeletion
isOpen={!!selectDeleteState}
data={states?.find((state) => state.id === selectDeleteState) ?? null}
onClose={() => setSelectDeleteState(null)}
/>
<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 className="flex flex-col justify-between gap-3">
{Object.keys(groupedStates).map((key) => (
<div className="w-full space-y-1" key={key}>
<div className="flex justify-between">
<p className="font-medium capitalize">{key} states</p>
<button type="button" onClick={() => setActiveGroup(key as keyof StateGroup)}>
<PlusIcon className="h-4 w-4 text-gray-600" />
</button>
</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>
{groupedStates[key]?.map((state) =>
state.id !== selectedState ? (
<div
key={state.id}
className="bg-white px-4 py-2 rounded flex justify-between items-center border"
>
<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 className="flex gap-x-2">
<button type="button" onClick={() => setSelectDeleteState(state.id)}>
<TrashIcon className="h-5 w-5 text-red-400" />
</button>
<button type="button" onClick={() => setSelectedState(state.id)}>
<PencilSquareIcon className="h-5 w-5 text-gray-400" />
</button>
</div>
</div>
) : (
<CreateUpdateState
key={state.id}
projectId={projectId as string}
onClose={() => {
setActiveGroup(null);
setSelectedState(null);
}}
workspaceSlug={activeWorkspace?.slug}
data={states?.find((state) => state.id === selectedState) ?? null}
selectedGroup={key as keyof StateGroup}
/>
)
)}
{key === activeGroup && (
<CreateUpdateState
projectId={projectId as string}
onClose={() => {
setActiveGroup(null);
setSelectedState(null);
}}
workspaceSlug={activeWorkspace?.slug}
data={null}
selectedGroup={key as keyof StateGroup}
/>
)}
</div>
))}
</div>
</section>
</>

View File

@ -8,3 +8,11 @@ export const ROLE = {
};
export const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
export const GROUP_CHOICES = {
backlog: "Backlog",
unstarted: "Unstarted",
started: "Started",
completed: "Completed",
cancelled: "Cancelled",
};