[WEB-1298] chore: project cycle revamp (#4454)

* chore: cycle endpoint changes

* chore: completed cycle icon updated

* chore: project cycle list revamp and code refactor

* chore: cycle page improvement

* chore: added created by in retrieve endopoint

* fix: build error

* chore: cycle list page disclosure button improvement

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
Anmol Singh Bhatia 2024-05-14 19:22:08 +05:30 committed by GitHub
parent febf19ccc0
commit ab6f1ef780
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 230 additions and 288 deletions

View File

@ -241,6 +241,7 @@ class CycleViewSet(BaseViewSet):
"backlog_issues",
"assignee_ids",
"status",
"created_by"
)
if data:
@ -365,6 +366,7 @@ class CycleViewSet(BaseViewSet):
"backlog_issues",
"assignee_ids",
"status",
"created_by",
)
return Response(data, status=status.HTTP_200_OK)
@ -564,6 +566,7 @@ class CycleViewSet(BaseViewSet):
"backlog_issues",
"assignee_ids",
"status",
"created_by",
)
.first()
)

View File

@ -3,8 +3,8 @@ import * as React from "react";
import { ISvgIcons } from "../type";
export const CircleDotFullIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
<svg viewBox="0 0 16 16" className={`${className} stroke-1`} fill="none" xmlns="http://www.w3.org/2000/svg" {...rest}>
<circle cx="8.33333" cy="8.33333" r="5.33333" stroke="currentColor" strokeLinecap="round" />
<circle cx="8.33333" cy="8.33333" r="4.33333" fill="currentColor" />
<svg viewBox="0 0 24 24" className={`${className} stroke-1`} fill="none" xmlns="http://www.w3.org/2000/svg" {...rest}>
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" strokeLinecap="round" />
<circle cx="12" cy="12" r="6.25" fill="currentColor" stroke-width="0.5" />
</svg>
);

View File

