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

@ -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">
<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"
>
<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" : ""
}`}
>
{cycle.name}
</a>
</Link>
</Menu.Item>
))}
</Menu.Items>
</Transition>
</Menu>
<CustomMenu
label={
<>
<ArrowPathIcon className="h-3 w-3" />
{cycles?.find((c) => c.id === cycleId)?.name}
</>
}
className="ml-1.5"
>
{cycles?.map((cycle) => (
<CustomMenu.MenuItem
key={cycle.id}
renderAs="a"
href={`/projects/${activeProject?.id}/cycles/${cycle.id}`}
>
{cycle.name}
</CustomMenu.MenuItem>
))}
</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