mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[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",
|
"backlog_issues",
|
||||||
"assignee_ids",
|
"assignee_ids",
|
||||||
"status",
|
"status",
|
||||||
|
"created_by"
|
||||||
)
|
)
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
@ -365,6 +366,7 @@ class CycleViewSet(BaseViewSet):
|
|||||||
"backlog_issues",
|
"backlog_issues",
|
||||||
"assignee_ids",
|
"assignee_ids",
|
||||||
"status",
|
"status",
|
||||||
|
"created_by",
|
||||||
)
|
)
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@ -564,6 +566,7 @@ class CycleViewSet(BaseViewSet):
|
|||||||
"backlog_issues",
|
"backlog_issues",
|
||||||
"assignee_ids",
|
"assignee_ids",
|
||||||
"status",
|
"status",
|
||||||
|
"created_by",
|
||||||
)
|
)
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
@ -3,8 +3,8 @@ import * as React from "react";
|
|||||||
import { ISvgIcons } from "../type";
|
import { ISvgIcons } from "../type";
|
||||||
|
|
||||||
export const CircleDotFullIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
|
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}>
|
<svg viewBox="0 0 24 24" 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="12" cy="12" r="10" stroke="currentColor" stroke-width="2" strokeLinecap="round" />
|
||||||
<circle cx="8.33333" cy="8.33333" r="4.33333" fill="currentColor" />
|
<circle cx="12" cy="12" r="6.25" fill="currentColor" stroke-width="0.5" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
@ -3,6 +3,7 @@ import { observer } from "mobx-react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { CalendarCheck } from "lucide-react";
|
import { CalendarCheck } from "lucide-react";
|
||||||
|
// headless ui
|
||||||
import { Tab } from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
// types
|
// types
|
||||||
import { ICycle, TIssue } from "@plane/types";
|
import { ICycle, TIssue } from "@plane/types";
|
||||||
@ -60,7 +61,7 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
|||||||
const cycleIssues = activeCycleIssues ?? [];
|
const cycleIssues = activeCycleIssues ?? [];
|
||||||
|
|
||||||
return (
|
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
|
<Tab.Group
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
defaultIndex={currentValue(tab)}
|
defaultIndex={currentValue(tab)}
|
||||||
|
@ -15,7 +15,7 @@ export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = (props)
|
|||||||
const { cycle } = props;
|
const { cycle } = props;
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<h3 className="text-base text-custom-text-300 font-semibold">Issue burndown</h3>
|
<h3 className="text-base text-custom-text-300 font-semibold">Issue burndown</h3>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,7 +31,7 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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 flex-col gap-3">
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<h3 className="text-base text-custom-text-300 font-semibold">Progress</h3>
|
<h3 className="text-base text-custom-text-300 font-semibold">Progress</h3>
|
||||||
|
@ -1,20 +1,21 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// ui
|
// ui
|
||||||
|
import { Disclosure } from "@headlessui/react";
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
ActiveCycleHeader,
|
|
||||||
ActiveCycleProductivity,
|
ActiveCycleProductivity,
|
||||||
ActiveCycleProgress,
|
ActiveCycleProgress,
|
||||||
ActiveCycleStats,
|
ActiveCycleStats,
|
||||||
UpcomingCyclesList,
|
CycleListGroupHeader,
|
||||||
|
CyclesListItem,
|
||||||
} from "@/components/cycles";
|
} from "@/components/cycles";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
// constants
|
// constants
|
||||||
import { EmptyStateType } from "@/constants/empty-state";
|
import { EmptyStateType } from "@/constants/empty-state";
|
||||||
// hooks
|
// hooks
|
||||||
import { useCycle, useCycleFilter } from "@/hooks/store";
|
import { useCycle } from "@/hooks/store";
|
||||||
|
|
||||||
interface IActiveCycleDetails {
|
interface IActiveCycleDetails {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -25,10 +26,7 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
|
|||||||
// props
|
// props
|
||||||
const { workspaceSlug, projectId } = props;
|
const { workspaceSlug, projectId } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { fetchActiveCycle, currentProjectActiveCycleId, currentProjectUpcomingCycleIds, getActiveCycleById } =
|
const { fetchActiveCycle, currentProjectActiveCycleId, getActiveCycleById } = useCycle();
|
||||||
useCycle();
|
|
||||||
// cycle filters hook
|
|
||||||
const { updateDisplayFilters } = useCycleFilter();
|
|
||||||
// derived values
|
// derived values
|
||||||
const activeCycle = currentProjectActiveCycleId ? getActiveCycleById(currentProjectActiveCycleId) : null;
|
const activeCycle = currentProjectActiveCycleId ? getActiveCycleById(currentProjectActiveCycleId) : null;
|
||||||
// fetch active cycle details
|
// fetch active cycle details
|
||||||
@ -37,11 +35,6 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
|
|||||||
workspaceSlug && projectId ? () => fetchActiveCycle(workspaceSlug, projectId) : null
|
workspaceSlug && projectId ? () => fetchActiveCycle(workspaceSlug, projectId) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleEmptyStateAction = () =>
|
|
||||||
updateDisplayFilters(projectId, {
|
|
||||||
active_tab: "all",
|
|
||||||
});
|
|
||||||
|
|
||||||
// show loader if active cycle is loading
|
// show loader if active cycle is loading
|
||||||
if (!activeCycle && isLoading)
|
if (!activeCycle && isLoading)
|
||||||
return (
|
return (
|
||||||
@ -50,43 +43,40 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
|
|||||||
</Loader>
|
</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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-6 grid h-52 w-full place-items-center">
|
<Disclosure as="div" className="flex flex-shrink-0 flex-col" defaultOpen>
|
||||||
<div className="text-center">
|
{({ open }) => (
|
||||||
<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">
|
<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">
|
||||||
<ActiveCycleHeader cycle={activeCycle} workspaceSlug={workspaceSlug} projectId={projectId} />
|
<CycleListGroupHeader title="Active cycle" type="current" isExpanded={open} />
|
||||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3">
|
</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} />
|
<ActiveCycleProgress cycle={activeCycle} />
|
||||||
<ActiveCycleProductivity cycle={activeCycle} />
|
<ActiveCycleProductivity cycle={activeCycle} />
|
||||||
<ActiveCycleStats cycle={activeCycle} workspaceSlug={workspaceSlug} projectId={projectId} />
|
<ActiveCycleStats cycle={activeCycle} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{currentProjectUpcomingCycleIds && <UpcomingCyclesList handleEmptyStateAction={handleEmptyStateAction} />}
|
</div>
|
||||||
|
)}
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Disclosure>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -2,24 +2,17 @@ import { useCallback, useRef, useState } from "react";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// icons
|
// icons
|
||||||
import { ListFilter, Search, X } from "lucide-react";
|
import { ListFilter, Search, X } from "lucide-react";
|
||||||
// headless ui
|
|
||||||
import { Tab } from "@headlessui/react";
|
|
||||||
// types
|
// types
|
||||||
import { TCycleFilters } from "@plane/types";
|
import { TCycleFilters } from "@plane/types";
|
||||||
// ui
|
|
||||||
import { Tooltip } from "@plane/ui";
|
|
||||||
// components
|
// components
|
||||||
import { CycleFiltersSelection } from "@/components/cycles";
|
import { CycleFiltersSelection } from "@/components/cycles";
|
||||||
import { FiltersDropdown } from "@/components/issues";
|
import { FiltersDropdown } from "@/components/issues";
|
||||||
// constants
|
|
||||||
import { CYCLE_TABS_LIST, CYCLE_VIEW_LAYOUTS } from "@/constants/cycle";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useCycleFilter } from "@/hooks/store";
|
import { useCycleFilter } from "@/hooks/store";
|
||||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -30,23 +23,13 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
|
|||||||
// refs
|
// refs
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const { currentProjectFilters, searchQuery, updateFilters, updateSearchQuery } = useCycleFilter();
|
||||||
currentProjectDisplayFilters,
|
|
||||||
currentProjectFilters,
|
|
||||||
searchQuery,
|
|
||||||
updateDisplayFilters,
|
|
||||||
updateFilters,
|
|
||||||
updateSearchQuery,
|
|
||||||
} = useCycleFilter();
|
|
||||||
const { isMobile } = usePlatformOS();
|
|
||||||
// states
|
// states
|
||||||
const [isSearchOpen, setIsSearchOpen] = useState(searchQuery !== "" ? true : false);
|
const [isSearchOpen, setIsSearchOpen] = useState(searchQuery !== "" ? true : false);
|
||||||
// outside click detector hook
|
// outside click detector hook
|
||||||
useOutsideClickDetector(inputRef, () => {
|
useOutsideClickDetector(inputRef, () => {
|
||||||
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
|
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
|
||||||
});
|
});
|
||||||
// derived values
|
|
||||||
const activeLayout = currentProjectDisplayFilters?.layout ?? "list";
|
|
||||||
|
|
||||||
const handleFilters = useCallback(
|
const handleFilters = useCallback(
|
||||||
(key: keyof TCycleFilters, value: string | string[]) => {
|
(key: keyof TCycleFilters, value: string | string[]) => {
|
||||||
@ -81,23 +64,7 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
|
|||||||
const isFiltersApplied = calculateTotalFilters(currentProjectFilters ?? {}) !== 0;
|
const isFiltersApplied = calculateTotalFilters(currentProjectFilters ?? {}) !== 0;
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex items-center gap-3">
|
||||||
<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 && (
|
{!isSearchOpen && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -148,32 +115,6 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
|
|||||||
>
|
>
|
||||||
<CycleFiltersSelection filters={currentProjectFilters ?? {}} handleFiltersUpdate={handleFilters} />
|
<CycleFiltersSelection filters={currentProjectFilters ?? {}} handleFiltersUpdate={handleFilters} />
|
||||||
</FiltersDropdown>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
// types
|
|
||||||
import { TCycleLayoutOptions } from "@plane/types";
|
|
||||||
// components
|
// components
|
||||||
import { CyclesBoard, CyclesList, CyclesListGanttChartView } from "@/components/cycles";
|
import { CyclesList } from "@/components/cycles";
|
||||||
// ui
|
// ui
|
||||||
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "@/components/ui";
|
import { CycleModuleListLayout } from "@/components/ui";
|
||||||
// hooks
|
// hooks
|
||||||
import { useCycle, useCycleFilter } from "@/hooks/store";
|
import { useCycle, useCycleFilter } from "@/hooks/store";
|
||||||
// assets
|
// assets
|
||||||
@ -14,30 +12,24 @@ import AllFiltersImage from "public/empty-state/cycle/all-filters.svg";
|
|||||||
import NameFilterImage from "public/empty-state/cycle/name-filter.svg";
|
import NameFilterImage from "public/empty-state/cycle/name-filter.svg";
|
||||||
|
|
||||||
export interface ICyclesView {
|
export interface ICyclesView {
|
||||||
layout: TCycleLayoutOptions;
|
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
peekCycle: string | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CyclesView: FC<ICyclesView> = observer((props) => {
|
export const CyclesView: FC<ICyclesView> = observer((props) => {
|
||||||
const { layout, workspaceSlug, projectId, peekCycle } = props;
|
const { workspaceSlug, projectId } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getFilteredCycleIds, getFilteredCompletedCycleIds, loader } = useCycle();
|
const { getFilteredCycleIds, getFilteredCompletedCycleIds, loader, currentProjectActiveCycleId } = useCycle();
|
||||||
const { searchQuery } = useCycleFilter();
|
const { searchQuery } = useCycleFilter();
|
||||||
// derived values
|
// derived values
|
||||||
const filteredCycleIds = getFilteredCycleIds(projectId, layout === "gantt");
|
const filteredCycleIds = getFilteredCycleIds(projectId, false);
|
||||||
const filteredCompletedCycleIds = getFilteredCompletedCycleIds(projectId);
|
const filteredCompletedCycleIds = getFilteredCompletedCycleIds(projectId);
|
||||||
|
const filteredUpcomingCycleIds = (filteredCycleIds ?? []).filter(
|
||||||
if (loader || !filteredCycleIds)
|
(cycleId) => cycleId !== currentProjectActiveCycleId
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{layout === "list" && <CycleModuleListLayout />}
|
|
||||||
{layout === "board" && <CycleModuleBoardLayout />}
|
|
||||||
{layout === "gantt" && <GanttLayoutLoader />}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (loader || !filteredCycleIds) return <CycleModuleListLayout />;
|
||||||
|
|
||||||
if (filteredCycleIds.length === 0 && filteredCompletedCycleIds?.length === 0)
|
if (filteredCycleIds.length === 0 && filteredCompletedCycleIds?.length === 0)
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full w-full place-items-center">
|
<div className="grid h-full w-full place-items-center">
|
||||||
@ -59,24 +51,13 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{layout === "list" && (
|
|
||||||
<CyclesList
|
<CyclesList
|
||||||
completedCycleIds={filteredCompletedCycleIds ?? []}
|
completedCycleIds={filteredCompletedCycleIds ?? []}
|
||||||
|
upcomingCycleIds={filteredUpcomingCycleIds}
|
||||||
cycleIds={filteredCycleIds}
|
cycleIds={filteredCycleIds}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{layout === "board" && (
|
|
||||||
<CyclesBoard
|
|
||||||
completedCycleIds={filteredCompletedCycleIds ?? []}
|
|
||||||
cycleIds={filteredCycleIds}
|
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
projectId={projectId}
|
|
||||||
peekCycle={peekCycle}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{layout === "gantt" && <CyclesListGanttChartView cycleIds={filteredCycleIds} workspaceSlug={workspaceSlug} />}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -14,6 +14,7 @@ export * from "./quick-actions";
|
|||||||
export * from "./sidebar";
|
export * from "./sidebar";
|
||||||
export * from "./transfer-issues-modal";
|
export * from "./transfer-issues-modal";
|
||||||
export * from "./transfer-issues";
|
export * from "./transfer-issues";
|
||||||
|
export * from "./cycles-view-header";
|
||||||
|
|
||||||
// archived cycles
|
// archived cycles
|
||||||
export * from "./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
|
// components
|
||||||
import { FavoriteStar } from "@/components/core";
|
import { FavoriteStar } from "@/components/core";
|
||||||
import { CycleQuickActions } from "@/components/cycles";
|
import { CycleQuickActions } from "@/components/cycles";
|
||||||
|
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||||
// constants
|
// constants
|
||||||
import { CYCLE_STATUS } from "@/constants/cycle";
|
import { CYCLE_STATUS } from "@/constants/cycle";
|
||||||
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "@/constants/event-tracker";
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderDate && (
|
{renderDate && (
|
||||||
@ -130,6 +133,9 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* created by */}
|
||||||
|
{createdByDetails && <ButtonAvatars showTooltip={false} userIds={createdByDetails?.id} />}
|
||||||
|
|
||||||
<Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`} isMobile={isMobile}>
|
<Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`} isMobile={isMobile}>
|
||||||
<div className="flex w-10 cursor-default items-center justify-center">
|
<div className="flex w-10 cursor-default items-center justify-center">
|
||||||
{cycleDetails.assignee_ids && cycleDetails.assignee_ids?.length > 0 ? (
|
{cycleDetails.assignee_ids && cycleDetails.assignee_ids?.length > 0 ? (
|
||||||
|
@ -2,3 +2,4 @@ export * from "./cycles-list-item";
|
|||||||
export * from "./cycles-list-map";
|
export * from "./cycles-list-map";
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
export * from "./cycle-list-item-action";
|
export * from "./cycle-list-item-action";
|
||||||
|
export * from "./cycle-list-group-header";
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ChevronRight } from "lucide-react";
|
|
||||||
import { Disclosure } from "@headlessui/react";
|
import { Disclosure } from "@headlessui/react";
|
||||||
// components
|
// components
|
||||||
import { ListLayout } from "@/components/core/list";
|
import { ListLayout } from "@/components/core/list";
|
||||||
import { CyclePeekOverview, CyclesListMap } from "@/components/cycles";
|
import { ActiveCycleRoot, CycleListGroupHeader, CyclePeekOverview, CyclesListMap } from "@/components/cycles";
|
||||||
// helpers
|
|
||||||
import { cn } from "@/helpers/common.helper";
|
|
||||||
|
|
||||||
export interface ICyclesList {
|
export interface ICyclesList {
|
||||||
completedCycleIds: string[];
|
completedCycleIds: string[];
|
||||||
|
upcomingCycleIds?: string[] | undefined;
|
||||||
cycleIds: string[];
|
cycleIds: string[];
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -17,39 +15,62 @@ export interface ICyclesList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CyclesList: FC<ICyclesList> = observer((props) => {
|
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 (
|
return (
|
||||||
<div className="h-full overflow-y-auto">
|
|
||||||
<div className="flex h-full w-full justify-between ">
|
<div className="flex h-full w-full justify-between ">
|
||||||
<ListLayout>
|
<ListLayout>
|
||||||
<CyclesListMap
|
{isArchived ? (
|
||||||
cycleIds={cycleIds}
|
<>
|
||||||
projectId={projectId}
|
<CyclesListMap cycleIds={cycleIds} projectId={projectId} workspaceSlug={workspaceSlug} />
|
||||||
workspaceSlug={workspaceSlug}
|
</>
|
||||||
/>
|
) : (
|
||||||
{completedCycleIds.length !== 0 && (
|
<>
|
||||||
<Disclosure as="div" className="py-8 pl-3 space-y-4">
|
<ActiveCycleRoot workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
||||||
<Disclosure.Button className="bg-custom-background-80 font-semibold text-sm py-1 px-2 rounded ml-5 flex items-center gap-1">
|
|
||||||
|
{upcomingCycleIds && (
|
||||||
|
<Disclosure as="div" className="flex flex-shrink-0 flex-col" defaultOpen>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
Completed cycles ({completedCycleIds.length})
|
<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">
|
||||||
<ChevronRight
|
<CycleListGroupHeader
|
||||||
className={cn("h-3 w-3 transition-all", {
|
title="Upcoming cycle"
|
||||||
"rotate-90": open,
|
type="upcoming"
|
||||||
})}
|
count={upcomingCycleIds.length}
|
||||||
|
showCount
|
||||||
|
isExpanded={open}
|
||||||
/>
|
/>
|
||||||
|
</Disclosure.Button>
|
||||||
|
<Disclosure.Panel>
|
||||||
|
<CyclesListMap cycleIds={upcomingCycleIds} 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.Button>
|
||||||
<Disclosure.Panel>
|
<Disclosure.Panel>
|
||||||
<CyclesListMap cycleIds={completedCycleIds} projectId={projectId} workspaceSlug={workspaceSlug} />
|
<CyclesListMap cycleIds={completedCycleIds} projectId={projectId} workspaceSlug={workspaceSlug} />
|
||||||
</Disclosure.Panel>
|
</Disclosure.Panel>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Disclosure>
|
</Disclosure>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</ListLayout>
|
</ListLayout>
|
||||||
<CyclePeekOverview projectId={projectId} workspaceSlug={workspaceSlug} isArchived={isArchived} />
|
<CyclePeekOverview projectId={projectId} workspaceSlug={workspaceSlug} isArchived={isArchived} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -5,6 +5,7 @@ import { useRouter } from "next/router";
|
|||||||
import { Breadcrumbs, Button, ContrastIcon } from "@plane/ui";
|
import { Breadcrumbs, Button, ContrastIcon } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink } from "@/components/common";
|
import { BreadcrumbLink } from "@/components/common";
|
||||||
|
import { CyclesViewHeader } from "@/components/cycles";
|
||||||
import { ProjectLogo } from "@/components/project";
|
import { ProjectLogo } from "@/components/project";
|
||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "@/constants/project";
|
import { EUserProjectRoles } from "@/constants/project";
|
||||||
@ -54,8 +55,9 @@ export const CyclesHeader: FC = observer(() => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{canUserCreateCycle && (
|
{canUserCreateCycle && currentProjectDetails && (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
<CyclesViewHeader projectId={currentProjectDetails.id} />
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
@ -1,33 +1,25 @@
|
|||||||
import { Fragment, useState, ReactElement } from "react";
|
import { useState, ReactElement } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Tab } from "@headlessui/react";
|
// types
|
||||||
import { TCycleFilters } from "@plane/types";
|
import { TCycleFilters } from "@plane/types";
|
||||||
// hooks
|
// components
|
||||||
import { PageHead } from "@/components/core";
|
import { PageHead } from "@/components/core";
|
||||||
import {
|
import { CyclesView, CycleCreateUpdateModal, CycleAppliedFiltersList } from "@/components/cycles";
|
||||||
CyclesView,
|
|
||||||
CycleCreateUpdateModal,
|
|
||||||
CyclesViewHeader,
|
|
||||||
CycleAppliedFiltersList,
|
|
||||||
ActiveCycleRoot,
|
|
||||||
} from "@/components/cycles";
|
|
||||||
import CyclesListMobileHeader from "@/components/cycles/cycles-list-mobile-header";
|
import CyclesListMobileHeader from "@/components/cycles/cycles-list-mobile-header";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import { CyclesHeader } from "@/components/headers";
|
import { CyclesHeader } from "@/components/headers";
|
||||||
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "@/components/ui";
|
import { CycleModuleListLayout } from "@/components/ui";
|
||||||
import { CYCLE_TABS_LIST } from "@/constants/cycle";
|
// constants
|
||||||
import { EmptyStateType } from "@/constants/empty-state";
|
import { EmptyStateType } from "@/constants/empty-state";
|
||||||
|
// helpers
|
||||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
|
// hooks
|
||||||
import { useEventTracker, useCycle, useProject, useCycleFilter } from "@/hooks/store";
|
import { useEventTracker, useCycle, useProject, useCycleFilter } from "@/hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "@/layouts/app-layout";
|
import { AppLayout } from "@/layouts/app-layout";
|
||||||
// components
|
|
||||||
// ui
|
|
||||||
// helpers
|
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "@/lib/types";
|
import { NextPageWithLayout } from "@/lib/types";
|
||||||
// constants
|
|
||||||
|
|
||||||
const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
||||||
// states
|
// states
|
||||||
@ -38,17 +30,13 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
|||||||
const { getProjectById, currentProjectDetails } = useProject();
|
const { getProjectById, currentProjectDetails } = useProject();
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, peekCycle } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
// cycle filters hook
|
// cycle filters hook
|
||||||
const { clearAllFilters, currentProjectDisplayFilters, currentProjectFilters, updateDisplayFilters, updateFilters } =
|
const { clearAllFilters, currentProjectFilters, updateFilters } = useCycleFilter();
|
||||||
useCycleFilter();
|
|
||||||
// derived values
|
// derived values
|
||||||
const totalCycles = currentProjectCycleIds?.length ?? 0;
|
const totalCycles = currentProjectCycleIds?.length ?? 0;
|
||||||
const project = projectId ? getProjectById(projectId?.toString()) : undefined;
|
const project = projectId ? getProjectById(projectId?.toString()) : undefined;
|
||||||
const pageTitle = project?.name ? `${project?.name} - Cycles` : 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) => {
|
const handleRemoveFilter = (key: keyof TCycleFilters, value: string | null) => {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
@ -73,14 +61,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (loader)
|
if (loader) return <CycleModuleListLayout />;
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{cycleLayout === "list" && <CycleModuleListLayout />}
|
|
||||||
{cycleLayout === "board" && <CycleModuleBoardLayout />}
|
|
||||||
{cycleLayout === "gantt" && <GanttLayoutLoader />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -103,21 +84,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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 && (
|
{calculateTotalFilters(currentProjectFilters ?? {}) !== 0 && (
|
||||||
<div className="border-b border-custom-border-200 px-5 py-3">
|
<div className="border-b border-custom-border-200 px-5 py-3">
|
||||||
<CycleAppliedFiltersList
|
<CycleAppliedFiltersList
|
||||||
@ -127,20 +94,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Tab.Panels as={Fragment}>
|
|
||||||
<Tab.Panel as="div" className="h-full space-y-5 overflow-y-auto p-4 sm:p-5">
|
<CyclesView workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
||||||
<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>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
Loading…
Reference in New Issue
Block a user