@ -3,6 +3,7 @@ import { observer } from "mobx-react";
import Link from "next/link";
import useSWR from "swr";
import { CalendarCheck } from "lucide-react";
// headless ui
import { Tab } from "@headlessui/react";
// types
import { ICycle, TIssue } from "@plane/types";
@ -60,7 +61,7 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
const cycleIssues = activeCycleIssues ?? [];
return (
<div className="flex flex-col gap-4 p-4 min-h-[17rem] overflow-hidden col-span-1 lg:col-span-2 xl:col-span-1 border border-custom-border-200 rounded-lg">
<div className="flex flex-col gap-4 p-4 min-h-[17rem] overflow-hidden bg-custom-background-100 col-span-1 lg:col-span-2 xl:col-span-1 border border-custom-border-200 rounded-lg">
<Tab.Group
as={Fragment}
defaultIndex={currentValue(tab)}

View File

@ -15,7 +15,7 @@ export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = (props)
const { cycle } = props;
return (
<div className="flex flex-col justify-center min-h-[17rem] gap-5 py-4 px-3.5 border border-custom-border-200 rounded-lg">
<div className="flex flex-col justify-center min-h-[17rem] gap-5 py-4 px-3.5 bg-custom-background-100 border border-custom-border-200 rounded-lg">
<div className="flex items-center justify-between gap-4">
<h3 className="text-base text-custom-text-300 font-semibold">Issue burndown</h3>
</div>

View File

@ -31,7 +31,7 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
};
return (
<div className="flex flex-col min-h-[17rem] gap-5 py-4 px-3.5 border border-custom-border-200 rounded-lg">
<div className="flex flex-col min-h-[17rem] gap-5 py-4 px-3.5 bg-custom-background-100 border border-custom-border-200 rounded-lg">
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between gap-4">
<h3 className="text-base text-custom-text-300 font-semibold">Progress</h3>

View File

@ -1,20 +1,21 @@
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// ui
import { Disclosure } from "@headlessui/react";
import { Loader } from "@plane/ui";
// components
import {
ActiveCycleHeader,
ActiveCycleProductivity,
ActiveCycleProgress,
ActiveCycleStats,
UpcomingCyclesList,
CycleListGroupHeader,
CyclesListItem,
} from "@/components/cycles";
import { EmptyState } from "@/components/empty-state";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// hooks
import { useCycle, useCycleFilter } from "@/hooks/store";
import { useCycle } from "@/hooks/store";
interface IActiveCycleDetails {
workspaceSlug: string;
@ -25,10 +26,7 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
// props
const { workspaceSlug, projectId } = props;
// store hooks
const { fetchActiveCycle, currentProjectActiveCycleId, currentProjectUpcomingCycleIds, getActiveCycleById } =
useCycle();
// cycle filters hook
const { updateDisplayFilters } = useCycleFilter();
const { fetchActiveCycle, currentProjectActiveCycleId, getActiveCycleById } = useCycle();
// derived values
const activeCycle = currentProjectActiveCycleId ? getActiveCycleById(currentProjectActiveCycleId) : null;
// fetch active cycle details
@ -37,11 +35,6 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
workspaceSlug && projectId ? () => fetchActiveCycle(workspaceSlug, projectId) : null
);
const handleEmptyStateAction = () =>
updateDisplayFilters(projectId, {
active_tab: "all",
});
// show loader if active cycle is loading
if (!activeCycle && isLoading)
return (
@ -50,43 +43,40 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
</Loader>
);
if (!activeCycle) {
// show empty state if no active cycle is present
if (currentProjectUpcomingCycleIds?.length === 0)
return <EmptyState type={EmptyStateType.PROJECT_CYCLE_ACTIVE} size="sm" />;
// show upcoming cycles list, if present
else
return (
<>
<div className="mb-6 grid h-52 w-full place-items-center">
<div className="text-center">
<h5 className="mb-1 text-xl font-medium">No active cycle</h5>
<p className="text-base text-custom-text-400">
Create new cycles to find them here or check
<br />
{"'"}All{"'"} cycles tab to see all cycles or{" "}
<button type="button" className="font-medium text-custom-primary-100" onClick={handleEmptyStateAction}>
click here
</button>
</p>
</div>
</div>
<UpcomingCyclesList handleEmptyStateAction={handleEmptyStateAction} />
</>
);
}
return (
<>
<div className="flex flex-col gap-4">
<ActiveCycleHeader cycle={activeCycle} workspaceSlug={workspaceSlug} projectId={projectId} />
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3">
<ActiveCycleProgress cycle={activeCycle} />
<ActiveCycleProductivity cycle={activeCycle} />
<ActiveCycleStats cycle={activeCycle} workspaceSlug={workspaceSlug} projectId={projectId} />
</div>
</div>
{currentProjectUpcomingCycleIds && <UpcomingCyclesList handleEmptyStateAction={handleEmptyStateAction} />}
<Disclosure as="div" className="flex flex-shrink-0 flex-col" defaultOpen>
{({ open }) => (
<>
<Disclosure.Button className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 px-7 py-1 cursor-pointer">
<CycleListGroupHeader title="Active cycle" type="current" isExpanded={open} />
</Disclosure.Button>
<Disclosure.Panel>
{!activeCycle ? (
<EmptyState type={EmptyStateType.PROJECT_CYCLE_ACTIVE} size="sm" />
) : (
<div className="flex flex-col bg-custom-background-90 border-b">
{currentProjectActiveCycleId && (
<CyclesListItem
key={currentProjectActiveCycleId}
cycleId={currentProjectActiveCycleId}
workspaceSlug={workspaceSlug}
projectId={projectId}
/>
)}
<div className="bg-custom-background-90 py-6 px-8">
<div className="grid grid-cols-1 bg-custom-background-90 gap-3 lg:grid-cols-2 xl:grid-cols-3">
<ActiveCycleProgress cycle={activeCycle} />
<ActiveCycleProductivity cycle={activeCycle} />
<ActiveCycleStats cycle={activeCycle} workspaceSlug={workspaceSlug} projectId={projectId} />
</div>
</div>
</div>
)}
</Disclosure.Panel>
</>
)}
</Disclosure>
</>
);
});

