feat: modules, style: cycles, all menus

This commit is contained in:
Aaryan Khandelwal 2022-12-16 21:50:09 +05:30
parent 830af71474
commit 278fd6cdd0
49 changed files with 1863 additions and 1530 deletions

View File

@ -24,7 +24,7 @@ import {
import ShortcutsModal from "components/command-palette/shortcuts";
import CreateProjectModal from "components/project/create-project-modal";
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal";
import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal";
// ui
import { Button } from "ui";
// types
@ -33,6 +33,7 @@ import { IIssue, IssueResponse } from "types";
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
// constants
import { classNames, copyTextToClipboard } from "constants/common";
import CreateUpdateModuleModal from "components/project/modules/create-update-module-modal";
type FormInput = {
issue_ids: string[];
@ -47,6 +48,7 @@ const CommandPalette: React.FC = () => {
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
const [isShortcutsModalOpen, setIsShortcutsModalOpen] = useState(false);
const [isCreateCycleModalOpen, setIsCreateCycleModalOpen] = useState(false);
const [isCreateModuleModalOpen, setisCreateModuleModalOpen] = useState(false);
const { activeWorkspace, activeProject, issues } = useUser();
@ -109,6 +111,9 @@ const CommandPalette: React.FC = () => {
} else if ((e.ctrlKey || e.metaKey) && e.key === "q") {
e.preventDefault();
setIsCreateCycleModalOpen(true);
} else if ((e.ctrlKey || e.metaKey) && e.key === "m") {
e.preventDefault();
setisCreateModuleModalOpen(true);
} else if ((e.ctrlKey || e.metaKey) && e.altKey && e.key === "c") {
e.preventDefault();
@ -184,11 +189,18 @@ const CommandPalette: React.FC = () => {
<ShortcutsModal isOpen={isShortcutsModalOpen} setIsOpen={setIsShortcutsModalOpen} />
<CreateProjectModal isOpen={isProjectModalOpen} setIsOpen={setIsProjectModalOpen} />
{activeProject && (
<>
<CreateUpdateCycleModal
isOpen={isCreateCycleModalOpen}
setIsOpen={setIsCreateCycleModalOpen}
projectId={activeProject.id}
/>
<CreateUpdateModuleModal
isOpen={isCreateModuleModalOpen}
setIsOpen={setisCreateModuleModalOpen}
projectId={activeProject.id}
/>
</>
)}
<CreateUpdateIssuesModal
isOpen={isIssueModalOpen}

View File

@ -74,6 +74,7 @@ const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
{ keys: "ctrl,p", description: "To create project" },
{ keys: "ctrl,i", description: "To create issue" },
{ keys: "ctrl,q", description: "To create cycle" },
{ keys: "ctrl,m", description: "To create module" },
{ keys: "ctrl,h", description: "To open shortcuts guide" },
{
keys: "ctrl,alt,c",

View File

@ -14,8 +14,8 @@ type Props = {
selectedGroup: NestedKeyOf<IIssue> | null;
members: IProjectMember[] | undefined;
openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void;
openIssuesListModal: (cycleId: string) => void;
removeIssueFromCycle: (cycleId: string, bridgeId: string) => void;
openIssuesListModal: () => void;
removeIssueFromCycle: (bridgeId: string) => void;
};
const CyclesBoardView: React.FC<Props> = ({

View File

@ -50,8 +50,8 @@ type Props = {
createdBy: string | null;
bgColor?: string;
openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void;
openIssuesListModal: (cycleId: string) => void;
removeIssueFromCycle: (cycleId: string, bridgeId: string) => void;
openIssuesListModal: () => void;
removeIssueFromCycle: (bridgeId: string) => void;
};
const SingleCycleBoard: React.FC<Props> = ({
@ -106,12 +106,6 @@ const SingleCycleBoard: React.FC<Props> = ({
backgroundColor: `${bgColor}20`,
}}
>
<span
className={`w-3 h-3 block rounded-full ${!show ? "" : "mr-1"}`}
style={{
backgroundColor: Boolean(bgColor) ? bgColor : undefined,
}}
/>
<h2
className={`text-[0.9rem] font-medium capitalize`}
style={{
@ -143,13 +137,13 @@ const SingleCycleBoard: React.FC<Props> = ({
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
<div className="p-1">
<Menu.Items className="absolute right-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10 text-xs">
<div className="py-1">
<Menu.Item as="div">
{(active) => (
<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"
className="w-full text-left p-2 text-gray-900 hover:bg-indigo-50 whitespace-nowrap"
onClick={() => openCreateIssueModal()}
>
Create new
@ -160,8 +154,8 @@ const SingleCycleBoard: React.FC<Props> = ({
{(active) => (
<button
type="button"
className="p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap"
// onClick={() => openIssuesListModal()}
className="w-full text-left p-2 text-gray-900 hover:bg-indigo-50 whitespace-nowrap"
onClick={() => openIssuesListModal()}
>
Add an existing issue
</button>

View File

@ -145,7 +145,7 @@ const CycleIssuesListModal: React.FC<Props> = ({
)}
<ul className="text-sm text-gray-700">
{filteredIssues.map((issue) => {
// if (issue.cycle !== cycleId)
if (!issue.issue_cycle)
return (
<Combobox.Option
key={issue.id}

View File

@ -11,7 +11,7 @@ import cycleServices from "lib/services/cycles.service";
// hooks
import useUser from "lib/hooks/useUser";
// ui
import { Spinner } from "ui";
import { CustomMenu, Spinner } from "ui";
// icons
import { PlusIcon, EllipsisHorizontalIcon, ChevronDownIcon } from "@heroicons/react/20/solid";
import { CalendarDaysIcon } from "@heroicons/react/24/outline";
@ -35,7 +35,7 @@ type Props = {
selectedGroup: NestedKeyOf<IIssue> | null;
openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void;
openIssuesListModal: (cycleId: string) => void;
removeIssueFromCycle: (cycleId: string, bridgeId: string) => void;
removeIssueFromCycle: (bridgeId: string) => void;
};
const CyclesListView: React.FC<Props> = ({
@ -241,51 +241,19 @@ const CyclesListView: React.FC<Props> = ({
</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
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
type="button"
<CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem
onClick={() => openCreateIssueModal(issue, "edit")}
>
Edit
</button>
</Menu.Item>
<Menu.Item>
<div className="hover:bg-gray-100 border-b last:border-0">
<button
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
type="button"
// onClick={() =>
// removeIssueFromCycle(issue.cycle, issue.id)
// }
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => removeIssueFromCycle(issue.bridge ?? "")}
>
Remove from cycle
</button>
</div>
</Menu.Item>
<Menu.Item>
<div className="hover:bg-gray-100 border-b last:border-0">
<button
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
type="button"
// onClick={() =>
// openCreateIssueModal(cycle.id, issue, "delete")
// }
>
Delete permanently
</button>
</div>
</Menu.Item>
</Menu.Items>
</Menu>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem>Delete permanently</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
);

View File

@ -1,20 +1,53 @@
// next
import Link from "next/link";
// hooks
import useUser from "lib/hooks/useUser";
// react
import { useState } from "react";
// components
import SingleStat from "components/project/cycles/stats-view/single-stat";
import ConfirmCycleDeletion from "components/project/cycles/confirm-cycle-deletion";
// types
import { ICycle } from "types";
import SingleStat from "./single-stat";
import { ICycle, SelectSprintType } from "types";
type Props = {
cycles: ICycle[];
setCreateUpdateCycleModal: React.Dispatch<React.SetStateAction<boolean>>;
setSelectedCycle: React.Dispatch<React.SetStateAction<SelectSprintType>>;
};
const CycleStatsView: React.FC<Props> = ({ cycles }) => {
const CycleStatsView: React.FC<Props> = ({
cycles,
setCreateUpdateCycleModal,
setSelectedCycle,
}) => {
const [selectedCycleForDelete, setSelectedCycleForDelete] = useState<SelectSprintType>();
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
const handleDeleteCycle = (cycle: ICycle) => {
setSelectedCycleForDelete({ ...cycle, actionType: "delete" });
setCycleDeleteModal(true);
};
const handleEditCycle = (cycle: ICycle) => {
setSelectedCycle({ ...cycle, actionType: "edit" });
setCreateUpdateCycleModal(true);
};
return (
<>
<ConfirmCycleDeletion
isOpen={
cycleDeleteModal &&
!!selectedCycleForDelete &&
selectedCycleForDelete.actionType === "delete"
}
setIsOpen={setCycleDeleteModal}
data={selectedCycleForDelete}
/>
{cycles.map((cycle) => (
<SingleStat key={cycle.id} cycle={cycle} />
<SingleStat
key={cycle.id}
cycle={cycle}
handleDeleteCycle={() => handleDeleteCycle(cycle)}
handleEditCycle={() => handleEditCycle(cycle)}
/>
))}
</>
);

View File

@ -1,37 +1,46 @@
// react
import React, { useState } from "react";
// next
import Link from "next/link";
import Image from "next/image";
// swr
import useSWR from "swr";
// services
import cyclesService from "lib/services/cycles.service";
// hooks
import useUser from "lib/hooks/useUser";
// ui
import { Button, CustomMenu } from "ui";
// types
import { CycleIssueResponse, ICycle } from "types";
// fetch-keys
import { CYCLE_ISSUES } from "constants/fetch-keys";
import { groupBy, renderShortNumericDateFormat } from "constants/common";
import {
CheckIcon,
ExclamationCircleIcon,
ExclamationTriangleIcon,
} from "@heroicons/react/24/outline";
import { ArrowPathIcon, CheckIcon, UserIcon } from "@heroicons/react/24/outline";
import { CalendarDaysIcon } from "@heroicons/react/20/solid";
import { useRouter } from "next/router";
type Props = { cycle: ICycle };
const stateGroupIcons: {
[key: string]: JSX.Element;
} = {
backlog: <ExclamationTriangleIcon className="h-4 w-4" />,
unstarted: <CheckIcon className="h-4 w-4" />,
started: <CheckIcon className="h-4 w-4" />,
cancelled: <ExclamationCircleIcon className="h-4 w-4" />,
completed: <CheckIcon className="h-4 w-4" />,
type Props = {
cycle: ICycle;
handleEditCycle: () => void;
handleDeleteCycle: () => void;
};
const SingleStat: React.FC<Props> = ({ cycle }) => {
const stateGroupColours: {
[key: string]: string;
} = {
backlog: "#3f76ff",
unstarted: "#ff9e9e",
started: "#d687ff",
cancelled: "#ff5353",
completed: "#096e8d",
};
const SingleStat: React.FC<Props> = ({ cycle, handleEditCycle, handleDeleteCycle }) => {
const { activeWorkspace, activeProject } = useUser();
const router = useRouter();
const { data: cycleIssues } = useSWR<CycleIssueResponse[]>(
activeWorkspace && activeProject && cycle.id ? CYCLE_ISSUES(cycle.id as string) : null,
activeWorkspace && activeProject && cycle.id
@ -48,7 +57,6 @@ const SingleStat: React.FC<Props> = ({ cycle }) => {
...groupBy(cycleIssues ?? [], "issue_details.state_detail.group"),
};
// status calculator
const startDate = new Date(cycle.start_date ?? "");
const endDate = new Date(cycle.end_date ?? "");
const today = new Date();
@ -56,44 +64,111 @@ const SingleStat: React.FC<Props> = ({ cycle }) => {
return (
<>
<div className="bg-white p-3">
<div className="flex justify-between items-center gap-2">
<div className="flex items-center gap-2">
<div className="grid grid-cols-8 gap-2 divide-x">
<div className="col-span-3 space-y-3">
<Link href={`/projects/${activeProject?.id}/cycles/${cycle.id}`}>
<a className="block rounded-md">{cycle.name}</a>
</Link>
<div className="text-xs text-gray-500">
<span>{renderShortNumericDateFormat(startDate)}</span>
{" - "}
<span>{renderShortNumericDateFormat(endDate)}</span>
</div>
</div>
<div className="text-xs bg-gray-100 px-2 py-1 rounded">
<a className="flex justify-between items-center">
<h2 className="font-medium">{cycle.name}</h2>
<div className="flex items-center gap-2">
<span className="text-xs bg-gray-100 px-2 py-1 rounded-xl">
{today.getDate() < startDate.getDate()
? "Not started"
: today.getDate() > endDate.getDate()
? "Over"
: "Active"}
</span>
<CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem onClick={handleEditCycle}>Edit cycle</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleDeleteCycle}>
Delete cycle permanently
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</a>
</Link>
<div className="grid grid-cols-2 gap-x-2 gap-y-3 text-xs">
<div className="flex items-center gap-2 text-gray-500">
<CalendarDaysIcon className="h-4 w-4" />
Cycle dates
</div>
<div>
{renderShortNumericDateFormat(startDate)} - {renderShortNumericDateFormat(endDate)}
</div>
<div className="flex items-center gap-2 text-gray-500">
<UserIcon className="h-4 w-4" />
Created by
</div>
<div className="flex items-center gap-2">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
<Image
src={cycle.owned_by.avatar}
height={16}
width={16}
className="rounded-full"
alt={cycle.owned_by.first_name}
/>
) : (
<span className="h-5 w-5 capitalize bg-gray-700 text-white grid place-items-center rounded-full">
{cycle.owned_by.first_name.charAt(0)}
</span>
)}
{cycle.owned_by.first_name}
</div>
<div className="flex items-center gap-2 text-gray-500">
<CalendarDaysIcon className="h-4 w-4" />
Active members
</div>
<div></div>
</div>
<div className="flex items-center gap-2">
<Button theme="secondary" className="flex items-center gap-2" disabled>
<CheckIcon className="h-3 w-3" />
Participating
</Button>
<Button
theme="secondary"
className="flex items-center gap-2"
onClick={() => router.push(`/projects/${activeProject?.id}/cycles/${cycle.id}`)}
>
<ArrowPathIcon className="h-3 w-3" />
Open Cycle
</Button>
</div>
</div>
<div className="text-sm mt-6 mb-4 space-y-2">
<div className="grid grid-cols-5 gap-4">
<div className="col-span-2 px-5 space-y-3">
<h4 className="text-sm tracking-widest">PROGRESS</h4>
<div className="text-xs space-y-3">
{Object.keys(groupedIssues).map((group) => {
return (
<div key={group} className="bg-gray-100 rounded-md p-3 flex items-center gap-3">
<div>
<div className="h-8 w-8 rounded-full grid place-items-center bg-green-500 text-white">
{stateGroupIcons[group]}
</div>
<div key={group} className="flex items-center gap-2">
<div className="flex items-center gap-2 basis-2/3">
<span
className="h-2 w-2 block rounded-full"
style={{
backgroundColor: stateGroupColours[group],
}}
></span>
<h6 className="capitalize text-xs">{group}</h6>
</div>
<div>
<h5 className="capitalize">{group}</h5>
<span>{groupedIssues[group].length}</span>
<span>
{groupedIssues[group].length}{" "}
<span className="text-gray-500">
-{" "}
{cycleIssues && cycleIssues.length > 0
? `${(groupedIssues[group].length / cycleIssues.length) * 100}%`
: "0%"}
</span>
</span>
</div>
</div>
);
})}
</div>
</div>
<div className="col-span-3"></div>
</div>
</div>
</>
);

View File

@ -130,12 +130,6 @@ const SingleBoard: React.FC<Props> = ({
backgroundColor: `${bgColor}20`,
}}
>
<span
className={`w-3 h-3 block rounded-full ${!show ? "" : "mr-1"}`}
style={{
backgroundColor: Boolean(bgColor) ? bgColor : undefined,
}}
/>
<h2
className={`text-[0.9rem] font-medium capitalize`}
style={{

View File

@ -6,7 +6,7 @@ import { Listbox, Transition } from "@headlessui/react";
// hooks
import useUser from "lib/hooks/useUser";
// components
import CreateUpdateSprintsModal from "components/project/cycles/CreateUpdateCyclesModal";
import CreateUpdateSprintsModal from "components/project/cycles/create-update-cycle-modal";
// icons
import { CheckIcon, ChevronDownIcon, PlusIcon } from "@heroicons/react/20/solid";
// types

View File

@ -8,7 +8,7 @@ import type { IIssue, IssueResponse } from "types";
// icons
import { UserIcon } from "@heroicons/react/24/outline";
// components
import IssuesListModal from "components/project/issues/IssuesListModal";
import IssuesListModal from "components/project/issues/issues-list-modal";
type Props = {
control: Control<IIssue, any>;

View File

@ -33,7 +33,7 @@ import SelectPriority from "./SelectPriority";
import SelectAssignee from "./SelectAssignee";
import SelectParent from "./SelectParentIssue";
import CreateUpdateStateModal from "components/project/issues/BoardView/state/CreateUpdateStateModal";
import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal";
import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal";
// types
import type { IIssue, IssueResponse, CycleIssueResponse } from "types";
import { EllipsisHorizontalIcon } from "@heroicons/react/24/outline";

View File

@ -1,180 +0,0 @@
// react
import React, { useState } from "react";
// headless ui
import { Combobox, Dialog, Transition } from "@headlessui/react";
// ui
import { Button } from "ui";
// icons
import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
// types
import { IIssue } from "types";
import { classNames } from "constants/common";
import useUser from "lib/hooks/useUser";
type Props = {
isOpen: boolean;
handleClose: () => void;
value?: any;
onChange: (...event: any[]) => void;
issues: IIssue[];
title?: string;
multiple?: boolean;
customDisplay?: JSX.Element;
};
const IssuesListModal: React.FC<Props> = ({
isOpen,
handleClose: onClose,
value,
onChange,
issues,
title = "Issues",
multiple = false,
customDisplay,
}) => {
const [query, setQuery] = useState("");
const [values, setValues] = useState<string[]>([]);
const { activeProject } = useUser();
const handleClose = () => {
onClose();
setQuery("");
setValues([]);
};
const filteredIssues: IIssue[] =
query === ""
? issues ?? []
: issues?.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">
<Combobox
value={value}
onChange={(val) => {
if (multiple) setValues(val);
else onChange(val);
}}
// multiple={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)}
displayValue={() => ""}
/>
</div>
<div className="p-3">{customDisplay}</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">
{title}
</h2>
)}
<ul className="text-sm text-gray-700">
{filteredIssues.map((issue) => (
<Combobox.Option
key={issue.id}
value={issue.id}
className={({ active }) =>
classNames(
"flex items-center gap-2 cursor-pointer select-none rounded-md px-3 py-2",
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
)
}
onClick={() => {
if (!multiple) handleClose();
}}
>
{({ selected }) => (
<>
{multiple ? (
<input type="checkbox" checked={selected} readOnly />
) : null}
<span
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<span className="flex-shrink-0 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>
{multiple ? (
<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={() => onChange(values)}>
Add to Cycle
</Button>
</div>
) : null}
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
</>
);
};
export default IssuesListModal;

View File

@ -6,33 +6,23 @@ import { Listbox, Transition } from "@headlessui/react";
// react hook form
import { useForm, Controller, UseFormWatch } from "react-hook-form";
// services
import stateServices from "lib/services/state.service";
import issuesServices from "lib/services/issues.service";
import workspaceService from "lib/services/workspace.service";
// hooks
import useUser from "lib/hooks/useUser";
import useToast from "lib/hooks/useToast";
// components
import IssuesListModal from "components/project/issues/IssuesListModal";
// fetching keys
import { STATE_LIST, WORKSPACE_MEMBERS, PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
// commons
import { classNames, copyTextToClipboard } from "constants/common";
import { PRIORITIES } from "constants/";
import { copyTextToClipboard } from "constants/common";
// ui
import { Input, Button, Spinner } from "ui";
import { Popover } from "@headlessui/react";
// icons
import {
UserIcon,
TagIcon,
UserGroupIcon,
ChevronDownIcon,
Squares2X2Icon,
ChartBarIcon,
ClipboardDocumentIcon,
LinkIcon,
ArrowPathIcon,
CalendarDaysIcon,
TrashIcon,
PlusIcon,
@ -40,7 +30,7 @@ import {
} from "@heroicons/react/24/outline";
// types
import type { Control } from "react-hook-form";
import type { IIssue, IIssueLabels, IssueResponse, IState, NestedKeyOf } from "types";
import type { IIssue, IIssueLabels, NestedKeyOf } from "types";
import { TwitterPicker } from "react-color";
import { positionEditorElement } from "components/lexical/helpers/editor";
import SelectState from "./select-state";
@ -48,6 +38,8 @@ import SelectPriority from "./select-priority";
import SelectParent from "./select-parent";
import SelectCycle from "./select-cycle";
import SelectAssignee from "./select-assignee";
import SelectBlocker from "./select-blocker";
import SelectBlocked from "./select-blocked";
type Props = {
control: Control<IIssue, any>;
@ -69,11 +61,9 @@ const IssueDetailSidebar: React.FC<Props> = ({
watch: watchIssue,
setDeleteIssueModal,
}) => {
const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false);
const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false);
const [createLabelForm, setCreateLabelForm] = useState(false);
const { activeWorkspace, activeProject, cycles, issues } = useUser();
const { activeWorkspace, activeProject, issues } = useUser();
const { setToastAlert } = useToast();
@ -106,43 +96,6 @@ const IssueDetailSidebar: React.FC<Props> = ({
});
};
const sidebarSections: Array<
Array<{
label: string;
name: NestedKeyOf<IIssue>;
canSelectMultipleOptions: boolean;
icon: (props: any) => JSX.Element;
options?: Array<{ label: string; value: any; color?: string }>;
modal: boolean;
issuesList?: Array<IIssue>;
isOpen?: boolean;
setIsOpen?: (arg: boolean) => void;
}>
> = [
[
// {
// label: "Blocker",
// name: "blockers_list",
// canSelectMultipleOptions: true,
// icon: UserIcon,
// issuesList: issues?.results.filter((i) => i.id !== issueDetail?.id) ?? [],
// modal: true,
// isOpen: isBlockerModalOpen,
// setIsOpen: setIsBlockerModalOpen,
// },
// {
// label: "Blocked",
// name: "blocked_list",
// canSelectMultipleOptions: true,
// icon: UserIcon,
// issuesList: issues?.results.filter((i) => i.id !== issueDetail?.id) ?? [],
// modal: true,
// isOpen: isBlockedModalOpen,
// setIsOpen: setIsBlockedModalOpen,
// },
],
];
const handleCycleChange = (cycleId: string) => {
if (activeWorkspace && activeProject && issueDetail)
issuesServices.addIssueToCycle(activeWorkspace.slug, activeProject.id, cycleId, {
@ -150,6 +103,8 @@ const IssueDetailSidebar: React.FC<Props> = ({
});
};
console.log(issueDetail);
return (
<>
<div className="h-full w-full divide-y-2 divide-gray-100">
@ -215,7 +170,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
<div className="py-1">
<SelectState control={control} submitChanges={submitChanges} />
<SelectAssignee control={control} submitChanges={submitChanges} />
<SelectPriority control={control} submitChanges={submitChanges} />
<SelectPriority control={control} submitChanges={submitChanges} watch={watchIssue} />
</div>
<div className="py-1">
<SelectParent
@ -245,7 +200,17 @@ const IssueDetailSidebar: React.FC<Props> = ({
</div>
)
}
watchIssue={watchIssue}
watch={watchIssue}
/>
<SelectBlocker
submitChanges={submitChanges}
issuesList={issues?.results.filter((i) => i.id !== issueDetail?.id) ?? []}
watch={watchIssue}
/>
<SelectBlocked
submitChanges={submitChanges}
issuesList={issues?.results.filter((i) => i.id !== issueDetail?.id) ?? []}
watch={watchIssue}
/>
<div className="flex items-center py-2 flex-wrap">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
@ -274,72 +239,6 @@ const IssueDetailSidebar: React.FC<Props> = ({
<div className="py-1">
<SelectCycle control={control} handleCycleChange={handleCycleChange} />
</div>
{/* {sidebarSections.map((section, index) => (
<div key={index} className="py-1">
{section.map((item) => (
<div key={item.label} className="flex items-center py-2 flex-wrap">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
<item.icon className="flex-shrink-0 h-4 w-4" />
<p>{item.label}</p>
</div>
<div className="sm:basis-1/2">
<Controller
control={control}
name={item.name as keyof IIssue}
render={({ field: { value, onChange } }) => (
<>
<IssuesListModal
isOpen={Boolean(item?.isOpen)}
handleClose={() => item.setIsOpen && item.setIsOpen(false)}
onChange={(val) => {
console.log(val);
submitChanges({ [item.name]: val });
onChange(val);
}}
issues={item?.issuesList ?? []}
title={`Select ${item.label}`}
multiple={item.canSelectMultipleOptions}
value={value}
customDisplay={
issueDetail?.parent_detail ? (
<button
type="button"
className="flex items-center gap-2 bg-gray-100 px-3 py-2 text-xs rounded"
onClick={() => submitChanges({ parent: null })}
>
{issueDetail.parent_detail?.name}
<XMarkIcon className="h-3 w-3" />
</button>
) : (
<div className="inline-block bg-gray-100 px-3 py-2 text-xs rounded">
No parent selected
</div>
)
}
/>
<button
type="button"
className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
onClick={() => item.setIsOpen && item.setIsOpen(true)}
>
{watchIssue(`${item.name as keyof IIssue}`) &&
watchIssue(`${item.name as keyof IIssue}`) !== ""
? `${activeProject?.identifier}-
${
issues?.results.find(
(i) => i.id === watchIssue(`${item.name as keyof IIssue}`)
)?.sequence_id
}`
: `Select ${item.label}`}
</button>
</>
)}
/>
</div>
</div>
))}
</div>
))} */}
</div>
<div className="pt-3 space-y-3">
<div className="flex justify-between items-start">
@ -376,7 +275,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
as="div"
value={value}
multiple
onChange={(val) => submitChanges({ labels_list: val })}
onChange={(val: any) => submitChanges({ labels_list: val })}
className="flex-shrink-0"
>
{({ open }) => (
@ -395,7 +294,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 right-0 mt-1 w-40 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">
<div className="p-1">
<div className="py-1">
{issueLabels ? (
issueLabels.length > 0 ? (
issueLabels.map((label: IIssueLabels) => (
@ -403,10 +302,8 @@ const IssueDetailSidebar: React.FC<Props> = ({
key={label.id}
className={({ active, selected }) =>
`${
active || selected
? "text-white bg-theme"
: "text-gray-900"
} flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate`
active || selected ? "bg-indigo-50" : ""
} flex items-center gap-2 text-gray-900 cursor-pointer select-none relative p-2 truncate`
}
value={label.id}
>

View File

@ -126,7 +126,7 @@ const SelectAssignee: React.FC<Props> = ({ control, submitChanges }) => {
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 right-0 mt-1 w-40 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">
<div className="p-1">
<div className="py-1">
{people ? (
people.length > 0 ? (
people.map((option) => (
@ -134,8 +134,8 @@ const SelectAssignee: React.FC<Props> = ({ control, submitChanges }) => {
key={option.member.id}
className={({ active, selected }) =>
`${
active || selected ? "text-white bg-theme" : "text-gray-900"
} flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate`
active || selected ? "bg-indigo-50" : ""
} flex items-center gap-2 text-gray-900 cursor-pointer select-none relative p-2 rounded-md truncate`
}
value={option.member.id}
>

View File

@ -0,0 +1,238 @@
// react
import React, { useState } from "react";
// react-hook-form
import { SubmitHandler, useForm, UseFormWatch } from "react-hook-form";
// hooks
import useUser from "lib/hooks/useUser";
import useToast from "lib/hooks/useToast";
// headless ui
import { Combobox, Dialog, Transition } from "@headlessui/react";
// ui
import { Button } from "ui";
// icons
import {
FolderIcon,
MagnifyingGlassIcon,
UserGroupIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
// types
import { IIssue } from "types";
// constants
import { classNames } from "constants/common";
type FormInput = {
issue_ids: string[];
};
type Props = {
submitChanges: (formData: Partial<IIssue>) => void;
issuesList: IIssue[];
watch: UseFormWatch<IIssue>;
};
const SelectBlocked: React.FC<Props> = ({ submitChanges, issuesList, watch }) => {
const [query, setQuery] = useState("");
const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false);
const { activeProject, issues } = useUser();
const { setToastAlert } = useToast();
const { register, handleSubmit, reset, watch: watchIssues } = useForm<FormInput>();
const handleClose = () => {
setIsBlockedModalOpen(false);
reset();
};
const onSubmit: SubmitHandler<FormInput> = (data) => {
if (!data.issue_ids || data.issue_ids.length === 0) {
setToastAlert({
title: "Error",
type: "error",
message: "Please select atleast one issue",
});
return;
}
const newBlocked = [...watch("blocked_list"), ...data.issue_ids];
submitChanges({ blocked_list: newBlocked });
handleClose();
};
return (
<div className="flex items-start py-2 flex-wrap">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
<UserGroupIcon className="flex-shrink-0 h-4 w-4" />
<p>Blocked issues</p>
</div>
<div className="sm:basis-1/2 space-y-1">
<div className="flex gap-1 flex-wrap">
{watch("blocked_list") && watch("blocked_list").length > 0
? watch("blocked_list").map((issue) => (
<span
key={issue}
className="group flex items-center gap-1 border rounded-2xl text-xs px-1 py-0.5 hover:bg-red-50 hover:border-red-500 cursor-pointer"
onClick={() => {
const updatedBlockers = watch("blocked_list").filter((i) => i !== issue);
submitChanges({
blocked_list: updatedBlockers,
});
}}
>
{`${activeProject?.identifier}-${
issues?.results.find((i) => i.id === issue)?.sequence_id
}`}
<XMarkIcon className="h-2 w-2 group-hover:text-red-500" />
</span>
))
: null}
</div>
<Transition.Root
show={isBlockedModalOpen}
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>
<Combobox>
<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={(event) => setQuery(event.target.value)}
/>
</div>
<Combobox.Options
static
className="max-h-80 scroll-py-2 divide-y divide-gray-500 divide-opacity-10 overflow-y-auto"
>
{issuesList.length > 0 && (
<>
<li className="p-2">
{query === "" && (
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-gray-900">
Select blocked issues
</h2>
)}
<ul className="text-sm text-gray-700">
{issuesList.map((issue) => {
if (!watch("blocked_list").includes(issue.id)) {
return (
<Combobox.Option
key={issue.id}
as="label"
htmlFor={`issue-${issue.id}`}
value={{
name: issue.name,
url: `/projects/${issue.project}/issues/${issue.id}`,
}}
className={({ active }) =>
classNames(
"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 }) => (
<>
<div className="flex items-center gap-2">
<input
type="checkbox"
{...register("issue_ids")}
id={`issue-${issue.id}`}
value={issue.id}
/>
<span
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<span className="flex-shrink-0 text-xs text-gray-500">
{activeProject?.identifier}-{issue.sequence_id}
</span>
<span>{issue.name}</span>
</div>
</>
)}
</Combobox.Option>
);
}
})}
</ul>
</li>
</>
)}
</Combobox.Options>
{query !== "" && issuesList.length === 0 && (
<div className="py-14 px-6 text-center sm:px-14">
<FolderIcon
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 onClick={handleSubmit(onSubmit)} size="sm">
Add selected issues
</Button>
<div>
<Button type="button" theme="danger" size="sm" onClick={handleClose}>
Close
</Button>
</div>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
<button
type="button"
className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
onClick={() => setIsBlockedModalOpen(true)}
>
Select issues
</button>
</div>
</div>
);
};
export default SelectBlocked;

View File

@ -0,0 +1,238 @@
// react
import React, { useState } from "react";
// react-hook-form
import { SubmitHandler, useForm, UseFormWatch } from "react-hook-form";
// hooks
import useUser from "lib/hooks/useUser";
import useToast from "lib/hooks/useToast";
// headless ui
import { Combobox, Dialog, Transition } from "@headlessui/react";
// ui
import { Button } from "ui";
// icons
import {
FolderIcon,
MagnifyingGlassIcon,
UserGroupIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
// types
import { IIssue } from "types";
// constants
import { classNames } from "constants/common";
type FormInput = {
issue_ids: string[];
};
type Props = {
submitChanges: (formData: Partial<IIssue>) => void;
issuesList: IIssue[];
watch: UseFormWatch<IIssue>;
};
const SelectBlocker: React.FC<Props> = ({ submitChanges, issuesList, watch }) => {
const [query, setQuery] = useState("");
const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false);
const { activeProject, issues } = useUser();
const { setToastAlert } = useToast();
const { register, handleSubmit, reset } = useForm<FormInput>();
const handleClose = () => {
setIsBlockerModalOpen(false);
reset();
};
const onSubmit: SubmitHandler<FormInput> = (data) => {
if (!data.issue_ids || data.issue_ids.length === 0) {
setToastAlert({
title: "Error",
type: "error",
message: "Please select atleast one issue",
});
return;
}
const newBlockers = [...watch("blockers_list"), ...data.issue_ids];
submitChanges({ blockers_list: newBlockers });
handleClose();
};
return (
<div className="flex items-start py-2 flex-wrap">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
<UserGroupIcon className="flex-shrink-0 h-4 w-4" />
<p>Blocker issues</p>
</div>
<div className="sm:basis-1/2 space-y-1">
<div className="flex gap-1 flex-wrap">
{watch("blockers_list") && watch("blockers_list").length > 0
? watch("blockers_list").map((issue) => (
<span
key={issue}
className="group flex items-center gap-1 border rounded-2xl text-xs px-1 py-0.5 hover:bg-red-50 hover:border-red-500 cursor-pointer"
onClick={() => {
const updatedBlockers = watch("blockers_list").filter((i) => i !== issue);
submitChanges({
blockers_list: updatedBlockers,
});
}}
>
{`${activeProject?.identifier}-${
issues?.results.find((i) => i.id === issue)?.sequence_id
}`}
<XMarkIcon className="h-2 w-2 group-hover:text-red-500" />
</span>
))
: null}
</div>
<Transition.Root
show={isBlockerModalOpen}
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>
<Combobox>
<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={(event) => setQuery(event.target.value)}
/>
</div>
<Combobox.Options
static
className="max-h-80 scroll-py-2 divide-y divide-gray-500 divide-opacity-10 overflow-y-auto"
>
{issuesList.length > 0 && (
<>
<li className="p-2">
{query === "" && (
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-gray-900">
Select blocker issues
</h2>
)}
<ul className="text-sm text-gray-700">
{issuesList.map((issue) => {
if (!watch("blockers_list").includes(issue.id)) {
return (
<Combobox.Option
key={issue.id}
as="label"
htmlFor={`issue-${issue.id}`}
value={{
name: issue.name,
url: `/projects/${issue.project}/issues/${issue.id}`,
}}
className={({ active }) =>
classNames(
"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 }) => (
<>
<div className="flex items-center gap-2">
<input
type="checkbox"
{...register("issue_ids")}
id={`issue-${issue.id}`}
value={issue.id}
/>
<span
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<span className="flex-shrink-0 text-xs text-gray-500">
{activeProject?.identifier}-{issue.sequence_id}
</span>
<span>{issue.name}</span>
</div>
</>
)}
</Combobox.Option>
);
}
})}
</ul>
</li>
</>
)}
</Combobox.Options>
{query !== "" && issuesList.length === 0 && (
<div className="py-14 px-6 text-center sm:px-14">
<FolderIcon
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 onClick={handleSubmit(onSubmit)} size="sm">
Add selected issues
</Button>
<div>
<Button type="button" theme="danger" size="sm" onClick={handleClose}>
Close
</Button>
</div>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
<button
type="button"
className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
onClick={() => setIsBlockerModalOpen(true)}
>
Select issues
</button>
</div>
</div>
);
};
export default SelectBlocker;

View File

@ -10,6 +10,7 @@ import { classNames } from "constants/common";
import { Spinner } from "ui";
import React from "react";
import { ArrowPathIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
import CustomSelect from "ui/custom-select";
type Props = {
control: Control<IIssue, any>;
@ -30,17 +31,9 @@ const SelectCycle: React.FC<Props> = ({ control, handleCycleChange }) => {
control={control}
name="cycle"
render={({ field: { value } }) => (
<Listbox
as="div"
value={value}
onChange={(value: any) => {
handleCycleChange(value);
}}
className="flex-shrink-0"
>
{({ open }) => (
<div className="relative">
<Listbox.Button className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 w-full py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
<>
<CustomSelect
label={
<span
className={classNames(
value ? "" : "text-gray-900",
@ -49,32 +42,18 @@ const SelectCycle: React.FC<Props> = ({ control, handleCycleChange }) => {
>
{value ? cycles?.find((c) => c.id === value)?.name : "None"}
</span>
<ChevronDownIcon className="h-3 w-3" />
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
}
value={value}
onChange={(value: any) => {
handleCycleChange(value);
}}
>
<Listbox.Options className="absolute z-10 right-0 mt-1 w-40 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">
<div className="p-1">
{cycles ? (
cycles.length > 0 ? (
cycles.map((option) => (
<Listbox.Option
key={option.id}
className={({ active, selected }) =>
`${
active || selected ? "text-white bg-theme" : "text-gray-900"
} flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate`
}
value={option.id}
>
<CustomSelect.Option key={option.id} value={option.id}>
{option.name}
</Listbox.Option>
</CustomSelect.Option>
))
) : (
<div className="text-center">No cycles found</div>
@ -82,12 +61,8 @@ const SelectCycle: React.FC<Props> = ({ control, handleCycleChange }) => {
) : (
<Spinner />
)}
</div>
</Listbox.Options>
</Transition>
</div>
)}
</Listbox>
</CustomSelect>
</>
)}
/>
</div>

View File

@ -5,7 +5,7 @@ import { Control, Controller, UseFormWatch } from "react-hook-form";
// hooks
import useUser from "lib/hooks/useUser";
// components
import IssuesListModal from "components/project/issues/IssuesListModal";
import IssuesListModal from "components/project/issues/issues-list-modal";
// icons
import { UserIcon } from "@heroicons/react/24/outline";
// types
@ -16,7 +16,7 @@ type Props = {
submitChanges: (formData: Partial<IIssue>) => void;
issuesList: IIssue[];
customDisplay: JSX.Element;
watchIssue: UseFormWatch<IIssue>;
watch: UseFormWatch<IIssue>;
};
const SelectParent: React.FC<Props> = ({
@ -24,7 +24,7 @@ const SelectParent: React.FC<Props> = ({
submitChanges,
issuesList,
customDisplay,
watchIssue,
watch,
}) => {
const [isParentModalOpen, setIsParentModalOpen] = useState(false);
@ -60,11 +60,11 @@ const SelectParent: React.FC<Props> = ({
className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
onClick={() => setIsParentModalOpen(true)}
>
{watchIssue("parent") && watchIssue("parent") !== ""
{watch("parent") && watch("parent") !== ""
? `${activeProject?.identifier}-${
issues?.results.find((i) => i.id === watchIssue("parent"))?.sequence_id
issues?.results.find((i) => i.id === watch("parent"))?.sequence_id
}`
: "Select Parent"}
: "Select issue"}
</button>
</div>
</div>

View File

@ -1,7 +1,7 @@
// react
import React from "react";
// react-hook-form
import { Control, Controller } from "react-hook-form";
import { Control, Controller, UseFormWatch } from "react-hook-form";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// icons
@ -11,13 +11,15 @@ import { IIssue } from "types";
// constants
import { classNames } from "constants/common";
import { PRIORITIES } from "constants/";
import CustomSelect from "ui/custom-select";
type Props = {
control: Control<IIssue, any>;
submitChanges: (formData: Partial<IIssue>) => void;
watch: UseFormWatch<IIssue>;
};
const SelectPriority: React.FC<Props> = ({ control, submitChanges }) => {
const SelectPriority: React.FC<Props> = ({ control, submitChanges, watch }) => {
return (
<div className="flex items-center py-2 flex-wrap">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
@ -29,51 +31,23 @@ const SelectPriority: React.FC<Props> = ({ control, submitChanges }) => {
control={control}
name="state"
render={({ field: { value } }) => (
<Listbox
as="div"
<CustomSelect
label={
<span className={classNames(value ? "" : "text-gray-900", "text-left capitalize")}>
{watch("priority") && watch("priority") !== "" ? watch("priority") : "None"}
</span>
}
value={value}
onChange={(value: any) => {
submitChanges({ priority: value });
}}
className="flex-shrink-0"
>
{({ open }) => (
<div className="relative">
<Listbox.Button className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 w-full py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
<span className={classNames(value ? "" : "text-gray-900", "text-left")}>
{value}
</span>
<ChevronDownIcon className="h-3 w-3" />
</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 right-0 mt-1 w-40 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">
<div className="p-1">
{PRIORITIES.map((option) => (
<Listbox.Option
key={option}
className={({ active, selected }) =>
`${
active || selected ? "text-white bg-theme" : "text-gray-900"
} flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate capitalize`
}
value={option}
>
<CustomSelect.Option key={option} value={option} className="capitalize">
{option}
</Listbox.Option>
</CustomSelect.Option>
))}
</div>
</Listbox.Options>
</Transition>
</div>
)}
</Listbox>
</CustomSelect>
)}
/>
</div>

View File

@ -7,9 +7,10 @@ import { Listbox, Transition } from "@headlessui/react";
// types
import { IIssue } from "types";
import { classNames } from "constants/common";
import { Spinner } from "ui";
import { CustomMenu, Spinner } from "ui";
import React from "react";
import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
import CustomSelect from "ui/custom-select";
type Props = {
control: Control<IIssue, any>;
@ -30,17 +31,8 @@ const SelectState: React.FC<Props> = ({ control, submitChanges }) => {
control={control}
name="state"
render={({ field: { value } }) => (
<Listbox
as="div"
value={value}
onChange={(value: any) => {
submitChanges({ state: value });
}}
className="flex-shrink-0"
>
{({ open }) => (
<div className="relative">
<Listbox.Button className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 w-full py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
<CustomSelect
label={
<span
className={classNames(
value ? "" : "text-gray-900",
@ -61,30 +53,17 @@ const SelectState: React.FC<Props> = ({ control, submitChanges }) => {
"None"
)}
</span>
<ChevronDownIcon className="h-3 w-3" />
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
}
value={value}
onChange={(value: any) => {
submitChanges({ state: value });
}}
>
<Listbox.Options className="absolute z-10 right-0 mt-1 w-40 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">
<div className="p-1">
{states ? (
states.length > 0 ? (
states.map((option) => (
<Listbox.Option
key={option.id}
className={({ active, selected }) =>
`${
active || selected ? "text-white bg-theme" : "text-gray-900"
} flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate`
}
value={option.id}
>
<CustomSelect.Option key={option.id} value={option.id}>
<>
{option.color && (
<span
className="h-2 w-2 rounded-full flex-shrink-0"
@ -92,7 +71,8 @@ const SelectState: React.FC<Props> = ({ control, submitChanges }) => {
></span>
)}
{option.name}
</Listbox.Option>
</>
</CustomSelect.Option>
))
) : (
<div className="text-center">No states found</div>
@ -100,12 +80,7 @@ const SelectState: React.FC<Props> = ({ control, submitChanges }) => {
) : (
<Spinner />
)}
</div>
</Listbox.Options>
</Transition>
</div>
)}
</Listbox>
</CustomSelect>
)}
/>
</div>

View File

@ -0,0 +1,249 @@
// react
import React, { useState } from "react";
// headless ui
import { Combobox, Dialog, Transition } from "@headlessui/react";
// ui
import { Button } from "ui";
// icons
import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
// types
import { IIssue } from "types";
import { classNames } from "constants/common";
import useUser from "lib/hooks/useUser";
type Props = {
isOpen: boolean;
handleClose: () => void;
value?: any;
onChange: (...event: any[]) => void;
issues: IIssue[];
title?: string;
multiple?: boolean;
customDisplay?: JSX.Element;
};
const IssuesListModal: React.FC<Props> = ({
isOpen,
handleClose: onClose,
value,
onChange,
issues,
title = "Issues",
multiple = false,
customDisplay,
}) => {
const [query, setQuery] = useState("");
const [values, setValues] = useState<string[]>([]);
const { activeProject } = useUser();
const handleClose = () => {
onClose();
setQuery("");
setValues([]);
};
const filteredIssues: IIssue[] =
query === ""
? issues ?? []
: issues?.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">
{multiple ? (
<>
<Combobox
value={value}
onChange={(val) => {
// setValues(val);
console.log(val);
}}
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)}
displayValue={() => ""}
/>
</div>
<div className="p-3">{customDisplay}</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">
{title}
</h2>
)}
<ul className="text-sm text-gray-700">
{filteredIssues.map((issue) => (
<Combobox.Option
key={issue.id}
value={issue.id}
className={({ active }) =>
classNames(
"flex items-center gap-2 cursor-pointer select-none 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="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<span className="flex-shrink-0 text-xs text-gray-500">
{activeProject?.identifier}-{issue.sequence_id}
</span>{" "}
{issue.id}
</>
)}
</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={() => onChange(values)}>
Add issues
</Button>
</div>
</>
) : (
<Combobox value={value} onChange={onChange}>
<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)}
displayValue={() => ""}
/>
</div>
<div className="p-3">{customDisplay}</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">
{title}
</h2>
)}
<ul className="text-sm text-gray-700">
{filteredIssues.map((issue) => (
<Combobox.Option
key={issue.id}
value={issue.id}
className={({ active }) =>
classNames(
"flex items-center gap-2 cursor-pointer select-none rounded-md px-3 py-2",
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
)
}
onClick={() => handleClose()}
>
{({ selected }) => (
<>
<span
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<span className="flex-shrink-0 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>
)}
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
</>
);
};
export default IssuesListModal;

View File

@ -0,0 +1,245 @@
import React, { useEffect } from "react";
// swr
import { mutate } from "swr";
// react hook form
import { useForm } from "react-hook-form";
// headless
import { Dialog, Transition } from "@headlessui/react";
// ui
import { Button, Input, TextArea, Select } from "ui";
// services
import modulesService from "lib/services/modules.service";
// hooks
import useUser from "lib/hooks/useUser";
// types
import type { IModule } from "types";
// common
import { renderDateFormat } from "constants/common";
// fetch keys
import { MODULE_LIST } from "constants/fetch-keys";
type Props = {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
projectId: string;
data?: IModule;
};
const defaultValues: Partial<IModule> = {
name: "",
description: "",
};
const CreateUpdateModuleModal: React.FC<Props> = ({ isOpen, setIsOpen, data, projectId }) => {
const handleClose = () => {
setIsOpen(false);
const timeout = setTimeout(() => {
reset(defaultValues);
clearTimeout(timeout);
}, 500);
};
const { activeWorkspace } = useUser();
const {
register,
formState: { errors, isSubmitting },
handleSubmit,
reset,
setError,
} = useForm<IModule>({
defaultValues,
});
const onSubmit = async (formData: IModule) => {
if (!activeWorkspace) return;
const payload = {
...formData,
start_date: formData.start_date ? renderDateFormat(formData.start_date) : null,
target_date: formData.target_date ? renderDateFormat(formData.target_date) : null,
};
if (!data) {
await modulesService
.createModule(activeWorkspace.slug, projectId, payload)
.then((res) => {
mutate<IModule[]>(
MODULE_LIST(projectId),
(prevData) => [res, ...(prevData ?? [])],
false
);
handleClose();
})
.catch((err) => {
Object.keys(err).map((key) => {
setError(key as keyof typeof defaultValues, {
message: err[key].join(", "),
});
});
});
} else {
await modulesService
.updateModule(activeWorkspace.slug, projectId, data.id, payload)
.then((res) => {
mutate<IModule[]>(
MODULE_LIST(projectId),
(prevData) => {
const newData = prevData?.map((item) => {
if (item.id === res.id) {
return res;
}
return item;
});
return newData;
},
false
);
handleClose();
})
.catch((err) => {
Object.keys(err).map((key) => {
setError(key as keyof typeof defaultValues, {
message: err[key].join(", "),
});
});
});
}
};
useEffect(() => {
if (data) {
setIsOpen(true);
reset(data);
} else {
reset(defaultValues);
}
}, [data, setIsOpen, reset]);
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<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-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-5">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
{data ? "Update" : "Create"} Module
</Dialog.Title>
<div className="space-y-3">
<div>
<Input
id="name"
label="Name"
name="name"
type="name"
placeholder="Enter name"
autoComplete="off"
error={errors.name}
register={register}
validations={{
required: "Name is required",
}}
/>
</div>
<div>
<TextArea
id="description"
name="description"
label="Description"
placeholder="Enter description"
error={errors.description}
register={register}
/>
</div>
<div>
<Select
id="status"
name="status"
label="Status"
error={errors.status}
register={register}
validations={{
required: "Status is required",
}}
options={[
{ label: "Backlog", value: "backlog" },
{ label: "Planned", value: "planned" },
{ label: "In Progress", value: "in-progress" },
{ label: "Paused", value: "paused" },
{ label: "Completed", value: "completed" },
{ label: "Cancelled", value: "cancelled" },
]}
/>
</div>
<div className="flex gap-x-2">
<div className="w-full">
<Input
id="start_date"
label="Start Date"
name="start_date"
type="date"
placeholder="Enter start date"
error={errors.start_date}
register={register}
/>
</div>
<div className="w-full">
<Input
id="target_date"
label="Target Date"
name="target_date"
type="date"
placeholder="Enter target date"
error={errors.target_date}
register={register}
/>
</div>
</div>
</div>
</div>
<div className="mt-5 sm:mt-6 sm:grid sm:grid-flow-row-dense sm:grid-cols-2 sm:gap-3">
<Button theme="secondary" onClick={handleClose}>
Cancel
</Button>
<Button type="submit" disabled={isSubmitting}>
{data
? isSubmitting
? "Updating Module..."
: "Update Module"
: isSubmitting
? "Creating Module..."
: "Create Module"}
</Button>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};
export default CreateUpdateModuleModal;

View File

@ -86,7 +86,7 @@ const WorkspaceOptions: React.FC<Props> = ({ sidebarCollapse }) => {
<Menu as="div" className="col-span-4 inline-block text-left w-full">
<div className="w-full">
<Menu.Button
className={`inline-flex justify-between items-center w-full rounded-md px-2 py-2 text-sm font-semibold text-gray-700 focus:outline-none ${
className={`inline-flex justify-between items-center w-full rounded-md px-1 py-2 text-sm font-semibold text-gray-700 focus:outline-none ${
!sidebarCollapse
? "hover:bg-gray-50 focus:bg-gray-50 border border-gray-300 shadow-sm"
: ""
@ -118,7 +118,7 @@ const WorkspaceOptions: React.FC<Props> = ({ sidebarCollapse }) => {
</div>
{!sidebarCollapse && (
<div className="flex-grow flex justify-end">
<ChevronDownIcon className="-mr-1 ml-2 h-5 w-5" aria-hidden="true" />
<ChevronDownIcon className="ml-2 h-3 w-3" aria-hidden="true" />
</div>
)}
</Menu.Button>

View File

@ -132,3 +132,18 @@ export const REMOVE_ISSUE_FROM_CYCLE = (
bridgeId: string
) =>
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/${bridgeId}/`;
// modules
export const MODULES_ENDPOINT = (workspaceSlug: string, projectId: string) =>
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/`;
export const MODULE_DETAIL = (workspaceSlug: string, projectId: string, moduleId: string) =>
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`;
export const MODULE_ISSUES = (workspaceSlug: string, projectId: string, moduleId: string) =>
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/`;
export const MODULE_ISSUE_DETAIL = (
workspaceSlug: string,
projectId: string,
moduleId: string,
issueId: string
) =>
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/${issueId}/`;

View File

@ -23,10 +23,14 @@ export const PROJECT_ISSUE_BY_STATE = (projectId: string) => `PROJECT_ISSUE_BY_S
export const PROJECT_ISSUE_LABELS = (projectId: string) => `PROJECT_ISSUE_LABELS_${projectId}`;
export const CYCLE_LIST = (projectId: string) => `CYCLE_LIST_${projectId}`;
export const CYCLE_ISSUES = (sprintId: string) => `CYCLE_ISSUES_${sprintId}`;
export const CYCLE_ISSUES = (cycleId: string) => `CYCLE_ISSUES_${cycleId}`;
export const CYCLE_DETAIL = "CYCLE_DETAIL";
export const STATE_LIST = (projectId: string) => `STATE_LIST_${projectId}`;
export const STATE_DETAIL = "STATE_DETAIL";
export const USER_ISSUE = (workspaceSlug: string) => `USER_ISSUE_${workspaceSlug}`;
export const MODULE_LIST = (projectId: string) => `MODULE_LIST_${projectId}`;
export const MODULE_ISSUES = (moduleId: string) => `MODULE_ISSUES_${moduleId}`;
export const MODULE_DETAIL = "MODULE_DETAIL";

View File

@ -1,8 +1,8 @@
import React from "react";
// layouts
import Container from "layouts/Container";
import DefaultTopBar from "layouts/Navbar/DefaultTopBar";
import Container from "layouts/container";
import DefaultTopBar from "layouts/navbar/DefaultTopBar";
// types
import type { Props } from "./types";

View File

@ -1,629 +0,0 @@
import React, { useState } from "react";
// next
import Link from "next/link";
import { useRouter } from "next/router";
import Image from "next/image";
// services
import userService from "lib/services/user.service";
import authenticationService from "lib/services/authentication.service";
// hooks
import useUser from "lib/hooks/useUser";
import useTheme from "lib/hooks/useTheme";
import useToast from "lib/hooks/useToast";
// headless ui
import { Dialog, Disclosure, Menu, Transition } from "@headlessui/react";
// icons
import {
ArrowPathIcon,
Bars3Icon,
ChevronDownIcon,
Cog6ToothIcon,
HomeIcon,
ClipboardDocumentListIcon,
PlusIcon,
RectangleStackIcon,
UserGroupIcon,
UserIcon,
XMarkIcon,
ArrowLongLeftIcon,
QuestionMarkCircleIcon,
EllipsisHorizontalIcon,
ClipboardDocumentIcon,
} from "@heroicons/react/24/outline";
// constants
import { classNames, copyTextToClipboard } from "constants/common";
// ui
import { CustomListbox, Spinner, Tooltip } from "ui";
// types
import type { IUser } from "types";
const navigation = (projectId: string) => [
{
name: "Issues",
href: `/projects/${projectId}/issues`,
icon: RectangleStackIcon,
},
{
name: "Cycles",
href: `/projects/${projectId}/cycles`,
icon: ArrowPathIcon,
},
{
name: "Members",
href: `/projects/${projectId}/members`,
icon: UserGroupIcon,
},
{
name: "Settings",
href: `/projects/${projectId}/settings`,
icon: Cog6ToothIcon,
},
];
const workspaceLinks = [
{
icon: HomeIcon,
name: "Home",
href: `/workspace`,
},
{
icon: ClipboardDocumentListIcon,
name: "Projects",
href: "/projects",
},
{
icon: RectangleStackIcon,
name: "My Issues",
href: "/me/my-issues",
},
{
icon: UserGroupIcon,
name: "Members",
href: "/workspace/members",
},
// {
// icon: InboxIcon,
// name: "Inbox",
// href: "#",
// },
{
icon: Cog6ToothIcon,
name: "Settings",
href: "/workspace/settings",
},
];
const userLinks = [
{
name: "My Profile",
href: "/me/profile",
},
{
name: "Workspace Invites",
href: "/invitations",
},
];
const Sidebar: React.FC = () => {
const [sidebarOpen, setSidebarOpen] = useState(false);
const router = useRouter();
const { projects, user } = useUser();
const { projectId } = router.query;
const { workspaces, activeWorkspace, mutateUser } = useUser();
const { collapsed: sidebarCollapse, toggleCollapsed } = useTheme();
const { setToastAlert } = useToast();
return (
<nav className="h-full">
<Transition.Root show={sidebarOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-40 md:hidden" onClose={setSidebarOpen}>
<Transition.Child
as={React.Fragment}
enter="transition-opacity ease-linear duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-linear duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-600 bg-opacity-75" />
</Transition.Child>
<div className="fixed inset-0 z-40 flex">
<Transition.Child
as={React.Fragment}
enter="transition ease-in-out duration-300 transform"
enterFrom="-translate-x-full"
enterTo="translate-x-0"
leave="transition ease-in-out duration-300 transform"
leaveFrom="translate-x-0"
leaveTo="-translate-x-full"
>
<Dialog.Panel className="relative flex w-full max-w-xs flex-1 flex-col bg-white">
<Transition.Child
as={React.Fragment}
enter="ease-in-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="absolute top-0 right-0 -mr-12 pt-2">
<button
type="button"
className="ml-1 flex h-10 w-10 items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
onClick={() => setSidebarOpen(false)}
>
<span className="sr-only">Close sidebar</span>
<XMarkIcon className="h-6 w-6 text-white" aria-hidden="true" />
</button>
</div>
</Transition.Child>
<div className="h-0 flex-1 overflow-y-auto pt-5 pb-4">
<nav className="mt-5 space-y-1 px-2">
{projectId &&
navigation(projectId as string).map((item) => (
<Link href={item.href} key={item.name}>
<a
className={classNames(
item.href === router.asPath
? "bg-gray-100 text-gray-900"
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900",
"group flex items-center px-2 py-2 text-base font-medium rounded-md"
)}
>
<item.icon
className={classNames(
item.href === router.asPath
? "text-gray-500"
: "text-gray-400 group-hover:text-gray-500",
"mr-4 flex-shrink-0 h-6 w-6"
)}
aria-hidden="true"
/>
{item.name}
</a>
</Link>
))}
</nav>
</div>
</Dialog.Panel>
</Transition.Child>
<div className="w-14 flex-shrink-0" />
</div>
</Dialog>
</Transition.Root>
<div
className={`${
sidebarCollapse ? "" : "w-auto md:w-64"
} hidden md:inset-y-0 md:flex md:flex-col h-full`}
>
<div className="flex flex-1 flex-col border-r border-gray-200">
<div className="h-full flex flex-1 flex-col pt-5">
<div className="px-2">
<div
className={`relative ${
sidebarCollapse ? "flex" : "grid grid-cols-5 gap-2 items-center"
}`}
>
<Menu as="div" className="col-span-4 inline-block text-left w-full">
<div className="w-full">
<Menu.Button
className={`inline-flex justify-between items-center w-full rounded-md px-2 py-2 text-sm font-semibold text-gray-700 focus:outline-none ${
!sidebarCollapse
? "hover:bg-gray-50 focus:bg-gray-50 border border-gray-300 shadow-sm"
: ""
}`}
>
<div className="flex gap-x-1 items-center">
<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 !== "" ? (
<Image
src={activeWorkspace.logo}
alt="Workspace Logo"
layout="fill"
objectFit="cover"
/>
) : (
activeWorkspace?.name?.charAt(0) ?? "N"
)}
</div>
{!sidebarCollapse && (
<p className="truncate w-20 text-left ml-1">
{activeWorkspace?.name ?? "Loading..."}
</p>
)}
</div>
{!sidebarCollapse && (
<div className="flex-grow flex justify-end">
<ChevronDownIcon className="-mr-1 ml-2 h-5 w-5" 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 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">
{workspaces ? (
<>
{workspaces.length > 0 ? (
workspaces.map((workspace: any) => (
<Menu.Item key={workspace.id}>
{({ active }) => (
<button
type="button"
onClick={() => {
mutateUser(
(prevData) => ({
...(prevData as IUser),
last_workspace_id: workspace.id,
}),
false
);
userService
.updateUser({
last_workspace_id: workspace?.id,
})
.then((res) => {
const isInProject =
router.pathname.includes("/[projectId]/");
if (isInProject) router.push("/workspace");
})
.catch((err) => console.error(err));
}}
className={`${
active ? "bg-theme text-white" : "text-gray-900"
} group flex w-full items-center rounded-md p-2 text-sm`}
>
{workspace.name}
</button>
)}
</Menu.Item>
))
) : (
<p>No workspace found!</p>
)}
<Menu.Item
as="button"
onClick={() => {
router.push("/create-workspace");
}}
className="w-full"
>
{({ active }) => (
<a
className={`flex items-center gap-x-1 p-2 w-full text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-sm ${
active ? "bg-theme text-white" : "text-gray-900"
}`}
>
<PlusIcon className="w-5 h-5" />
<span>Create Workspace</span>
</a>
)}
</Menu.Item>
</>
) : (
<div className="w-full flex justify-center">
<Spinner />
</div>
)}
</div>
</Menu.Items>
</Transition>
</Menu>
{!sidebarCollapse && (
<Menu as="div" className="inline-block text-left flex-shrink-0 w-full">
<div className="h-10 w-10">
<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 !== "" ? (
<Image
src={user.avatar}
alt="User Avatar"
layout="fill"
className="rounded-md"
/>
) : (
<UserIcon className="h-5 w-5" />
)}
</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-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">
{userLinks.map((item) => (
<Menu.Item key={item.name} as="div">
{(active) => (
<Link href={item.href}>
<a className="flex items-center gap-x-1 p-2 w-full text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-sm">
{item.name}
</a>
</Link>
)}
</Menu.Item>
))}
<Menu.Item as="div">
<button
type="button"
className="flex items-center gap-x-1 p-2 w-full text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-sm"
onClick={async () => {
await authenticationService
.signOut({
refresh_token: authenticationService.getRefreshToken(),
})
.then((response) => {
console.log("user signed out", response);
})
.catch((error) => {
console.log("Failed to sign out", error);
})
.finally(() => {
mutateUser();
router.push("/signin");
});
}}
>
Sign Out
</button>
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
)}
</div>
<div className="mt-3 flex-1 space-y-1 bg-white">
{workspaceLinks.map((link, index) => (
<Link key={index} href={link.href}>
<a
className={`${
link.href === router.asPath
? "bg-theme text-white"
: "hover:bg-indigo-100 focus:bg-indigo-100"
} flex items-center gap-3 p-2 text-xs font-medium rounded-md outline-none ${
sidebarCollapse ? "justify-center" : ""
}`}
>
<link.icon
className={`${
link.href === router.asPath ? "text-white" : ""
} flex-shrink-0 h-4 w-4`}
aria-hidden="true"
/>
{!sidebarCollapse && link.name}
</a>
</Link>
))}
</div>
</div>
<div
className={`flex flex-col px-2 pt-5 pb-3 mt-3 space-y-2 bg-gray-50 h-full overflow-y-auto ${
sidebarCollapse ? "rounded-xl" : "rounded-t-3xl"
}`}
>
{projects ? (
<>
{projects.length > 0 ? (
projects.map((project) => (
<Disclosure key={project?.id} defaultOpen={projectId === project?.id}>
{({ open }) => (
<>
<div className="flex items-center">
<Disclosure.Button
className={`w-full flex items-center gap-2 font-medium rounded-md p-2 text-sm ${
sidebarCollapse ? "justify-center" : ""
}`}
>
<span className="bg-gray-700 text-white rounded h-7 w-7 grid place-items-center uppercase flex-shrink-0">
{project?.name.charAt(0)}
</span>
{!sidebarCollapse && (
<span className="flex items-center justify-between w-full">
{project?.name}
<span>
<ChevronDownIcon
className={`h-4 w-4 duration-300 ${
open ? "rotate-180" : ""
}`}
/>
</span>
</span>
)}
</Disclosure.Button>
{!sidebarCollapse && (
<Menu as="div" className="relative inline-block">
<Menu.Button className="grid relative place-items-center focus:outline-none">
<EllipsisHorizontalIcon className="h-4 w-4" />
</Menu.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="origin-top-right absolute right-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-20">
<div className="p-1">
<Menu.Item as="div">
{(active) => (
<button
className="flex items-center gap-2 p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap"
onClick={() =>
copyTextToClipboard(
`https://app.plane.so/projects/${project?.id}/issues/`
).then(() => {
setToastAlert({
title: "Link Copied",
message: "Link copied to clipboard",
type: "success",
});
})
}
>
<ClipboardDocumentIcon className="h-3 w-3" />
Copy Link
</button>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
)}
</div>
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Disclosure.Panel
className={`${
sidebarCollapse ? "" : "ml-[2.25rem]"
} flex flex-col gap-y-1`}
>
{navigation(project?.id).map((item) => (
<Link key={item.name} href={item.href}>
<a
className={classNames(
item.href === router.asPath
? "bg-gray-200 text-gray-900"
: "text-gray-500 hover:bg-gray-100 hover:text-gray-900 focus:bg-gray-100 focus:text-gray-900",
"group flex items-center px-2 py-2 text-xs font-medium rounded-md outline-none",
sidebarCollapse ? "justify-center" : ""
)}
>
<item.icon
className={classNames(
item.href === router.asPath
? "text-gray-900"
: "text-gray-500 group-hover:text-gray-900",
"flex-shrink-0 h-4 w-4",
!sidebarCollapse ? "mr-3" : ""
)}
aria-hidden="true"
/>
{!sidebarCollapse && item.name}
</a>
</Link>
))}
</Disclosure.Panel>
</Transition>
</>
)}
</Disclosure>
))
) : (
<div className="text-center space-y-3">
{!sidebarCollapse && (
<h4 className="text-gray-700 text-sm">
You don{"'"}t have any project yet
</h4>
)}
<button
type="button"
className="group flex justify-center items-center gap-2 w-full rounded-md p-2 text-sm bg-theme text-white"
onClick={() => {
const e = new KeyboardEvent("keydown", {
ctrlKey: true,
key: "p",
});
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-5 w-5" />
{!sidebarCollapse && "Create Project"}
</button>
</div>
)}
</>
) : (
<div className="w-full flex justify-center">
<Spinner />
</div>
)}
</div>
<div
className={`px-2 py-2 bg-gray-50 w-full self-baseline flex items-center ${
sidebarCollapse ? "flex-col-reverse" : ""
}`}
>
<button
type="button"
className={`flex items-center gap-3 px-2 py-2 text-xs font-medium rounded-md text-gray-500 hover:bg-gray-100 hover:text-gray-900 outline-none ${
sidebarCollapse ? "justify-center w-full" : ""
}`}
onClick={() => toggleCollapsed()}
>
<ArrowLongLeftIcon
className={`h-4 w-4 text-gray-500 group-hover:text-gray-900 flex-shrink-0 duration-300 ${
sidebarCollapse ? "rotate-180" : ""
}`}
/>
</button>
<button
type="button"
className={`flex items-center gap-3 px-2 py-2 text-xs font-medium rounded-md text-gray-500 hover:bg-gray-100 hover:text-gray-900 outline-none ${
sidebarCollapse ? "justify-center w-full" : ""
}`}
onClick={() => {
const e = new KeyboardEvent("keydown", {
ctrlKey: true,
key: "h",
});
document.dispatchEvent(e);
}}
title="Help"
>
<QuestionMarkCircleIcon className="h-4 w-4 text-gray-500" />
</button>
</div>
</div>
</div>
</div>
<div className="sticky top-0 z-10 bg-white pl-1 pt-1 sm:pl-3 sm:pt-3 md:hidden">
<button
type="button"
className="-ml-0.5 -mt-0.5 inline-flex h-12 w-12 items-center justify-center rounded-md text-gray-500 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
onClick={() => setSidebarOpen(true)}
>
<span className="sr-only">Open sidebar</span>
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
</nav>
);
};
export default Sidebar;

View File

@ -19,6 +19,7 @@ import {
XMarkIcon,
ArrowLongLeftIcon,
QuestionMarkCircleIcon,
RectangleGroupIcon,
} from "@heroicons/react/24/outline";
// constants
import { classNames } from "constants/common";
@ -34,6 +35,11 @@ const navigation = (projectId: string) => [
href: `/projects/${projectId}/cycles`,
icon: ArrowPathIcon,
},
{
name: "Modules",
href: `/projects/${projectId}/modules`,
icon: RectangleGroupIcon,
},
{
name: "Members",
href: `/projects/${projectId}/members`,
@ -138,7 +144,7 @@ const Sidebar: React.FC = () => {
</Transition.Root>
<div
className={`${
sidebarCollapse ? "" : "w-auto md:w-64"
sidebarCollapse ? "" : "w-auto md:w-60"
} h-full hidden md:inset-y-0 md:flex md:flex-col`}
>
<div className="h-full flex flex-1 flex-col border-r border-gray-200">

View File

@ -5,9 +5,9 @@ import { useRouter } from "next/router";
// hooks
import useUser from "lib/hooks/useUser";
// layouts
import Container from "layouts/Container";
import Sidebar from "layouts/Navbar/main-sidebar";
import Header from "layouts/Navbar/Header";
import Container from "layouts/container";
import Sidebar from "layouts/navbar/main-sidebar";
import Header from "layouts/navbar/Header";
// components
import CreateProjectModal from "components/project/create-project-modal";
// types

View File

@ -5,10 +5,10 @@ import { useRouter } from "next/router";
// hooks
import useUser from "lib/hooks/useUser";
// layouts
import Container from "layouts/Container";
import Sidebar from "layouts/Navbar/main-sidebar";
import SettingsSidebar from "layouts/Navbar/settings-sidebar";
import Header from "layouts/Navbar/Header";
import Container from "layouts/container";
import Sidebar from "layouts/navbar/main-sidebar";
import SettingsSidebar from "layouts/navbar/settings-sidebar";
import Header from "layouts/navbar/Header";
// components
import CreateProjectModal from "components/project/create-project-modal";
// types

View File

@ -0,0 +1,119 @@
// api routes
import {
MODULES_ENDPOINT,
MODULE_DETAIL,
MODULE_ISSUES,
MODULE_ISSUE_DETAIL,
} from "constants/api-routes";
// services
import APIService from "lib/services/api.service";
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
class ProjectIssuesServices extends APIService {
constructor() {
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
}
async getModules(workspaceSlug: string, projectId: string): Promise<any> {
return this.get(MODULES_ENDPOINT(workspaceSlug, projectId))
.then((response) => {
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
}
async createModule(workspaceSlug: string, projectId: string, data: any): Promise<any> {
return this.post(MODULES_ENDPOINT(workspaceSlug, projectId), data)
.then((response) => {
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
}
async updateModule(
workspaceSlug: string,
projectId: string,
moduleId: string,
data: any
): Promise<any> {
return this.put(MODULE_DETAIL(workspaceSlug, projectId, moduleId), data)
.then((response) => {
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
}
async patchModule(
workspaceSlug: string,
projectId: string,
moduleId: string,
data: any
): Promise<any> {
return this.patch(MODULE_DETAIL(workspaceSlug, projectId, moduleId), data)
.then((response) => {
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
}
async deleteModule(workspaceSlug: string, projectId: string, moduleId: string): Promise<any> {
return this.delete(MODULE_DETAIL(workspaceSlug, projectId, moduleId))
.then((response) => {
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
}
async getModuleIssues(workspaceSlug: string, projectId: string, moduleId: string): Promise<any> {
return this.get(MODULE_ISSUES(workspaceSlug, projectId, moduleId))
.then((response) => {
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
}
async addIssueToModule(
workspaceSlug: string,
projectId: string,
moduleId: string,
data: any
): Promise<any> {
return this.post(MODULE_ISSUES(workspaceSlug, projectId, moduleId), data)
.then((response) => {
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
}
async removeIssueFromModule(
workspaceSlug: string,
projectId: string,
moduleId: string,
issueId: string
): Promise<any> {
return this.delete(MODULE_ISSUE_DETAIL(workspaceSlug, projectId, moduleId, issueId))
.then((response) => {
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
}
}
export default new ProjectIssuesServices();

View File

@ -21,7 +21,7 @@ import useUser from "lib/hooks/useUser";
import useIssuesFilter from "lib/hooks/useIssuesFilter";
import useIssuesProperties from "lib/hooks/useIssuesProperties";
// headless ui
import { Menu, Popover, Transition } from "@headlessui/react";
import { Popover, Transition } from "@headlessui/react";
// ui
import { BreadcrumbItem, Breadcrumbs, CustomMenu } from "ui";
// icons
@ -41,6 +41,8 @@ import { CYCLE_ISSUES, PROJECT_MEMBERS } from "constants/fetch-keys";
// constants
import { classNames, replaceUnderscoreIfSnakeCase } from "constants/common";
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
import CycleIssuesListModal from "components/project/cycles/cycle-issues-list-modal";
import ConfirmCycleDeletion from "components/project/cycles/confirm-cycle-deletion";
const groupByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> | null }> = [
{ name: "State", key: "state_detail.name" },
@ -79,8 +81,9 @@ const SingleCycle: React.FC<Props> = () => {
const [isIssueModalOpen, setIsIssueModalOpen] = useState(false);
const [selectedCycle, setSelectedCycle] = useState<SelectSprintType>();
const [selectedIssues, setSelectedIssues] = useState<SelectIssue>();
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
const { activeWorkspace, activeProject, cycles } = useUser();
const { activeWorkspace, activeProject, cycles, issues } = useUser();
const router = useRouter();
@ -98,9 +101,8 @@ const SingleCycle: React.FC<Props> = () => {
cycleServices.getCycleIssues(activeWorkspace?.slug, activeProject?.id, cycleId as string)
: null
);
const cycleIssuesArray = cycleIssues?.map((issue) => {
return issue.issue_details;
return { bridge: issue.id, ...issue.issue_details };
});
const { data: members } = useSWR(
@ -143,6 +145,10 @@ const SingleCycle: React.FC<Props> = () => {
}
};
const openIssuesListModal = () => {
setCycleIssuesListModal(true);
};
const addIssueToCycle = (cycleId: string, issueId: string) => {
if (!activeWorkspace || !activeProject?.id) return;
@ -202,16 +208,16 @@ const SingleCycle: React.FC<Props> = () => {
// console.log(result);
};
const removeIssueFromCycle = (cycleId: string, bridgeId: string) => {
const removeIssueFromCycle = (bridgeId: string) => {
if (activeWorkspace && activeProject) {
mutate<CycleIssueResponse[]>(
CYCLE_ISSUES(cycleId),
CYCLE_ISSUES(cycleId as string),
(prevData) => prevData?.filter((p) => p.id !== bridgeId),
false
);
issuesServices
.removeIssueFromCycle(activeWorkspace.slug, activeProject.id, cycleId, bridgeId)
.removeIssueFromCycle(activeWorkspace.slug, activeProject.id, cycleId as string, bridgeId)
.then((res) => {
console.log(res);
})
@ -234,6 +240,12 @@ const SingleCycle: React.FC<Props> = () => {
setIsOpen={setIsIssueModalOpen}
projectId={activeProject?.id}
/>
<CycleIssuesListModal
isOpen={cycleIssuesListModal}
handleClose={() => setCycleIssuesListModal(false)}
issues={issues}
cycleId={cycleId as string}
/>
<AppLayout
breadcrumbs={
<Breadcrumbs>
@ -244,38 +256,25 @@ const SingleCycle: React.FC<Props> = () => {
</Breadcrumbs>
}
left={
<Menu as="div" className="relative inline-block">
<Menu.Button className="flex items-center gap-1 border ml-2 px-2 py-1 rounded hover:bg-gray-100 text-xs font-medium">
<CustomMenu
label={
<>
<ArrowPathIcon className="h-3 w-3" />
{cycles?.find((c) => c.id === cycleId)?.name}
</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"
</>
}
className="ml-1.5"
>
<Menu.Items className="absolute left-3 mt-2 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
{cycles?.map((cycle) => (
<Menu.Item key={cycle.id}>
<Link href={`/projects/${activeProject?.id}/cycles/${cycle.id}`}>
<a
className={`block text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full ${
cycle.id === cycleId ? "bg-theme text-white" : ""
}`}
<CustomMenu.MenuItem
key={cycle.id}
renderAs="a"
href={`/projects/${activeProject?.id}/cycles/${cycle.id}`}
>
{cycle.name}
</a>
</Link>
</Menu.Item>
</CustomMenu.MenuItem>
))}
</Menu.Items>
</Transition>
</Menu>
</CustomMenu>
}
right={
<div className="flex items-center gap-2">
@ -327,7 +326,7 @@ const SingleCycle: React.FC<Props> = () => {
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute mr-5 right-1/2 z-10 mt-1 w-screen max-w-xs translate-x-1/2 transform p-3 bg-white rounded-lg shadow-lg overflow-hidden">
<Popover.Panel className="absolute right-0 z-10 mt-1 w-screen max-w-xs transform p-3 bg-white rounded-lg shadow-lg overflow-hidden">
<div className="relative flex flex-col gap-1 gap-y-4">
<div className="flex justify-between items-center">
<h4 className="text-sm text-gray-600">Group by</h4>
@ -336,6 +335,7 @@ const SingleCycle: React.FC<Props> = () => {
groupByOptions.find((option) => option.key === groupByProperty)
?.name ?? "Select"
}
width="auto"
>
{groupByOptions.map((option) => (
<CustomMenu.MenuItem
@ -354,6 +354,7 @@ const SingleCycle: React.FC<Props> = () => {
orderByOptions.find((option) => option.key === orderBy)?.name ??
"Select"
}
width="auto"
>
{orderByOptions.map((option) =>
groupByProperty === "priority" && option.key === "priority" ? null : (
@ -374,6 +375,7 @@ const SingleCycle: React.FC<Props> = () => {
filterIssueOptions.find((option) => option.key === filterIssue)
?.name ?? "Select"
}
width="auto"
>
{filterIssueOptions.map((option) => (
<CustomMenu.MenuItem
@ -420,9 +422,7 @@ const SingleCycle: React.FC<Props> = () => {
selectedGroup={groupByProperty}
properties={properties}
openCreateIssueModal={openCreateIssueModal}
openIssuesListModal={() => {
return;
}}
openIssuesListModal={openIssuesListModal}
removeIssueFromCycle={removeIssueFromCycle}
/>
) : (
@ -434,9 +434,7 @@ const SingleCycle: React.FC<Props> = () => {
selectedGroup={groupByProperty}
members={members}
openCreateIssueModal={openCreateIssueModal}
openIssuesListModal={() => {
return;
}}
openIssuesListModal={openIssuesListModal}
/>
</div>
)}

View File

@ -5,46 +5,31 @@ import { useRouter } from "next/router";
import type { NextPage } from "next";
// swr
import useSWR from "swr";
// hoc
import withAuth from "lib/hoc/withAuthWrapper";
// services
import sprintService from "lib/services/cycles.service";
// hooks
import useUser from "lib/hooks/useUser";
import useIssuesProperties from "lib/hooks/useIssuesProperties";
// fetching keys
import { CYCLE_LIST } from "constants/fetch-keys";
// hoc
import withAuth from "lib/hoc/withAuthWrapper";
// layouts
import AppLayout from "layouts/app-layout";
// components
import CycleIssuesListModal from "components/project/cycles/CycleIssuesListModal";
import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion";
import ConfirmSprintDeletion from "components/project/cycles/ConfirmCycleDeletion";
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
import CreateUpdateSprintsModal from "components/project/cycles/CreateUpdateCyclesModal";
import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal";
import CycleStatsView from "components/project/cycles/stats-view";
// headless ui
import { Popover, Transition } from "@headlessui/react";
// ui
import { BreadcrumbItem, Breadcrumbs, HeaderButton, Spinner, EmptySpace, EmptySpaceItem } from "ui";
// icons
import { ArrowPathIcon, ChevronDownIcon, PlusIcon } from "@heroicons/react/24/outline";
import { ArrowPathIcon, PlusIcon } from "@heroicons/react/24/outline";
// types
import { IIssue, ICycle, SelectSprintType, SelectIssue, Properties } from "types";
// constants
import { classNames, replaceUnderscoreIfSnakeCase } from "constants/common";
import { ICycle, SelectSprintType } from "types";
// fetching keys
import { CYCLE_LIST } from "constants/fetch-keys";
const ProjectSprints: NextPage = () => {
const [isOpen, setIsOpen] = useState(false);
const [selectedSprint, setSelectedSprint] = useState<SelectSprintType>();
const [selectedCycle, setSelectedCycle] = useState<SelectSprintType>();
const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false);
const [isIssueModalOpen, setIsIssueModalOpen] = useState(false);
const [selectedIssues, setSelectedIssues] = useState<SelectIssue>();
const [deleteIssue, setDeleteIssue] = useState<string | undefined>();
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
const [cycleId, setCycleId] = useState("");
const { activeWorkspace, activeProject, issues } = useUser();
const { activeWorkspace, activeProject } = useUser();
const router = useRouter();
@ -57,45 +42,13 @@ const ProjectSprints: NextPage = () => {
: null
);
const [properties, setProperties] = useIssuesProperties(
activeWorkspace?.slug,
projectId as string
);
const openCreateIssueModal = (
cycleId: string,
issue?: IIssue,
actionType: "create" | "edit" | "delete" = "create"
) => {
const cycle = cycles?.find((cycle) => cycle.id === cycleId);
if (cycle) {
setSelectedSprint({
...cycle,
actionType: "create-issue",
});
if (issue) setSelectedIssues({ ...issue, actionType });
setIsIssueModalOpen(true);
}
};
const openIssuesListModal = (cycleId: string) => {
setCycleId(cycleId);
setCycleIssuesListModal(true);
};
useEffect(() => {
if (isOpen) return;
if (createUpdateCycleModal) return;
const timer = setTimeout(() => {
setSelectedSprint(undefined);
setSelectedCycle(undefined);
clearTimeout(timer);
}, 500);
}, [isOpen]);
useEffect(() => {
if (selectedIssues?.actionType === "delete") {
setDeleteIssue(selectedIssues.id);
}
}, [selectedIssues]);
}, [createUpdateCycleModal]);
return (
<AppLayout
@ -109,101 +62,33 @@ const ProjectSprints: NextPage = () => {
</Breadcrumbs>
}
right={
<div className="flex items-center gap-2">
<Popover className="relative">
{({ open }) => (
<>
<Popover.Button
className={classNames(
open ? "bg-gray-100 text-gray-900" : "text-gray-500",
"group flex gap-2 items-center rounded-md bg-transparent text-xs font-medium hover:bg-gray-100 hover:text-gray-900 focus:outline-none border p-2"
)}
>
<span>View</span>
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
</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 mr-5 right-1/2 z-10 mt-1 w-screen max-w-xs translate-x-1/2 transform p-4 bg-white rounded-lg shadow-lg overflow-hidden">
<div className="relative flex flex-col gap-1 gap-y-4">
<div className="relative flex flex-col gap-1">
<h4 className="text-base text-gray-600">Properties</h4>
<div>
{Object.keys(properties).map((key) => (
<button
key={key}
type="button"
className={`px-2 py-1 inline capitalize rounded border border-theme text-sm m-1 ${
properties[key as keyof Properties]
? "border-theme bg-theme text-white"
: ""
}`}
onClick={() => setProperties(key as keyof Properties)}
>
{replaceUnderscoreIfSnakeCase(key)}
</button>
))}
</div>
</div>
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
<HeaderButton Icon={PlusIcon} label="Add Cycle" onClick={() => setIsOpen(true)} />
</div>
<HeaderButton
Icon={PlusIcon}
label="Add Cycle"
onClick={() => {
const e = new KeyboardEvent("keydown", {
ctrlKey: true,
key: "q",
});
document.dispatchEvent(e);
}}
/>
}
>
<CreateUpdateSprintsModal
isOpen={
isOpen &&
selectedSprint?.actionType !== "delete" &&
selectedSprint?.actionType !== "create-issue"
}
setIsOpen={setIsOpen}
data={selectedSprint}
<CreateUpdateCycleModal
isOpen={createUpdateCycleModal}
setIsOpen={setCreateUpdateCycleModal}
projectId={projectId as string}
/>
<ConfirmSprintDeletion
isOpen={isOpen && !!selectedSprint && selectedSprint.actionType === "delete"}
setIsOpen={setIsOpen}
data={selectedSprint}
/>
<ConfirmIssueDeletion
handleClose={() => setDeleteIssue(undefined)}
isOpen={!!deleteIssue}
data={selectedIssues}
/>
<CreateUpdateIssuesModal
isOpen={
isIssueModalOpen &&
selectedSprint?.actionType === "create-issue" &&
selectedIssues?.actionType !== "delete"
}
data={selectedIssues}
prePopulateData={{ sprints: selectedSprint?.id }}
setIsOpen={setIsOpen}
projectId={projectId as string}
/>
<CycleIssuesListModal
isOpen={cycleIssuesListModal}
handleClose={() => setCycleIssuesListModal(false)}
issues={issues}
cycleId={cycleId}
data={selectedCycle}
/>
{cycles ? (
cycles.length > 0 ? (
<div className="space-y-5">
<CycleStatsView cycles={cycles} />
<CycleStatsView
cycles={cycles}
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
setSelectedCycle={setSelectedCycle}
/>
</div>
) : (
<div className="w-full h-full flex flex-col justify-center items-center px-4">
@ -221,7 +106,7 @@ const ProjectSprints: NextPage = () => {
</span>
}
Icon={PlusIcon}
action={() => setIsOpen(true)}
action={() => setCreateUpdateCycleModal(true)}
/>
</EmptySpace>
</div>

View File

@ -8,7 +8,7 @@ import React, { useCallback, useEffect, useState } from "react";
// swr
import useSWR, { mutate } from "swr";
// react hook form
import { useForm, Controller } from "react-hook-form";
import { useForm } from "react-hook-form";
// headless ui
import { Disclosure, Menu, Tab, Transition } from "@headlessui/react";
// services
@ -28,7 +28,7 @@ import AppLayout from "layouts/app-layout";
// components
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
import IssueCommentSection from "components/project/issues/issue-detail/comment/IssueCommentSection";
import AddAsSubIssue from "components/command-palette/addAsSubIssue";
import AddAsSubIssue from "components/project/issues/issue-detail/add-as-sub-issue";
import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion";
// common
import { debounce } from "constants/common";

View File

@ -219,6 +219,7 @@ const ProjectIssues: NextPage = () => {
groupByOptions.find((option) => option.key === groupByProperty)?.name ??
"Select"
}
width="auto"
>
{groupByOptions.map((option) => (
<CustomMenu.MenuItem
@ -237,6 +238,7 @@ const ProjectIssues: NextPage = () => {
orderByOptions.find((option) => option.key === orderBy)?.name ??
"Select"
}
width="auto"
>
{orderByOptions.map((option) =>
groupByProperty === "priority" && option.key === "priority" ? null : (
@ -257,6 +259,7 @@ const ProjectIssues: NextPage = () => {
filterIssueOptions.find((option) => option.key === filterIssue)?.name ??
"Select"
}
width="auto"
>
{filterIssueOptions.map((option) => (
<CustomMenu.MenuItem

View File

@ -0,0 +1,104 @@
// next
import type { NextPage } from "next";
import useSWR from "swr";
import { useRouter } from "next/router";
// layouts
import AppLayout from "layouts/app-layout";
// hoc
import withAuth from "lib/hoc/withAuthWrapper";
// services
import modulesService from "lib/services/modules.service";
// hooks
import useUser from "lib/hooks/useUser";
// ui
import { BreadcrumbItem, Breadcrumbs, EmptySpace, EmptySpaceItem, HeaderButton, Spinner } from "ui";
// icons
import { PlusIcon, RectangleGroupIcon } from "@heroicons/react/24/outline";
// types
import { IModule } from "types/modules";
// fetch-keys
import { MODULE_LIST } from "constants/fetch-keys";
const ProjectModules: NextPage = () => {
const { activeWorkspace, activeProject } = useUser();
const router = useRouter();
const { projectId } = router.query;
const { data: modules } = useSWR<IModule[]>(
activeWorkspace && projectId ? MODULE_LIST(projectId as string) : null,
activeWorkspace && projectId
? () => modulesService.getModules(activeWorkspace.slug, projectId as string)
: null
);
console.log(modules);
return (
<AppLayout
meta={{
title: "Plane - Modules",
}}
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link="/projects" />
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Modules`} />
</Breadcrumbs>
}
right={
<HeaderButton
Icon={PlusIcon}
label="Add Module"
onClick={() => {
const e = new KeyboardEvent("keydown", {
ctrlKey: true,
key: "m",
});
document.dispatchEvent(e);
}}
/>
}
>
{modules ? (
modules.length > 0 ? (
<div className="space-y-5">
{modules.map((module) => (
<div key={module.id} className="bg-white p-3 rounded-md">
<h3>{module.name}</h3>
<p className="text-gray-500 text-sm mt-2">{module.description}</p>
</div>
))}
</div>
) : (
<div className="w-full h-full flex flex-col justify-center items-center px-4">
<EmptySpace
title="You don't have any module yet."
description="A cycle is a fixed time period where a team commits to a set number of issues from their backlog. Cycles are usually one, two, or four weeks long."
Icon={RectangleGroupIcon}
>
<EmptySpaceItem
title="Create a new module"
description={
<span>
Use <pre className="inline bg-gray-100 px-2 py-1 rounded">Ctrl/Command + Q</pre>{" "}
shortcut to create a new cycle
</span>
}
Icon={PlusIcon}
action={() => {
return;
}}
/>
</EmptySpace>
</div>
)
) : (
<div className="w-full h-full flex justify-center items-center">
<Spinner />
</div>
)}
</AppLayout>
);
};
export default withAuth(ProjectModules);

View File

@ -57,7 +57,23 @@ const Projects: NextPage = () => {
};
return (
<AppLayout>
<AppLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title={`${activeWorkspace?.name ?? "Workspace"} Projects`} />
</Breadcrumbs>
}
right={
<HeaderButton
Icon={PlusIcon}
label="Add Project"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "p", ctrlKey: true });
document.dispatchEvent(e);
}}
/>
}
>
<ConfirmProjectDeletion
isOpen={!!deleteProject}
onClose={() => setDeleteProject(null)}
@ -91,20 +107,6 @@ const Projects: NextPage = () => {
</div>
) : (
<div className="h-full w-full space-y-5">
<Breadcrumbs>
<BreadcrumbItem title={`${activeWorkspace?.name ?? "Workspace"} Projects`} />
</Breadcrumbs>
<div className="flex items-center justify-between cursor-pointer w-full">
<h2 className="text-2xl font-medium">Projects</h2>
<HeaderButton
Icon={PlusIcon}
label="Add Project"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "p", ctrlKey: true });
document.dispatchEvent(e);
}}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{projects.map((item) => (
<ProjectMemberInvitations

View File

@ -1,4 +1,4 @@
import type { IUser, IIssue } from "./";
import type { IUser, IIssue } from ".";
export interface ICycle {
id: string;

View File

@ -1,10 +1,11 @@
export * from "./users";
export * from "./workspace";
export * from "./sprints";
export * from "./cycles";
export * from "./projects";
export * from "./state";
export * from "./invitation";
export * from "./issues";
export * from "./modules";
export type NestedKeyOf<ObjectType extends object> = {
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object

View File

@ -1,4 +1,4 @@
import type { IState, IUser, IProject } from "./";
import type { IState, IUser, IProject, ICycle } from "./";
export interface IssueResponse {
next_cursor: string;
@ -17,6 +17,7 @@ export interface IIssue {
label_details: any[];
assignee_details: IUser[];
assignees_list: string[];
bridge?: string;
blocked_by_issue_details: any[];
blocked_issues: BlockeIssue[];
blocker_issues: BlockeIssue[];
@ -26,6 +27,18 @@ export interface IIssue {
updated_at: Date;
name: string;
// TODO change type of description
issue_cycle: {
created_at: Date;
created_by: string;
cycle: string;
cycle_detail: ICycle;
id: string;
issue: string;
project: string;
updated_at: Date;
updated_by: string;
workspace: string;
};
description: any;
priority: string | null;
start_date: string | null;

28
apps/app/types/modules.d.ts vendored Normal file
View File

@ -0,0 +1,28 @@
import type { IUser, IIssue, IProject } from ".";
export interface IModule {
created_at: Date;
created_by: string;
description: string;
description_text: any;
description_html: any;
id: string;
lead: string | null;
lead_detail: IUserLite;
members_list: string[];
name: string;
project: string;
project_detail: IProject;
start_date: Date | null;
status: "backlog" | "planned" | "in-progress" | "paused" | "completed" | "cancelled";
target_date: Date | null;
updated_at: Date;
updated_by: string;
workspace: string;
}
export type SelectModuleType =
| (IModule & { actionType: "edit" | "delete" | "create-issue" })
| undefined;
export type SelectIssue = (IIssue & { actionType: "edit" | "delete" | "create" }) | undefined;

View File

@ -20,7 +20,7 @@ const Button = React.forwardRef<HTMLButtonElement, Props>(
onClick,
type = "button",
size = "sm",
className,
className = "",
theme = "primary",
disabled = false,
},
@ -39,7 +39,7 @@ const Button = React.forwardRef<HTMLButtonElement, Props>(
disabled ? "opacity-70" : ""
} text-white shadow-sm bg-theme hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 border border-transparent`
: theme === "secondary"
? "border bg-white"
? "border bg-white hover:bg-gray-100"
: theme === "success"
? `${
disabled ? "opacity-70" : ""
@ -54,7 +54,7 @@ const Button = React.forwardRef<HTMLButtonElement, Props>(
: size === "lg"
? "px-4 py-2 text-base"
: "px-2.5 py-2 text-sm",
className || ""
className
)}
>
{children}

View File

@ -4,18 +4,30 @@ import Link from "next/link";
// headless ui
import { Menu, Transition } from "@headlessui/react";
// icons
import { ChevronDownIcon } from "@heroicons/react/20/solid";
// commons
import { classNames } from "constants/common";
import { ChevronDownIcon, EllipsisHorizontalIcon } from "@heroicons/react/24/outline";
// types
import type { MenuItemProps, Props } from "./types";
// constants
import { classNames } from "constants/common";
const CustomMenu = ({ children, label, textAlignment }: Props) => {
const CustomMenu = ({
children,
label,
className = "",
ellipsis = false,
width,
textAlignment,
}: Props) => {
return (
<Menu as="div" className="relative inline-block text-left">
<Menu as="div" className={`relative text-left ${className}`}>
<div>
{ellipsis ? (
<Menu.Button className="grid relative place-items-center hover:bg-gray-100 rounded p-1 focus:outline-none">
<EllipsisHorizontalIcon className="h-4 w-4" />
</Menu.Button>
) : (
<Menu.Button
className={`inline-flex w-32 justify-between gap-x-4 rounded-md border border-gray-300 bg-white px-4 py-1 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-gray-100 ${
className={`flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 w-full py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 ${
textAlignment === "right"
? "text-right"
: textAlignment === "center"
@ -23,9 +35,10 @@ const CustomMenu = ({ children, label, textAlignment }: Props) => {
: "text-left"
}`}
>
<span className="truncate w-20">{label}</span>
<ChevronDownIcon className="-mr-1 ml-2 h-5 w-5" aria-hidden="true" />
{label}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</Menu.Button>
)}
</div>
<Transition
@ -37,7 +50,11 @@ const CustomMenu = ({ children, label, textAlignment }: Props) => {
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-10 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 right-0 z-10 mt-2 origin-top-right rounded-md bg-white text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none ${
width === "auto" ? "min-w-full whitespace-nowrap" : "w-56"
}`}
>
<div className="py-1">{children}</div>
</Menu.Items>
</Transition>
@ -48,14 +65,12 @@ const CustomMenu = ({ children, label, textAlignment }: Props) => {
const MenuItem: React.FC<MenuItemProps> = ({ children, renderAs, href, onClick }) => {
return (
<Menu.Item>
{({ active }) =>
{({ active, close }) =>
renderAs === "a" ? (
<Link href={href ?? ""}>
<a
className={classNames(
active ? "bg-gray-100 text-gray-900" : "text-gray-700",
"block px-4 py-2 text-sm"
)}
className="block p-2 text-gray-700 hover:bg-indigo-50 hover:text-gray-900"
onClick={close}
>
{children}
</a>
@ -65,8 +80,8 @@ const MenuItem: React.FC<MenuItemProps> = ({ children, renderAs, href, onClick }
type="button"
onClick={onClick}
className={classNames(
active ? "bg-gray-100 text-gray-900" : "text-gray-700",
"block w-full px-4 py-2 text-left text-sm"
active ? "bg-indigo-50 text-gray-900" : "text-gray-700",
"block w-full p-2 text-left"
)}
>
{children}

View File

@ -1,6 +1,9 @@
export type Props = {
children: React.ReactNode;
label: string;
label?: string | JSX.Element;
className?: string;
ellipsis?: boolean;
width?: "auto";
textAlignment?: "left" | "center" | "right";
};

View File

@ -0,0 +1,78 @@
import React from "react";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// icons
import { ChevronDownIcon } from "@heroicons/react/20/solid";
type CustomSelectProps = {
value: any;
onChange: (props: any) => void;
children: React.ReactNode;
label: string | JSX.Element;
textAlignment?: "left" | "center" | "right";
};
const CustomSelect = ({ children, label, textAlignment, value, onChange }: CustomSelectProps) => {
return (
<Listbox
as="div"
value={value}
onChange={onChange}
className="relative text-left flex-shrink-0"
>
<div>
<Listbox.Button
className={`flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 w-full py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 ${
textAlignment === "right"
? "text-right"
: textAlignment === "center"
? "text-center"
: "text-left"
}`}
>
{label}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</Listbox.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"
>
<Listbox.Options className="absolute right-0 z-10 mt-1 w-56 origin-top-right rounded-md bg-white text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">{children}</div>
</Listbox.Options>
</Transition>
</Listbox>
);
};
type OptionProps = {
children: string | JSX.Element;
value: string;
className?: string;
};
const Option: React.FC<OptionProps> = ({ children, value, className }) => {
return (
<Listbox.Option
value={value}
className={({ active, selected }) =>
`${
active || selected ? "bg-indigo-50" : ""
} flex items-center gap-2 text-gray-900 cursor-pointer select-none relative p-2 truncate ${className}`
}
>
{children}
</Listbox.Option>
);
};
CustomSelect.Option = Option;
export default CustomSelect;