diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 621c1dcb7..bc55d9abb 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -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() ) diff --git a/packages/ui/src/icons/cycle/circle-dot-full-icon.tsx b/packages/ui/src/icons/cycle/circle-dot-full-icon.tsx index dd063e79c..f8b528a61 100644 --- a/packages/ui/src/icons/cycle/circle-dot-full-icon.tsx +++ b/packages/ui/src/icons/cycle/circle-dot-full-icon.tsx @@ -3,8 +3,8 @@ import * as React from "react"; import { ISvgIcons } from "../type"; export const CircleDotFullIcon: React.FC = ({ className = "text-current", ...rest }) => ( - - - + + + ); diff --git a/web/components/cycles/active-cycle/cycle-stats.tsx b/web/components/cycles/active-cycle/cycle-stats.tsx index 2eb128763..e31e9af53 100644 --- a/web/components/cycles/active-cycle/cycle-stats.tsx +++ b/web/components/cycles/active-cycle/cycle-stats.tsx @@ -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 = observer((props) => { const cycleIssues = activeCycleIssues ?? []; return ( -
+
= (props) const { cycle } = props; return ( -
+

Issue burndown

diff --git a/web/components/cycles/active-cycle/progress.tsx b/web/components/cycles/active-cycle/progress.tsx index 752f72bcc..6aae998be 100644 --- a/web/components/cycles/active-cycle/progress.tsx +++ b/web/components/cycles/active-cycle/progress.tsx @@ -31,7 +31,7 @@ export const ActiveCycleProgress: FC = (props) => { }; return ( -
+

Progress

diff --git a/web/components/cycles/active-cycle/root.tsx b/web/components/cycles/active-cycle/root.tsx index 60a60abd9..625210fd4 100644 --- a/web/components/cycles/active-cycle/root.tsx +++ b/web/components/cycles/active-cycle/root.tsx @@ -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 = 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 = 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 = observer((props) = ); - if (!activeCycle) { - // show empty state if no active cycle is present - if (currentProjectUpcomingCycleIds?.length === 0) - return ; - // show upcoming cycles list, if present - else - return ( - <> -
-
-
No active cycle
-

- Create new cycles to find them here or check -
- {"'"}All{"'"} cycles tab to see all cycles or{" "} - -

-
-
- - - ); - } - return ( <> -
- -
- - - -
-
- {currentProjectUpcomingCycleIds && } + + {({ open }) => ( + <> + + + + + {!activeCycle ? ( + + ) : ( +
+ {currentProjectActiveCycleId && ( + + )} +
+
+ + + +
+
+
+ )} +
+ + )} +
); }); diff --git a/web/components/cycles/cycles-view-header.tsx b/web/components/cycles/cycles-view-header.tsx index 394084a9c..adb0eea4b 100644 --- a/web/components/cycles/cycles-view-header.tsx +++ b/web/components/cycles/cycles-view-header.tsx @@ -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 = observer((props) => { // refs const inputRef = useRef(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 = observer((props) => { const isFiltersApplied = calculateTotalFilters(currentProjectFilters ?? {}) !== 0; return ( -
- - {CYCLE_TABS_LIST.map((tab) => ( - - `border-b-2 p-4 text-sm font-medium outline-none ${ - selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent" - }` - } - > - {tab.name} - - ))} - - {currentProjectDisplayFilters?.active_tab !== "active" && ( -
- {!isSearchOpen && ( - - )} -
- - updateSearchQuery(e.target.value)} - onKeyDown={handleInputKeyDown} - /> - {isSearchOpen && ( - - )} -
- } - title="Filters" - placement="bottom-end" - isFiltersApplied={isFiltersApplied} - > - - -
- {CYCLE_VIEW_LAYOUTS.map((layout) => ( - - - - ))} -
-
+
+ {!isSearchOpen && ( + )} +
+ + updateSearchQuery(e.target.value)} + onKeyDown={handleInputKeyDown} + /> + {isSearchOpen && ( + + )} +
+ } + title="Filters" + placement="bottom-end" + isFiltersApplied={isFiltersApplied} + > + +
); }); diff --git a/web/components/cycles/cycles-view.tsx b/web/components/cycles/cycles-view.tsx index 2c536d44f..938674f92 100644 --- a/web/components/cycles/cycles-view.tsx +++ b/web/components/cycles/cycles-view.tsx @@ -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 = 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" && } - {layout === "board" && } - {layout === "gantt" && } - - ); + if (loader || !filteredCycleIds) return ; if (filteredCycleIds.length === 0 && filteredCompletedCycleIds?.length === 0) return ( @@ -59,24 +51,13 @@ export const CyclesView: FC = observer((props) => { return ( <> - {layout === "list" && ( - - )} - {layout === "board" && ( - - )} - {layout === "gantt" && } + ); }); diff --git a/web/components/cycles/index.ts b/web/components/cycles/index.ts index b1b718175..12fc7564d 100644 --- a/web/components/cycles/index.ts +++ b/web/components/cycles/index.ts @@ -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"; diff --git a/web/components/cycles/list/cycle-list-group-header.tsx b/web/components/cycles/list/cycle-list-group-header.tsx new file mode 100644 index 000000000..469a83d90 --- /dev/null +++ b/web/components/cycles/list/cycle-list-group-header.tsx @@ -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) => { + const { type, title, count, showCount = false, isExpanded = false } = props; + return ( +
+
+
+ +
+ +
+
{title}
+ {showCount &&
{`${count ?? "0"}`}
} +
+
+ +
+ ); +}; diff --git a/web/components/cycles/list/cycle-list-item-action.tsx b/web/components/cycles/list/cycle-list-item-action.tsx index 05db2e2fa..e16636ebe 100644 --- a/web/components/cycles/list/cycle-list-item-action.tsx +++ b/web/components/cycles/list/cycle-list-item-action.tsx @@ -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 = observer((props) => { }); }; + const createdByDetails = cycleDetails.created_by ? getUserDetails(cycleDetails.created_by) : undefined; + return ( <> {renderDate && ( @@ -130,6 +133,9 @@ export const CycleListItemAction: FC = observer((props) => {
)} + {/* created by */} + {createdByDetails && } +
{cycleDetails.assignee_ids && cycleDetails.assignee_ids?.length > 0 ? ( diff --git a/web/components/cycles/list/index.ts b/web/components/cycles/list/index.ts index 5eda32861..4eebc5779 100644 --- a/web/components/cycles/list/index.ts +++ b/web/components/cycles/list/index.ts @@ -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"; diff --git a/web/components/cycles/list/root.tsx b/web/components/cycles/list/root.tsx index 34e34acf0..4c4852fce 100644 --- a/web/components/cycles/list/root.tsx +++ b/web/components/cycles/list/root.tsx @@ -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 = observer((props) => { - const { completedCycleIds, cycleIds, workspaceSlug, projectId, isArchived = false } = props; + const { completedCycleIds, upcomingCycleIds, cycleIds, workspaceSlug, projectId, isArchived = false } = props; return ( -
-
- - - {completedCycleIds.length !== 0 && ( - - +
+ + {isArchived ? ( + <> + + + ) : ( + <> + + + {upcomingCycleIds && ( + {({ open }) => ( <> - Completed cycles ({completedCycleIds.length}) - + + + + + + )} - - - - + + )} + + + {({ open }) => ( + <> + + + + + + + + )} - )} - - -
+ + )} +
+
); }); diff --git a/web/components/headers/cycles.tsx b/web/components/headers/cycles.tsx index c2be61d82..7b78e27fd 100644 --- a/web/components/headers/cycles.tsx +++ b/web/components/headers/cycles.tsx @@ -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(() => {
- {canUserCreateCycle && ( + {canUserCreateCycle && currentProjectDetails && (
+
); - if (loader) - return ( - <> - {cycleLayout === "list" && } - {cycleLayout === "board" && } - {cycleLayout === "gantt" && } - - ); + if (loader) return ; return ( <> @@ -103,21 +84,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { />
) : ( - 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, - }); - }} - > - + <> {calculateTotalFilters(currentProjectFilters ?? {}) !== 0 && (
{ />
)} - - - - - - - - -
+ + + )}