View File

@ -2,24 +2,17 @@ import { useCallback, useRef, useState } from "react";
import { observer } from "mobx-react";
// icons
import { ListFilter, Search, X } from "lucide-react";
// headless ui
import { Tab } from "@headlessui/react";
// types
import { TCycleFilters } from "@plane/types";
// ui
import { Tooltip } from "@plane/ui";
// components
import { CycleFiltersSelection } from "@/components/cycles";
import { FiltersDropdown } from "@/components/issues";
// constants
import { CYCLE_TABS_LIST, CYCLE_VIEW_LAYOUTS } from "@/constants/cycle";
// helpers
import { cn } from "@/helpers/common.helper";
import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks
import { useCycleFilter } from "@/hooks/store";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
import { usePlatformOS } from "@/hooks/use-platform-os";
type Props = {
projectId: string;
@ -30,23 +23,13 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
// refs
const inputRef = useRef<HTMLInputElement>(null);
// hooks
const {
currentProjectDisplayFilters,
currentProjectFilters,
searchQuery,
updateDisplayFilters,
updateFilters,
updateSearchQuery,
} = useCycleFilter();
const { isMobile } = usePlatformOS();
const { currentProjectFilters, searchQuery, updateFilters, updateSearchQuery } = useCycleFilter();
// states
const [isSearchOpen, setIsSearchOpen] = useState(searchQuery !== "" ? true : false);
// outside click detector hook
useOutsideClickDetector(inputRef, () => {
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
});
// derived values
const activeLayout = currentProjectDisplayFilters?.layout ?? "list";
const handleFilters = useCallback(
(key: keyof TCycleFilters, value: string | string[]) => {
@ -81,99 +64,57 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
const isFiltersApplied = calculateTotalFilters(currentProjectFilters ?? {}) !== 0;
return (
<div className="h-[50px] flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 border-b border-custom-border-200 px-6 sm:pb-0">
<Tab.List as="div" className="flex items-center overflow-x-scroll">
{CYCLE_TABS_LIST.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
`border-b-2 p-4 text-sm font-medium outline-none ${
selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent"
}`
}
>
{tab.name}
</Tab>
))}
</Tab.List>
{currentProjectDisplayFilters?.active_tab !== "active" && (
<div className="hidden h-full sm:flex items-center gap-3 self-end">
{!isSearchOpen && (
<button
type="button"
className="-mr-5 p-2 hover:bg-custom-background-80 rounded text-custom-text-400 grid place-items-center"
onClick={() => {
setIsSearchOpen(true);
inputRef.current?.focus();
}}
>
<Search className="h-3.5 w-3.5" />
</button>
)}
<div
className={cn(
"ml-auto flex items-center justify-start gap-1 rounded-md border border-transparent bg-custom-background-100 text-custom-text-400 w-0 transition-[width] ease-linear overflow-hidden opacity-0",
{
"w-64 px-2.5 py-1.5 border-custom-border-200 opacity-100": isSearchOpen,
}
)}
>
<Search className="h-3.5 w-3.5" />
<input
ref={inputRef}
className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 placeholder:text-custom-text-400 focus:outline-none"
placeholder="Search"
value={searchQuery}
onChange={(e) => updateSearchQuery(e.target.value)}
onKeyDown={handleInputKeyDown}
/>
{isSearchOpen && (
<button
type="button"
className="grid place-items-center"
onClick={() => {
updateSearchQuery("");
setIsSearchOpen(false);
}}
>
<X className="h-3 w-3" />
</button>
)}
</div>
<FiltersDropdown
icon={<ListFilter className="h-3 w-3" />}
title="Filters"
placement="bottom-end"
isFiltersApplied={isFiltersApplied}
>
<CycleFiltersSelection filters={currentProjectFilters ?? {}} handleFiltersUpdate={handleFilters} />
</FiltersDropdown>
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
{CYCLE_VIEW_LAYOUTS.map((layout) => (
<Tooltip key={layout.key} tooltipContent={layout.title} isMobile={isMobile}>
<button
type="button"
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
activeLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
}`}
onClick={() =>
updateDisplayFilters(projectId, {
layout: layout.key,
})
}
>
<layout.icon
strokeWidth={2}
className={`h-3.5 w-3.5 ${
activeLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"
}`}
/>
</button>
</Tooltip>
))}
</div>
</div>
<div className="flex items-center gap-3">
{!isSearchOpen && (
<button
type="button"
className="-mr-5 p-2 hover:bg-custom-background-80 rounded text-custom-text-400 grid place-items-center"
onClick={() => {
setIsSearchOpen(true);
inputRef.current?.focus();
}}
>
<Search className="h-3.5 w-3.5" />
</button>
)}
<div
className={cn(
"ml-auto flex items-center justify-start gap-1 rounded-md border border-transparent bg-custom-background-100 text-custom-text-400 w-0 transition-[width] ease-linear overflow-hidden opacity-0",
{
"w-64 px-2.5 py-1.5 border-custom-border-200 opacity-100": isSearchOpen,
}
)}
>
<Search className="h-3.5 w-3.5" />
<input
ref={inputRef}
className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 placeholder:text-custom-text-400 focus:outline-none"
placeholder="Search"
value={searchQuery}
onChange={(e) => updateSearchQuery(e.target.value)}
onKeyDown={handleInputKeyDown}
/>
{isSearchOpen && (
<button
type="button"
className="grid place-items-center"
onClick={() => {
updateSearchQuery("");
setIsSearchOpen(false);
}}
>
<X className="h-3 w-3" />
</button>
)}
</div>
<FiltersDropdown
icon={<ListFilter className="h-3 w-3" />}
title="Filters"
placement="bottom-end"
isFiltersApplied={isFiltersApplied}
>
<CycleFiltersSelection filters={currentProjectFilters ?? {}} handleFiltersUpdate={handleFilters} />
</FiltersDropdown>
</div>
);
});

View File

@ -1,12 +1,10 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
import Image from "next/image";
// types
import { TCycleLayoutOptions } from "@plane/types";
// components
import { CyclesBoard, CyclesList, CyclesListGanttChartView } from "@/components/cycles";
import { CyclesList } from "@/components/cycles";
// ui
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "@/components/ui";
import { CycleModuleListLayout } from "@/components/ui";
// hooks
import { useCycle, useCycleFilter } from "@/hooks/store";
// assets
@ -14,29 +12,23 @@ import AllFiltersImage from "public/empty-state/cycle/all-filters.svg";
import NameFilterImage from "public/empty-state/cycle/name-filter.svg";
export interface ICyclesView {
layout: TCycleLayoutOptions;
workspaceSlug: string;
projectId: string;
peekCycle: string | undefined;
}
export const CyclesView: FC<ICyclesView> = observer((props) => {
const { layout, workspaceSlug, projectId, peekCycle } = props;
const { workspaceSlug, projectId } = props;
// store hooks
const { getFilteredCycleIds, getFilteredCompletedCycleIds, loader } = useCycle();
const { getFilteredCycleIds, getFilteredCompletedCycleIds, loader, currentProjectActiveCycleId } = useCycle();
const { searchQuery } = useCycleFilter();
// derived values
const filteredCycleIds = getFilteredCycleIds(projectId, layout === "gantt");
const filteredCycleIds = getFilteredCycleIds(projectId, false);
const filteredCompletedCycleIds = getFilteredCompletedCycleIds(projectId);
const filteredUpcomingCycleIds = (filteredCycleIds ?? []).filter(
(cycleId) => cycleId !== currentProjectActiveCycleId
);
if (loader || !filteredCycleIds)
return (
<>
{layout === "list" && <CycleModuleListLayout />}
{layout === "board" && <CycleModuleBoardLayout />}
{layout === "gantt" && <GanttLayoutLoader />}
</>
);
if (loader || !filteredCycleIds) return <CycleModuleListLayout />;
if (filteredCycleIds.length === 0 && filteredCompletedCycleIds?.length === 0)
return (
@ -59,24 +51,13 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
return (
<>
{layout === "list" && (
<CyclesList
completedCycleIds={filteredCompletedCycleIds ?? []}
cycleIds={filteredCycleIds}
workspaceSlug={workspaceSlug}
projectId={projectId}
/>
)}
{layout === "board" && (
<CyclesBoard
completedCycleIds={filteredCompletedCycleIds ?? []}
cycleIds={filteredCycleIds}
workspaceSlug={workspaceSlug}
projectId={projectId}
peekCycle={peekCycle}
/>
)}
{layout === "gantt" && <CyclesListGanttChartView cycleIds={filteredCycleIds} workspaceSlug={workspaceSlug} />}
<CyclesList
completedCycleIds={filteredCompletedCycleIds ?? []}
upcomingCycleIds={filteredUpcomingCycleIds}
cycleIds={filteredCycleIds}
workspaceSlug={workspaceSlug}
projectId={projectId}
/>
</>
);
});

View File

@ -14,6 +14,7 @@ export * from "./quick-actions";
export * from "./sidebar";
export * from "./transfer-issues-modal";
export * from "./transfer-issues";
export * from "./cycles-view-header";
// archived cycles
export * from "./archived-cycles";

View File

@ -0,0 +1,39 @@
import React, { FC } from "react";
import { ChevronDown } from "lucide-react";
// types
import { TCycleGroups } from "@plane/types";
// icons
import { CycleGroupIcon } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
type Props = {
type: TCycleGroups;
title: string;
count?: number;
showCount?: boolean;
isExpanded?: boolean;
};
export const CycleListGroupHeader: FC<Props> = (props) => {
const { type, title, count, showCount = false, isExpanded = false } = props;
return (
<div className="relative flex items-center justify-between w-full gap-5 py-1.5">
<div className="flex items-center gap-5 flex-shrink-0">
<div className="flex h-5 w-5 flex-shrink-0 items-center justify-center overflow-hidden rounded-sm">
<CycleGroupIcon cycleGroup={type} className="h-5 w-5" />
</div>
<div className="relative flex w-full flex-row items-center gap-1 overflow-hidden">
<div className="inline-block line-clamp-1 truncate font-medium text-custom-text-100">{title}</div>
{showCount && <div className="pl-2 text-sm font-medium text-custom-text-300">{`${count ?? "0"}`}</div>}
</div>
</div>
<ChevronDown
className={cn("h-4 w-4 text-custom-sidebar-text-300 duration-300 ", {
"rotate-180": isExpanded,
})}
/>
</div>
);
};

View File

@ -8,6 +8,7 @@ import { Avatar, AvatarGroup, Tooltip, setPromiseToast } from "@plane/ui";
// components
import { FavoriteStar } from "@/components/core";
import { CycleQuickActions } from "@/components/cycles";
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
// constants
import { CYCLE_STATUS } from "@/constants/cycle";
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "@/constants/event-tracker";
@ -104,6 +105,8 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
});
};
const createdByDetails = cycleDetails.created_by ? getUserDetails(cycleDetails.created_by) : undefined;
return (
<>
{renderDate && (
@ -130,6 +133,9 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
</div>
)}
{/* created by */}
{createdByDetails && <ButtonAvatars showTooltip={false} userIds={createdByDetails?.id} />}
<Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`} isMobile={isMobile}>
<div className="flex w-10 cursor-default items-center justify-center">
{cycleDetails.assignee_ids && cycleDetails.assignee_ids?.length > 0 ? (

View File

@ -2,3 +2,4 @@ export * from "./cycles-list-item";
export * from "./cycles-list-map";
export * from "./root";
export * from "./cycle-list-item-action";
export * from "./cycle-list-group-header";

View File

@ -1,15 +1,13 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
import { ChevronRight } from "lucide-react";
import { Disclosure } from "@headlessui/react";
// components
import { ListLayout } from "@/components/core/list";
import { CyclePeekOverview, CyclesListMap } from "@/components/cycles";
// helpers
import { cn } from "@/helpers/common.helper";
import { ActiveCycleRoot, CycleListGroupHeader, CyclePeekOverview, CyclesListMap } from "@/components/cycles";
export interface ICyclesList {
completedCycleIds: string[];
upcomingCycleIds?: string[] | undefined;
cycleIds: string[];
workspaceSlug: string;
projectId: string;
@ -17,39 +15,62 @@ export interface ICyclesList {
}
export const CyclesList: FC<ICyclesList> = observer((props) => {
const { completedCycleIds, cycleIds, workspaceSlug, projectId, isArchived = false } = props;
const { completedCycleIds, upcomingCycleIds, cycleIds, workspaceSlug, projectId, isArchived = false } = props;
return (
<div className="h-full overflow-y-auto">
<div className="flex h-full w-full justify-between">
<ListLayout>
<CyclesListMap
cycleIds={cycleIds}
projectId={projectId}
workspaceSlug={workspaceSlug}
/>
{completedCycleIds.length !== 0 && (
<Disclosure as="div" className="py-8 pl-3 space-y-4">
<Disclosure.Button className="bg-custom-background-80 font-semibold text-sm py-1 px-2 rounded ml-5 flex items-center gap-1">
<div className="flex h-full w-full justify-between ">
<ListLayout>
{isArchived ? (
<>
<CyclesListMap cycleIds={cycleIds} projectId={projectId} workspaceSlug={workspaceSlug} />
</>
) : (
<>
<ActiveCycleRoot workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
{upcomingCycleIds && (
<Disclosure as="div" className="flex flex-shrink-0 flex-col" defaultOpen>
{({ open }) => (
<>
Completed cycles ({completedCycleIds.length})
<ChevronRight
className={cn("h-3 w-3 transition-all", {
"rotate-90": open,
})}
/>
<Disclosure.Button className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 px-7 py-1 cursor-pointer">
<CycleListGroupHeader
title="Upcoming cycle"
type="upcoming"
count={upcomingCycleIds.length}
showCount
isExpanded={open}
/>
</Disclosure.Button>
<Disclosure.Panel>
<CyclesListMap cycleIds={upcomingCycleIds} projectId={projectId} workspaceSlug={workspaceSlug} />
</Disclosure.Panel>
</>
)}
</Disclosure.Button>
<Disclosure.Panel>
<CyclesListMap cycleIds={completedCycleIds} projectId={projectId} workspaceSlug={workspaceSlug} />
</Disclosure.Panel>
</Disclosure>
)}
<Disclosure as="div" className="flex flex-shrink-0 flex-col pb-7">
{({ open }) => (
<>
<Disclosure.Button className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 px-7 py-1 cursor-pointer">
<CycleListGroupHeader
title="Completed cycle"
type="completed"
count={completedCycleIds.length}
showCount
isExpanded={open}
/>
</Disclosure.Button>
<Disclosure.Panel>
<CyclesListMap cycleIds={completedCycleIds} projectId={projectId} workspaceSlug={workspaceSlug} />
</Disclosure.Panel>
</>
)}
</Disclosure>
)}
</ListLayout>
<CyclePeekOverview projectId={projectId} workspaceSlug={workspaceSlug} isArchived={isArchived} />
</div>
</>
)}
</ListLayout>
<CyclePeekOverview projectId={projectId} workspaceSlug={workspaceSlug} isArchived={isArchived} />
</div>
);
});

View File

@ -5,6 +5,7 @@ import { useRouter } from "next/router";
import { Breadcrumbs, Button, ContrastIcon } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
import { CyclesViewHeader } from "@/components/cycles";
import { ProjectLogo } from "@/components/project";
// constants
import { EUserProjectRoles } from "@/constants/project";
@ -54,8 +55,9 @@ export const CyclesHeader: FC = observer(() => {
</Breadcrumbs>
</div>
</div>
{canUserCreateCycle && (
{canUserCreateCycle && currentProjectDetails && (
<div className="flex items-center gap-3">
<CyclesViewHeader projectId={currentProjectDetails.id} />
<Button
variant="primary"
size="sm"

View File

@ -1,33 +1,25 @@
import { Fragment, useState, ReactElement } from "react";
import { useState, ReactElement } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
import { Tab } from "@headlessui/react";
// types
import { TCycleFilters } from "@plane/types";
// hooks
// components
import { PageHead } from "@/components/core";
import {
CyclesView,
CycleCreateUpdateModal,
CyclesViewHeader,
CycleAppliedFiltersList,
ActiveCycleRoot,
} from "@/components/cycles";
import { CyclesView, CycleCreateUpdateModal, CycleAppliedFiltersList } from "@/components/cycles";
import CyclesListMobileHeader from "@/components/cycles/cycles-list-mobile-header";
import { EmptyState } from "@/components/empty-state";
import { CyclesHeader } from "@/components/headers";
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "@/components/ui";
import { CYCLE_TABS_LIST } from "@/constants/cycle";
import { CycleModuleListLayout } from "@/components/ui";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks
import { useEventTracker, useCycle, useProject, useCycleFilter } from "@/hooks/store";
// layouts
import { AppLayout } from "@/layouts/app-layout";
// components
// ui
// helpers
// types
import { NextPageWithLayout } from "@/lib/types";
// constants
const ProjectCyclesPage: NextPageWithLayout = observer(() => {
// states
@ -38,17 +30,13 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
const { getProjectById, currentProjectDetails } = useProject();
// router
const router = useRouter();
const { workspaceSlug, projectId, peekCycle } = router.query;
const { workspaceSlug, projectId } = router.query;
// cycle filters hook
const { clearAllFilters, currentProjectDisplayFilters, currentProjectFilters, updateDisplayFilters, updateFilters } =
useCycleFilter();
const { clearAllFilters, currentProjectFilters, updateFilters } = useCycleFilter();
// derived values
const totalCycles = currentProjectCycleIds?.length ?? 0;
const project = projectId ? getProjectById(projectId?.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined;
// selected display filters
const cycleTab = currentProjectDisplayFilters?.active_tab;
const cycleLayout = currentProjectDisplayFilters?.layout ?? "list";
const handleRemoveFilter = (key: keyof TCycleFilters, value: string | null) => {
if (!projectId) return;
@ -73,14 +61,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
</div>
);
if (loader)
return (
<>
{cycleLayout === "list" && <CycleModuleListLayout />}
{cycleLayout === "board" && <CycleModuleBoardLayout />}
{cycleLayout === "gantt" && <GanttLayoutLoader />}
</>
);
if (loader) return <CycleModuleListLayout />;
return (
<>
@ -103,21 +84,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
/>
</div>
) : (
<Tab.Group
as="div"
className="flex h-full flex-col overflow-hidden"
defaultIndex={CYCLE_TABS_LIST.findIndex((i) => i.key == cycleTab)}
selectedIndex={CYCLE_TABS_LIST.findIndex((i) => i.key == cycleTab)}
onChange={(i) => {
if (!projectId) return;
const tab = CYCLE_TABS_LIST[i];
if (!tab) return;
updateDisplayFilters(projectId.toString(), {
active_tab: tab.key,
});
}}
>
<CyclesViewHeader projectId={projectId.toString()} />
<>
{calculateTotalFilters(currentProjectFilters ?? {}) !== 0 && (
<div className="border-b border-custom-border-200 px-5 py-3">
<CycleAppliedFiltersList
@ -127,20 +94,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
/>
</div>
)}
<Tab.Panels as={Fragment}>
<Tab.Panel as="div" className="h-full space-y-5 overflow-y-auto p-4 sm:p-5">
<ActiveCycleRoot workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
</Tab.Panel>
<Tab.Panel as="div" className="h-full overflow-y-auto">
<CyclesView
layout={cycleLayout}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
peekCycle={peekCycle?.toString()}
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
<CyclesView workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
</>
)}
</div>
</>