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

@ -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

View File

@ -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(
"h-6 w-6 flex-none text-gray-900 text-opacity-40",
active ? "text-opacity-100" : ""
)}
aria-hidden="true"
/> */}
<input <input
type="checkbox" type="checkbox"
{...register("issue_ids")} {...register("issue_ids")}
id={`issue-${issue.id}`} id={`issue-${issue.id}`}
value={issue.id} value={issue.id}
/> />
<label <span
htmlFor={`issue-${issue.id}`} className={`h-1.5 w-1.5 block rounded-full`}
className="ml-3 flex-auto truncate" style={{
> backgroundColor: issue.state_detail.color,
}}
/>
<span className="text-xs text-gray-500">
{activeProject?.identifier}-{issue.sequence_id}
</span>
{issue.name} {issue.name}
</label> </div>
{active && ( {active && (
<span className="ml-3 flex-none text-gray-500"> <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... Jump to...
</span> </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">
<Controller
control={control}
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"> <Button onClick={handleSubmit(handleDelete)} theme="danger" size="sm">
Delete Delete selected
</Button> </Button>
</div>
<div> <div>
<Button type="button" size="sm" onClick={handleCommandPaletteClose}> <Button type="button" size="sm" onClick={handleCommandPaletteClose}>
Close Close

View File

@ -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());

View 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;

View File

@ -1,50 +1,77 @@
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> <span>
@ -53,51 +80,48 @@ const SprintView: React.FC<Props> = ({
className={`text-gray-500 ${!open ? "transform -rotate-90" : ""}`} className={`text-gray-500 ${!open ? "transform -rotate-90" : ""}`}
/> />
</span> </span>
<h2 className="text-xl">{sprint.name}</h2> <h2 className="font-medium leading-5">{cycle.name}</h2>
<p className="font-light text-gray-500"> <p className="flex gap-2 text-xs text-gray-500">
{sprint.status === "started" <span>
? sprint.start_date {cycle.status === "started"
? `${renderShortNumericDateFormat(sprint.start_date)} - ` ? cycle.start_date
? `${renderShortNumericDateFormat(cycle.start_date)} - `
: "" : ""
: sprint.status} : cycle.status}
{sprint.end_date ? renderShortNumericDateFormat(sprint.end_date) : ""} </span>
<span>
{cycle.end_date ? renderShortNumericDateFormat(cycle.end_date) : ""}
</span>
</p> </p>
</div> </div>
</Disclosure.Button> </Disclosure.Button>
<div className="relative"> <Menu as="div" className="relative inline-block">
<Menu> <Menu.Button className="grid place-items-center rounded p-1 hover:bg-gray-100 focus:outline-none">
<Menu.Button> <EllipsisHorizontalIcon className="h-4 w-4" />
<EllipsisHorizontalIcon width="16" height="16" />
</Menu.Button> </Menu.Button>
<Menu.Items className="absolute z-20 w-28 bg-white rounded border cursor-pointer -left-24"> <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> <Menu.Item>
<div className="hover:bg-gray-100 border-b last:border-0">
<button <button
className="w-full text-left py-2 pl-2"
type="button" type="button"
onClick={() => selectSprint({ ...sprint, actionType: "edit" })} 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 Edit
</button> </button>
</div>
</Menu.Item> </Menu.Item>
<Menu.Item> <Menu.Item>
<div className="hover:bg-gray-100 border-b last:border-0">
<button <button
className="w-full text-left py-2 pl-2"
type="button" type="button"
onClick={() => selectSprint({ ...sprint, actionType: "delete" })} 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 Delete
</button> </button>
</div>
</Menu.Item> </Menu.Item>
</Menu.Items> </Menu.Items>
</Menu> </Menu>
</div> </div>
</div>
<Transition <Transition
show={open} show={open}
enter="transition duration-100 ease-out" enter="transition duration-100 ease-out"
@ -108,63 +132,111 @@ const SprintView: React.FC<Props> = ({
leaveTo="transform opacity-0" leaveTo="transform opacity-0"
> >
<Disclosure.Panel> <Disclosure.Panel>
<div className="space-y-3"> <StrictModeDroppable droppableId={cycle.id}>
{sprintIssues ? ( {(provided) => (
sprintIssues.length > 0 ? ( <div ref={provided.innerRef} {...provided.droppableProps}>
sprintIssues.map((issue) => ( {cycleIssues ? (
<div cycleIssues.length > 0 ? (
cycleIssues.map((issue, index) => (
<Draggable
key={issue.id} key={issue.id}
className="p-4 bg-white border border-gray-200 rounded flex items-center justify-between" 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 <button
type="button" type="button"
onClick={() => className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 rotate-90 outline-none`}
router.push( {...provided.dragHandleProps}
`/projects/${projectId}/issues/${issue.issue_details.id}`
)
}
> >
<p>{issue.issue_details.name}</p> <EllipsisHorizontalIcon className="h-4 w-4 text-gray-600" />
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600 mt-[-0.7rem]" />
</button> </button>
<div className="flex items-center gap-x-4">
<span <span
className="text-black rounded px-2 py-0.5 text-sm border" 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={{ style={{
backgroundColor: `${issue.issue_details.state_detail?.color}20`, backgroundColor: `${issue.issue_details.state_detail?.color}20`,
borderColor: issue.issue_details.state_detail?.color, border: `2px solid ${issue.issue_details.state_detail?.color}`,
}} }}
> >
{issue.issue_details.state_detail?.name} {issue.issue_details.state_detail?.name}
</span> </span>
<div className="relative"> <Menu as="div" className="relative">
<Menu> <Menu.Button
<Menu.Button> as="button"
<EllipsisHorizontalIcon width="16" height="16" /> 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.Button>
<Menu.Items className="absolute z-20 w-28 bg-white rounded border cursor-pointer -left-24"> <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> <Menu.Item>
<div className="hover:bg-gray-100 border-b last:border-0">
<button <button
className="w-full text-left py-2 pl-2" className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
type="button" type="button"
onClick={() => onClick={() =>
openIssueModal(sprint.id, issue.issue_details, "edit") openIssueModal(cycle.id, issue.issue_details, "edit")
} }
> >
Edit Edit
</button> </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> </div>
</Menu.Item> </Menu.Item>
<Menu.Item> <Menu.Item>
<div className="hover:bg-gray-100 border-b last:border-0"> <div className="hover:bg-gray-100 border-b last:border-0">
<button <button
className="w-full text-left py-2 pl-2" className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
type="button" type="button"
onClick={() => onClick={() =>
openIssueModal(sprint.id, issue.issue_details, "delete") openIssueModal(
cycle.id,
issue.issue_details,
"delete"
)
} }
> >
Delete Delete permanently
</button> </button>
</div> </div>
</Menu.Item> </Menu.Item>
@ -172,43 +244,28 @@ const SprintView: React.FC<Props> = ({
</Menu> </Menu>
</div> </div>
</div> </div>
</div> )}
</Draggable>
)) ))
) : ( ) : (
<p className="text-sm text-gray-500">This cycle has no issues.</p> <p className="text-sm text-gray-500">This cycle has no issue.</p>
) )
) : ( ) : (
<div className="w-full h-full flex items-center justify-center"> <div className="w-full h-full flex items-center justify-center">
<Spinner /> <Spinner />
</div> </div>
)} )}
{provided.placeholder}
</div> </div>
)}
</StrictModeDroppable>
</Disclosure.Panel> </Disclosure.Panel>
</Transition> </Transition>
<div className="flex flex-col gap-y-2"> <Menu as="div" className="relative inline-block">
<button <Menu.Button className="flex items-center gap-1 px-2 py-1 rounded hover:bg-gray-100 text-xs font-medium">
className="text-indigo-600 flex items-center gap-x-2" <PlusIcon className="h-3 w-3" />
onClick={() => openIssueModal(sprint.id)} Add issue
>
<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> </Menu.Button>
</div>
<Transition <Transition
as={React.Fragment} as={React.Fragment}
@ -219,40 +276,39 @@ const SprintView: React.FC<Props> = ({
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="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"> <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="py-1"> <div className="p-1">
{projectIssues?.results.map((issue) => ( <Menu.Item as="div">
<Menu.Item {(active) => (
key={issue.id} <button
as="div" type="button"
onClick={() => { className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
addIssueToSprint(sprint.id, issue.id); onClick={() => openIssueModal(cycle.id)}
}}
> >
{({ active }) => ( Create new
<p </button>
className={classNames( )}
active ? "bg-gray-100 text-gray-900" : "text-gray-700", </Menu.Item>
"block px-4 py-2 text-sm" <Menu.Item as="div">
)} {(active) => (
> <button
{issue.name} type="button"
</p> className="p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap"
onClick={() => setCycleIssuesListModal(true)}
>
Add an existing issue
</button>
)} )}
</Menu.Item> </Menu.Item>
))}
</div> </div>
</Menu.Items> </Menu.Items>
</Transition> </Transition>
</Menu> </Menu>
</div> </div>
</div>
</div>
</div>
)} )}
</Disclosure> </Disclosure>
</div> </>
); );
}; };
export default SprintView; export default CycleView;

View File

@ -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 ? (

View File

@ -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={isIssueListModalOpen} isOpen={isOpen}
handleClose={() => setIsIssueListModalOpen(false)} handleClose={() => setIsOpen(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>
</>
)} )}
/> />
); );

View File

@ -21,7 +21,6 @@ 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"
@ -51,8 +50,7 @@ const SelectState: React.FC<Props> = ({ control, setIsOpen }) => {
} }
/> />
)} )}
></Controller> />
</>
); );
}; };

View File

@ -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>

View File

@ -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>
))} ))}

View File

@ -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>
))} ))}

View File

@ -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

View File

@ -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}

View 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;

View 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;

View 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;

View 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;

View File

@ -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}/`;

View File

@ -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]
); );

View File

@ -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,11 +552,14 @@ 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 ${
sidebarCollapse ? "flex-col-reverse" : ""
}`}
>
<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 focus:bg-gray-100 focus:text-gray-900 outline-none ${ 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" : "" sidebarCollapse ? "justify-center w-full" : ""
}`} }`}
onClick={() => toggleCollapsed()} onClick={() => toggleCollapsed()}
@ -568,9 +570,11 @@ const Sidebar: React.FC = () => {
}`} }`}
/> />
</button> </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={() => { onClick={() => {
const e = new KeyboardEvent("keydown", { const e = new KeyboardEvent("keydown", {
ctrlKey: true, ctrlKey: true,

View File

@ -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;
}) })

View File

@ -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,

View File

@ -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;

View File

@ -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,7 +196,11 @@ const MyIssues: NextPage = () => {
</tr> </tr>
</thead> </thead>
<tbody className="bg-white"> <tbody className="bg-white">
{myIssues.map((myIssue, index) => ( {myIssues
.filter((i) =>
selectedWorkspace ? i.workspace === selectedWorkspace : true
)
.map((myIssue, index) => (
<tr <tr
key={myIssue.id} key={myIssue.id}
className={classNames( className={classNames(
@ -141,12 +209,14 @@ const MyIssues: NextPage = () => {
)} )}
> >
<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 text-sm font-medium text-gray-900 hover:text-theme max-w-[15rem] duration-300">
<Link href={`/projects/${myIssue.project}/issues/${myIssue.id}`}> <Link
href={`/projects/${myIssue.project}/issues/${myIssue.id}`}
>
<a>{myIssue.name}</a> <a>{myIssue.name}</a>
</Link> </Link>
</td> </td>
<td className="px-3 py-4 max-w-[15rem] truncate"> <td className="px-3 py-4 max-w-[15rem] truncate">
{myIssue.description} {/* {myIssue.description} */}
</td> </td>
<td className="px-3 py-4"> <td className="px-3 py-4">
{myIssue.project_detail?.name} {myIssue.project_detail?.name}

View File

@ -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,11 +212,12 @@ 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">
<DragDropContext onDragEnd={handleDragEnd}>
{cycles.map((cycle) => ( {cycles.map((cycle) => (
<CycleView <CycleView
key={cycle.id} key={cycle.id}
sprint={cycle} cycle={cycle}
selectSprint={setSelectedSprint} selectSprint={setSelectedSprint}
projectId={projectId as string} projectId={projectId as string}
workspaceSlug={activeWorkspace?.slug as string} workspaceSlug={activeWorkspace?.slug as string}
@ -154,6 +225,7 @@ const ProjectSprints: NextPage = () => {
addIssueToSprint={addIssueToSprint} addIssueToSprint={addIssueToSprint}
/> />
))} ))}
</DragDropContext>
</div> </div>
</div> </div>
) : ( ) : (

View File

@ -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,23 +435,26 @@ 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}
</span>
</a>
</Link>
<div> <div>
<Menu as="div" className="relative inline-block"> <Menu as="div" className="relative inline-block">
<Menu.Button className="grid relative place-items-center focus:outline-none"> <Menu.Button className="grid relative place-items-center focus:outline-none">
@ -417,7 +476,7 @@ const IssueDetail: NextPage = () => {
{(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>
@ -428,8 +487,7 @@ const IssueDetail: NextPage = () => {
</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>

View File

@ -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,14 +134,13 @@ 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"> <Tab.List className="flex items-center gap-x-4 gap-y-2 flex-wrap mb-8">
{["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 duration-300 ${ `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" : "" selected ? "bg-theme text-white" : ""
}` }`
} }
@ -175,357 +149,23 @@ const ProjectSettings: NextPage = () => {
</Tab> </Tab>
))} ))}
</Tab.List> </Tab.List>
<Tab.Panels className="mt-8"> <Tab.Panels>
<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> </Tab.Panel>
<h3 className="text-lg font-medium leading-6 text-gray-900">Control</h3> </form>
<p className="mt-1 text-sm text-gray-500">Set the control for the project.</p> <Tab.Panel>
</div> <StatesSettings projectId={projectId} />
<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> <Tab.Panel>
<section className="space-y-5"> <LabelsSettings />
<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.Panel>
</Tab.Panels> </Tab.Panels>
</Tab.Group> </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">

View File

@ -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;

View File

@ -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">

View File

@ -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}>