diff --git a/apps/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/apps/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 5f5ca94c851..45475460225 100644 --- a/apps/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/apps/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -105,7 +105,7 @@ export const BaseKanBanRoot = observer(function BaseKanBanRoot(props: IBaseKanBa fetchNextIssues(groupId, subgroupId); } }, - [fetchNextIssues] + [fetchNextIssues, issues] ); const groupedIssueIds = issues?.groupedIssueIds; @@ -130,9 +130,11 @@ export const BaseKanBanRoot = observer(function BaseKanBanRoot(props: IBaseKanBa const handleOnDrop = useGroupIssuesDragNDrop(storeType, orderBy, group_by, sub_group_by); const canEditProperties = useCallback( - (projectId: string | undefined) => { + (targetProjectId: string | undefined) => { const isEditingAllowedBasedOnProject = - canEditPropertiesBasedOnProject && projectId ? canEditPropertiesBasedOnProject(projectId) : isEditingAllowed; + canEditPropertiesBasedOnProject && targetProjectId + ? canEditPropertiesBasedOnProject(targetProjectId) + : isEditingAllowed; return enableInlineEditing && isEditingAllowedBasedOnProject; }, @@ -161,7 +163,11 @@ export const BaseKanBanRoot = observer(function BaseKanBanRoot(props: IBaseKanBa return combine( dropTargetForElements({ element, - getData: () => ({ columnId: "issue-trash-box", groupId: "issue-trash-box", type: "DELETE" }), + getData: () => ({ + columnId: "issue-trash-box", + groupId: "issue-trash-box", + type: "DELETE", + }), onDragEnter: () => { setIsDragOverDelete(true); }, @@ -231,7 +237,21 @@ export const BaseKanBanRoot = observer(function BaseKanBanRoot(props: IBaseKanBa [workspaceSlug, issuesFilter, projectId, updateFilters] ); - const collapsedGroups = issuesFilter?.issueFilters?.kanbanFilters || { group_by: [], sub_group_by: [] }; + const collapsedGroups = issuesFilter?.issueFilters?.kanbanFilters || { + group_by: [], + sub_group_by: [], + }; + const [expandedGroupIds, setExpandedGroupIds] = useState>(new Set()); + const handleExpandedGroups = useCallback((_toggle: "group_by" | "sub_group_by", value: string) => { + setExpandedGroupIds((currentGroupIds) => { + const nextGroupIds = new Set(currentGroupIds); + + if (nextGroupIds.has(value)) nextGroupIds.delete(value); + else nextGroupIds.add(value); + + return nextGroupIds; + }); + }, []); return ( <> @@ -264,8 +284,8 @@ export const BaseKanBanRoot = observer(function BaseKanBanRoot(props: IBaseKanBa className={`horizontal-scrollbar relative scrollbar-lg flex h-full w-full bg-surface-2 ${sub_group_by ? "vertical-scrollbar overflow-y-auto" : "overflow-x-auto overflow-y-hidden"}`} ref={scrollableContainerRef} > -
-
+
+
void; + expandedGroupIds?: ReadonlySet; + handleExpandedGroups?: (toggle: "group_by" | "sub_group_by", value: string) => void; loadMoreIssues: (groupId?: string, subGroupId?: string) => void; enableQuickIssueCreate?: boolean; quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise; @@ -82,6 +84,8 @@ export const KanBan = observer(function KanBan(props: IKanBan) { quickActions, collapsedGroups, handleCollapsedGroups, + expandedGroupIds, + handleExpandedGroups, enableQuickIssueCreate, quickAddCallback, loadMoreIssues, @@ -161,8 +165,12 @@ export const KanBan = observer(function KanBan(props: IKanBan) { return (
{sub_group_by === null && ( @@ -183,6 +191,8 @@ export const KanBan = observer(function KanBan(props: IKanBan) { addIssuesToView={addIssuesToView} collapsedGroups={collapsedGroups} handleCollapsedGroups={handleCollapsedGroups} + expandedGroupIds={expandedGroupIds} + handleExpandedGroups={handleExpandedGroups} isEpic={isEpic} />
diff --git a/apps/web/core/components/issues/issue-layouts/kanban/headers/group-by-card.tsx b/apps/web/core/components/issues/issue-layouts/kanban/headers/group-by-card.tsx index c3ce5841a87..65203979ac4 100644 --- a/apps/web/core/components/issues/issue-layouts/kanban/headers/group-by-card.tsx +++ b/apps/web/core/components/issues/issue-layouts/kanban/headers/group-by-card.tsx @@ -8,7 +8,7 @@ import React from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // lucide icons -import { Circle } from "lucide-react"; +import { Circle, UnfoldHorizontal, FoldHorizontal } from "lucide-react"; import { AddOutline, ArrowCollapseOutline, FullScreenOutline } from "@makeplane/propel/icons"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { TIssue, ISearchIssueResponse, TIssueKanbanFilters, TIssueGroupByOptions } from "@plane/types"; @@ -30,6 +30,8 @@ interface IHeaderGroupByCard { count: number; collapsedGroups: TIssueKanbanFilters; handleCollapsedGroups: (toggle: "group_by" | "sub_group_by", value: string) => void; + handleExpandedGroups?: (toggle: "group_by" | "sub_group_by", value: string) => void; + expandedGroupIds?: ReadonlySet; issuePayload: Partial; disableIssueCreation?: boolean; addIssuesToView?: (issueIds: string[]) => Promise; @@ -45,6 +47,8 @@ export const HeaderGroupByCard = observer(function HeaderGroupByCard(props: IHea count, collapsedGroups, handleCollapsedGroups, + handleExpandedGroups, + expandedGroupIds, issuePayload, disableIssueCreation, addIssuesToView, @@ -136,12 +140,27 @@ export const HeaderGroupByCard = observer(function HeaderGroupByCard(props: IHea
{sub_group_by === null && ( - + )} + +
)} {!disableIssueCreation && diff --git a/apps/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx b/apps/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx index 9a93e0e6191..3e5d5e36830 100644 --- a/apps/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/apps/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -41,6 +41,8 @@ interface ISubGroupSwimlaneHeader { isSubGroupCumulative: boolean ) => number | undefined; handleCollapsedGroups: (toggle: "group_by" | "sub_group_by", value: string) => void; + expandedGroupIds?: ReadonlySet; + handleExpandedGroups?: (toggle: "group_by" | "sub_group_by", value: string) => void; isEpic?: boolean; list: IGroupByColumn[]; showEmptyGroup: boolean; @@ -64,6 +66,8 @@ const SubGroupSwimlaneHeader = observer(function SubGroupSwimlaneHeader({ getGroupIssueCount, group_by, handleCollapsedGroups, + expandedGroupIds, + handleExpandedGroups, isEpic = false, list, showEmptyGroup, @@ -93,6 +97,8 @@ const SubGroupSwimlaneHeader = observer(function SubGroupSwimlaneHeader({ count={groupCount} collapsedGroups={collapsedGroups} handleCollapsedGroups={handleCollapsedGroups} + expandedGroupIds={expandedGroupIds} + handleExpandedGroups={handleExpandedGroups} issuePayload={_list.payload} disableIssueCreation={getIsWorkflowWorkItemCreationDisabled(_list.id)} isEpic={isEpic} @@ -118,6 +124,8 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { ) => number | undefined; groupedIssueIds: TGroupedIssues | TSubGroupedIssues; handleCollapsedGroups: (toggle: "group_by" | "sub_group_by", value: string) => void; + expandedGroupIds?: ReadonlySet; + handleExpandedGroups?: (toggle: "group_by" | "sub_group_by", value: string) => void; handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise; isEpic?: boolean; issuesMap: IIssueMap; @@ -142,6 +150,8 @@ const SubGroupSwimlane = observer(function SubGroupSwimlane(props: ISubGroupSwim group_by, groupedIssueIds, handleCollapsedGroups, + expandedGroupIds, + handleExpandedGroups, handleOnDrop, isEpic = false, issuesMap, @@ -212,6 +222,8 @@ const SubGroupSwimlane = observer(function SubGroupSwimlane(props: ISubGroupSwim quickActions={quickActions} collapsedGroups={collapsedGroups} handleCollapsedGroups={handleCollapsedGroups} + expandedGroupIds={expandedGroupIds} + handleExpandedGroups={handleExpandedGroups} showEmptyGroup={showEmptyGroup} enableQuickIssueCreate={enableQuickIssueCreate} disableIssueCreation={disableIssueCreation} @@ -250,6 +262,8 @@ export interface IKanBanSwimLanes { group_by: TIssueGroupByOptions | undefined; groupedIssueIds: TGroupedIssues | TSubGroupedIssues; handleCollapsedGroups: (toggle: "group_by" | "sub_group_by", value: string) => void; + expandedGroupIds?: ReadonlySet; + handleExpandedGroups?: (toggle: "group_by" | "sub_group_by", value: string) => void; handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise; isEpic?: boolean; issuesMap: IIssueMap; @@ -276,6 +290,8 @@ export const KanBanSwimLanes = observer(function KanBanSwimLanes(props: IKanBanS quickActions, collapsedGroups, handleCollapsedGroups, + expandedGroupIds, + handleExpandedGroups, loadMoreIssues, showEmptyGroup, handleOnDrop, @@ -314,6 +330,8 @@ export const KanBanSwimLanes = observer(function KanBanSwimLanes(props: IKanBanS sub_group_by={sub_group_by} collapsedGroups={collapsedGroups} handleCollapsedGroups={handleCollapsedGroups} + expandedGroupIds={expandedGroupIds} + handleExpandedGroups={handleExpandedGroups} list={groupByList} showEmptyGroup={showEmptyGroup} isEpic={isEpic} @@ -334,6 +352,8 @@ export const KanBanSwimLanes = observer(function KanBanSwimLanes(props: IKanBanS quickActions={quickActions} collapsedGroups={collapsedGroups} handleCollapsedGroups={handleCollapsedGroups} + expandedGroupIds={expandedGroupIds} + handleExpandedGroups={handleExpandedGroups} loadMoreIssues={loadMoreIssues} showEmptyGroup={showEmptyGroup} handleOnDrop={handleOnDrop} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7e8251a913..c1091fa51f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -876,7 +876,7 @@ importers: version: 2.2.10(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@makeplane/propel': specifier: 'catalog:' - version: 0.2.0(@date-fns/tz@1.4.1)(@types/react@19.2.17)(date-fns@4.1.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.1.17) + version: 0.3.0(@date-fns/tz@1.4.1)(@types/react@19.2.17)(date-fns@4.1.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.1.17) '@plane/constants': specifier: workspace:* version: link:../../packages/constants