forked from github/plane
style: new cycles list page design with empty states (#1633)
This commit is contained in:
parent
fe60771943
commit
2ce7914b7a
@ -110,8 +110,43 @@ export const ActiveCycleDetails: React.FC = () => {
|
|||||||
|
|
||||||
if (!cycle)
|
if (!cycle)
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full items-center justify-start rounded-[10px] bg-custom-background-80 px-6 py-4">
|
<div className="h-full grid place-items-center text-center">
|
||||||
<h3 className="text-base font-medium text-custom-text-100 ">No active cycle is present.</h3>
|
<div className="space-y-2">
|
||||||
|
<div className="mx-auto flex justify-center">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="66"
|
||||||
|
height="66"
|
||||||
|
viewBox="0 0 66 66"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="34.375"
|
||||||
|
cy="34.375"
|
||||||
|
r="22"
|
||||||
|
stroke="rgb(var(--color-text-400))"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
|
||||||
|
fill="rgb(var(--color-text-400))"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h4 className="text-sm text-custom-text-200">No active cycle</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-custom-primary-100 text-sm outline-none"
|
||||||
|
onClick={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", {
|
||||||
|
key: "q",
|
||||||
|
});
|
||||||
|
document.dispatchEvent(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create a new cycle
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import cyclesService from "services/cycles.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
CreateUpdateCycleModal,
|
CreateUpdateCycleModal,
|
||||||
@ -18,11 +19,7 @@ import {
|
|||||||
SingleCycleList,
|
SingleCycleList,
|
||||||
} from "components/cycles";
|
} from "components/cycles";
|
||||||
// ui
|
// ui
|
||||||
import { EmptyState, Loader } from "components/ui";
|
import { Loader } from "components/ui";
|
||||||
// icons
|
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
|
||||||
// images
|
|
||||||
import emptyCycle from "public/empty-state/cycle.svg";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { getDateRangeStatus } from "helpers/date-time.helper";
|
import { getDateRangeStatus } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
@ -48,6 +45,8 @@ export const CyclesView: React.FC<Props> = ({ cycles, viewType }) => {
|
|||||||
const [deleteCycleModal, setDeleteCycleModal] = useState(false);
|
const [deleteCycleModal, setDeleteCycleModal] = useState(false);
|
||||||
const [selectedCycleToDelete, setSelectedCycleToDelete] = useState<ICycle | null>(null);
|
const [selectedCycleToDelete, setSelectedCycleToDelete] = useState<ICycle | null>(null);
|
||||||
|
|
||||||
|
const { storedValue: cycleTab } = useLocalStorage("cycleTab", "All");
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
@ -206,19 +205,48 @@ export const CyclesView: React.FC<Props> = ({ cycles, viewType }) => {
|
|||||||
<CyclesListGanttChartView cycles={cycles ?? []} />
|
<CyclesListGanttChartView cycles={cycles ?? []} />
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<EmptyState
|
<div className="h-full grid place-items-center text-center">
|
||||||
title="Plan your project with cycles"
|
<div className="space-y-2">
|
||||||
description="Cycle is a custom time period in which a team works to complete items on their backlog."
|
<div className="mx-auto flex justify-center">
|
||||||
image={emptyCycle}
|
<svg
|
||||||
buttonText="New Cycle"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
buttonIcon={<PlusIcon className="h-4 w-4" />}
|
width="66"
|
||||||
onClick={() => {
|
height="66"
|
||||||
const e = new KeyboardEvent("keydown", {
|
viewBox="0 0 66 66"
|
||||||
key: "q",
|
fill="none"
|
||||||
});
|
>
|
||||||
document.dispatchEvent(e);
|
<circle
|
||||||
}}
|
cx="34.375"
|
||||||
/>
|
cy="34.375"
|
||||||
|
r="22"
|
||||||
|
stroke="rgb(var(--color-text-400))"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
|
||||||
|
fill="rgb(var(--color-text-400))"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h4 className="text-sm text-custom-text-200">
|
||||||
|
{cycleTab === "All"
|
||||||
|
? "No cycles"
|
||||||
|
: `No ${cycleTab === "Drafts" ? "draft" : cycleTab?.toLowerCase()} cycles`}
|
||||||
|
</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-custom-primary-100 text-sm outline-none"
|
||||||
|
onClick={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", {
|
||||||
|
key: "q",
|
||||||
|
});
|
||||||
|
document.dispatchEvent(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create a new cycle
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
) : viewType === "list" ? (
|
) : viewType === "list" ? (
|
||||||
<Loader className="space-y-4">
|
<Loader className="space-y-4">
|
||||||
|
@ -2,15 +2,12 @@ import React, { useEffect, useState } from "react";
|
|||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Tab } from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
// hooks
|
// hooks
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
// services
|
import useProjectDetails from "hooks/use-project-details";
|
||||||
import projectService from "services/project.service";
|
|
||||||
// layouts
|
// layouts
|
||||||
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
||||||
// components
|
// components
|
||||||
@ -23,18 +20,33 @@ import {
|
|||||||
UpcomingCyclesList,
|
UpcomingCyclesList,
|
||||||
} from "components/cycles";
|
} from "components/cycles";
|
||||||
// ui
|
// ui
|
||||||
import { PrimaryButton } from "components/ui";
|
import { EmptyState, Icon, PrimaryButton } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// icons
|
// icons
|
||||||
import { ListBulletIcon, PlusIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
|
// images
|
||||||
|
import emptyCycle from "public/empty-state/cycle.svg";
|
||||||
// types
|
// types
|
||||||
import { SelectCycleType } from "types";
|
import { SelectCycleType } from "types";
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
// fetch-keys
|
|
||||||
import { PROJECT_DETAILS } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
const tabsList = ["All", "Active", "Upcoming", "Completed", "Drafts"];
|
const tabsList = ["All", "Active", "Upcoming", "Completed", "Drafts"];
|
||||||
|
|
||||||
|
const cycleViews = [
|
||||||
|
{
|
||||||
|
key: "list",
|
||||||
|
icon: "list",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "board",
|
||||||
|
icon: "dataset",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "gantt",
|
||||||
|
icon: "view_timeline",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const ProjectCycles: NextPage = () => {
|
const ProjectCycles: NextPage = () => {
|
||||||
const [selectedCycle, setSelectedCycle] = useState<SelectCycleType>();
|
const [selectedCycle, setSelectedCycle] = useState<SelectCycleType>();
|
||||||
const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false);
|
const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false);
|
||||||
@ -60,19 +72,14 @@ const ProjectCycles: NextPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const { user } = useUserAuth();
|
const { user } = useUserAuth();
|
||||||
|
const { projectDetails } = useProjectDetails();
|
||||||
const { data: activeProject } = useSWR(
|
|
||||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => projectService.getProject(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (createUpdateCycleModal) return;
|
if (createUpdateCycleModal) return;
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setSelectedCycle(undefined);
|
setSelectedCycle(undefined);
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
@ -84,7 +91,7 @@ const ProjectCycles: NextPage = () => {
|
|||||||
breadcrumbs={
|
breadcrumbs={
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
|
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
|
||||||
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Cycles`} />
|
<BreadcrumbItem title={`${projectDetails?.name ?? "Project"} Cycles`} />
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
}
|
}
|
||||||
right={
|
right={
|
||||||
@ -106,46 +113,26 @@ const ProjectCycles: NextPage = () => {
|
|||||||
data={selectedCycle}
|
data={selectedCycle}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
<div className="space-y-5 p-8 h-full flex flex-col overflow-hidden">
|
{projectDetails?.total_cycles === 0 ? (
|
||||||
<div className="flex gap-4 justify-between">
|
<div className="h-full grid place-items-center">
|
||||||
<h3 className="text-2xl font-semibold text-custom-text-100">Cycles</h3>
|
<EmptyState
|
||||||
<div className="flex items-center gap-x-1">
|
title="Plan your project with cycles"
|
||||||
<button
|
description="Cycle is a custom time period in which a team works to complete items on their backlog."
|
||||||
type="button"
|
image={emptyCycle}
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-custom-background-80 ${
|
buttonText="New Cycle"
|
||||||
cyclesView === "list" ? "bg-custom-background-80" : ""
|
buttonIcon={<PlusIcon className="h-4 w-4" />}
|
||||||
}`}
|
onClick={() => {
|
||||||
onClick={() => setCyclesView("list")}
|
const e = new KeyboardEvent("keydown", {
|
||||||
>
|
key: "q",
|
||||||
<ListBulletIcon className="h-4 w-4 text-custom-text-200" />
|
});
|
||||||
</button>
|
document.dispatchEvent(e);
|
||||||
<button
|
}}
|
||||||
type="button"
|
/>
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-custom-background-80 ${
|
|
||||||
cyclesView === "board" ? "bg-custom-background-80" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => setCyclesView("board")}
|
|
||||||
>
|
|
||||||
<Squares2X2Icon className="h-4 w-4 text-custom-text-200" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`grid h-7 w-7 place-items-center rounded outline-none duration-300 hover:bg-custom-background-80 ${
|
|
||||||
cyclesView === "gantt_chart" ? "bg-custom-background-80" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
setCyclesView("gantt_chart");
|
|
||||||
setCycleTab("All");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="material-symbols-rounded text-custom-text-200 text-[18px] rotate-90">
|
|
||||||
waterfall_chart
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
<Tab.Group
|
<Tab.Group
|
||||||
as={React.Fragment}
|
as="div"
|
||||||
|
className="h-full flex flex-col overflow-hidden"
|
||||||
defaultIndex={currentTabValue(cycleTab)}
|
defaultIndex={currentTabValue(cycleTab)}
|
||||||
selectedIndex={currentTabValue(cycleTab)}
|
selectedIndex={currentTabValue(cycleTab)}
|
||||||
onChange={(i) => {
|
onChange={(i) => {
|
||||||
@ -165,50 +152,73 @@ const ProjectCycles: NextPage = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tab.List as="div" className="flex flex-wrap items-center justify-start gap-4 text-base">
|
<div className="flex flex-col sm:flex-row gap-4 justify-between border-b border-custom-border-300 px-4 sm:px-5 pb-4 sm:pb-0">
|
||||||
{tabsList.map((tab, index) => {
|
<Tab.List as="div" className="flex items-center overflow-x-scroll">
|
||||||
if (cyclesView === "gantt_chart" && (tab === "Active" || tab === "Drafts"))
|
{tabsList.map((tab, index) => {
|
||||||
return null;
|
if (cyclesView === "gantt_chart" && (tab === "Active" || tab === "Drafts"))
|
||||||
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab
|
<Tab
|
||||||
key={index}
|
key={index}
|
||||||
className={({ selected }) =>
|
className={({ selected }) =>
|
||||||
`rounded-3xl border px-6 py-1 outline-none ${
|
`border-b-2 p-4 text-sm font-medium outline-none ${
|
||||||
selected
|
selected
|
||||||
? "border-custom-primary bg-custom-primary text-white font-medium"
|
? "border-custom-primary-100 text-custom-primary-100"
|
||||||
: "border-custom-border-200 bg-custom-background-100 hover:bg-custom-background-80"
|
: "border-transparent"
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{tab}
|
{tab}
|
||||||
</Tab>
|
</Tab>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Tab.List>
|
</Tab.List>
|
||||||
|
<div className="justify-end sm:justify-start flex items-center gap-x-1">
|
||||||
|
{cycleViews.map((view) => {
|
||||||
|
if (view.key === "gantt" && (cycleTab === "Active" || cycleTab === "Drafts"))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={view.key}
|
||||||
|
type="button"
|
||||||
|
className={`grid h-8 w-8 place-items-center rounded p-1 outline-none duration-300 hover:bg-custom-background-80 ${
|
||||||
|
cyclesView === view.key
|
||||||
|
? "bg-custom-background-80 text-custom-text-100"
|
||||||
|
: "text-custom-text-200"
|
||||||
|
}`}
|
||||||
|
onClick={() => setCyclesView(view.key)}
|
||||||
|
>
|
||||||
|
<Icon iconName={view.icon} className="!text-base" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Tab.Panels as={React.Fragment}>
|
<Tab.Panels as={React.Fragment}>
|
||||||
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
|
||||||
<AllCyclesList viewType={cyclesView} />
|
<AllCyclesList viewType={cyclesView} />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
{cyclesView !== "gantt_chart" && (
|
{cyclesView !== "gantt_chart" && (
|
||||||
<Tab.Panel as="div" className="mt-7 space-y-5 h-full overflow-y-auto">
|
<Tab.Panel as="div" className="p-4 sm:p-5 space-y-5 h-full overflow-y-auto">
|
||||||
<ActiveCycleDetails />
|
<ActiveCycleDetails />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
)}
|
)}
|
||||||
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
|
||||||
<UpcomingCyclesList viewType={cyclesView} />
|
<UpcomingCyclesList viewType={cyclesView} />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
|
||||||
<CompletedCyclesList viewType={cyclesView} />
|
<CompletedCyclesList viewType={cyclesView} />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
{cyclesView !== "gantt_chart" && (
|
{cyclesView !== "gantt_chart" && (
|
||||||
<Tab.Panel as="div" className="h-full overflow-y-auto">
|
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
|
||||||
<DraftCyclesList viewType={cyclesView} />
|
<DraftCyclesList viewType={cyclesView} />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
)}
|
)}
|
||||||
</Tab.Panels>
|
</Tab.Panels>
|
||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
</div>
|
)}
|
||||||
</ProjectAuthorizationWrapper>
|
</ProjectAuthorizationWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user