forked from github/plane
[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:
parent
febf19ccc0
commit
ab6f1ef780
@ -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()
|
||||
)
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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)}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -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";
|
||||
|
39
web/components/cycles/list/cycle-list-group-header.tsx
Normal file
39
web/components/cycles/list/cycle-list-group-header.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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 ? (
|
||||
|
@ -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";
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
</>
|
||||
|
Loading…
Reference in New Issue
Block a user