Skip to content

Commit 63dc243

Browse files
committed
add collapsed property
1 parent 550adfe commit 63dc243

3 files changed

Lines changed: 42 additions & 43 deletions

File tree

client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { MultiBaseComp } from "lowcoder-core";
22
import { BoolCodeControl, StringControl } from "comps/controls/codeControl";
3-
import { valueComp } from "comps/generators";
3+
import { BoolControl } from "comps/controls/boolControl";
4+
import { valueComp, withPropertyViewFn } from "comps/generators";
45
import { list } from "comps/generators/list";
56
import {
67
parseChildrenFromValueAndChildrenMap,
@@ -16,16 +17,21 @@ import { genRandomKey } from "comps/utils/idGenerator";
1617
import { LayoutActionComp } from "comps/comps/layout/layoutActionComp";
1718
import { migrateOldData } from "comps/generators/simpleGenerators";
1819

20+
// BoolControl without property view (internal state only)
21+
const CollapsedControl = withPropertyViewFn(BoolControl, () => null);
22+
1923
const childrenMap = {
2024
label: StringControl,
2125
hidden: BoolCodeControl,
2226
action: LayoutActionComp,
27+
collapsed: CollapsedControl, // tree editor collapsed state
2328
itemKey: valueComp<string>(""),
2429
icon: IconControl,
2530
};
2631

2732
type ChildrenType = ToInstanceType<typeof childrenMap> & {
2833
items: InstanceType<typeof LayoutMenuItemListComp>;
34+
collapsed: InstanceType<typeof CollapsedControl>;
2935
};
3036

3137
/**
@@ -73,6 +79,14 @@ export class LayoutMenuItemComp extends MultiBaseComp<ChildrenType> {
7379
getItemKey() {
7480
return this.children.itemKey.getView();
7581
}
82+
83+
getCollapsed(): boolean {
84+
return this.children.collapsed.getView();
85+
}
86+
87+
setCollapsed(collapsed: boolean) {
88+
this.children.collapsed.dispatchChangeValueAction(collapsed);
89+
}
7690
}
7791

7892
const LayoutMenuItemCompMigrate = migrateOldData(LayoutMenuItemComp, (oldData: any) => {

client/packages/lowcoder/src/comps/comps/navComp/components/MenuItemList.tsx

Lines changed: 12 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { SortableTree, TreeItems, TreeItemComponentProps, SimpleTreeItemWrapper
22
import LinkPlusButton from "components/LinkPlusButton";
33
import { BluePlusIcon, controlItem, ScrollBar } from "lowcoder-design";
44
import { trans } from "i18n";
5-
import React, { useMemo, useCallback, createContext, useContext, useState } from "react";
5+
import React, { useMemo, useCallback, createContext, useContext } from "react";
66
import styled from "styled-components";
77
import { NavCompType, NavListCompType, NavTreeItemData } from "./types";
88
import MenuItem from "./MenuItem";
@@ -105,68 +105,42 @@ interface IMenuItemListProps {
105105

106106
const menuItemLabel = trans("navigation.itemsDesc");
107107

108-
type TreeChangeReason = { type: string };
109-
110108
// Convert NavCompType[] to TreeItems format for dnd-kit-sortable-tree
111109
function convertToTreeItems(
112110
items: NavCompType[],
113-
basePath: number[] = [],
114-
collapsedIds: Set<string> = new Set()
111+
basePath: number[] = []
115112
): TreeItems<NavTreeItemData> {
116113
return items.map((item, index) => {
117114
const path = [...basePath, index];
118115
// Use stable itemKey if available, fallback to path-based ID for backwards compatibility
119116
const itemKey = item.getItemKey?.() || "";
120117
const id = itemKey || path.join("_");
121118
const subItems = item.getView().items || [];
119+
// Read collapsed state from the item itself
120+
const collapsed = item.getCollapsed?.() ?? false;
122121

123122
return {
124123
id,
125-
collapsed: collapsedIds.has(id),
124+
collapsed,
126125
comp: item,
127126
path: path,
128127
children: subItems.length > 0
129-
? convertToTreeItems(subItems, path, collapsedIds)
128+
? convertToTreeItems(subItems, path)
130129
: [],
131130
};
132131
});
133132
}
134133

135-
function extractCollapsedIds(treeItems: TreeItems<NavTreeItemData>): Set<string> {
136-
const ids = new Set<string>();
137-
const walk = (items: TreeItems<NavTreeItemData>) => {
138-
items.forEach((item) => {
139-
if (item.collapsed) {
140-
ids.add(String(item.id));
141-
}
142-
if (item.children?.length) {
143-
walk(item.children);
144-
}
145-
});
146-
};
147-
walk(treeItems);
148-
return ids;
149-
}
150-
151134
function MenuItemList(props: IMenuItemListProps) {
152135
const { items, onAddItem, onDeleteItem, onAddSubItem, onReorderItems } = props;
153136

154-
const [collapsedIds, setCollapsedIds] = useState<Set<string>>(() => new Set());
155-
156137
// Convert items to tree format
157-
const treeItems = useMemo(() => convertToTreeItems(items, [], collapsedIds), [items, collapsedIds]);
138+
const treeItems = useMemo(() => convertToTreeItems(items), [items]);
158139

159-
// Handle tree changes from drag and drop
140+
// Handle all tree changes (drag/drop, collapse/expand)
160141
const handleItemsChanged = useCallback(
161-
(newItems: TreeItems<NavTreeItemData>, reason: TreeChangeReason) => {
162-
// Persist collapsed/expanded state locally (SortableTree is controlled by `items` prop)
163-
setCollapsedIds(extractCollapsedIds(newItems));
164-
165-
// Only rewrite the underlying nav structure when the tree structure actually changed.
166-
// (If we rebuild on collapsed/expanded, it immediately resets the UI and looks like "toggle does nothing".)
167-
if (reason.type === "dropped" || reason.type === "removed") {
168-
onReorderItems(newItems);
169-
}
142+
(newItems: TreeItems<NavTreeItemData>) => {
143+
onReorderItems(newItems);
170144
},
171145
[onReorderItems]
172146
);
@@ -227,14 +201,14 @@ export function menuPropertyView(itemsComp: NavListCompType) {
227201
return getItemListByPath(path.slice(1), root.getView()[path[0]].children.items);
228202
};
229203

230-
// Convert flat tree structure back to nested comp structure
204+
// Convert tree structure back to nested comp structure
231205
const handleReorderItems = (newItems: TreeItems<NavTreeItemData>) => {
232-
// Build the new order from tree items
233206
const buildJsonFromTree = (treeItems: TreeItems<NavTreeItemData>): any[] => {
234207
return treeItems.map((item) => {
235208
const jsonValue = item.comp.toJsonValue() as Record<string, any>;
236209
return {
237210
...jsonValue,
211+
collapsed: item.collapsed ?? false, // sync collapsed from tree item
238212
items: item.children && item.children.length > 0
239213
? buildJsonFromTree(item.children)
240214
: [],
@@ -243,9 +217,6 @@ export function menuPropertyView(itemsComp: NavListCompType) {
243217
};
244218

245219
const newJson = buildJsonFromTree(newItems);
246-
247-
// Use setChildrensAction for atomic update instead of delete-all/add-all
248-
// This is more efficient and prevents UI glitches from multiple re-renders
249220
itemsComp.dispatch(itemsComp.setChildrensAction(newJson));
250221
};
251222

client/packages/lowcoder/src/comps/comps/navComp/navItemComp.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { BoolCodeControl, StringControl } from "comps/controls/codeControl";
2+
import { BoolControl } from "comps/controls/boolControl";
23
import { clickEvent, eventHandlerControl } from "comps/controls/eventHandlerControl";
3-
import { valueComp } from "comps/generators";
4+
import { valueComp, withPropertyViewFn } from "comps/generators";
45
import { list } from "comps/generators/list";
56
import { parseChildrenFromValueAndChildrenMap, ToViewReturn } from "comps/generators/multi";
67
import { migrateOldData, withDefault } from "comps/generators/simpleGenerators";
@@ -14,12 +15,16 @@ import { IconControl } from "comps/controls/iconControl";
1415

1516
const events = [clickEvent];
1617

18+
// BoolControl without property view (internal state only)
19+
const CollapsedControl = withPropertyViewFn(BoolControl, () => null);
20+
1721
const childrenMap = {
1822
label: StringControl,
1923
icon: IconControl,
2024
hidden: BoolCodeControl,
2125
disabled: BoolCodeControl,
2226
active: BoolCodeControl,
27+
collapsed: CollapsedControl, // tree editor collapsed state
2328
itemKey: valueComp<string>(""),
2429
onEvent: withDefault(eventHandlerControl(events), [
2530
{
@@ -38,6 +43,7 @@ type ChildrenType = {
3843
hidden: InstanceType<typeof BoolCodeControl>;
3944
disabled: InstanceType<typeof BoolCodeControl>;
4045
active: InstanceType<typeof BoolCodeControl>;
46+
collapsed: InstanceType<typeof CollapsedControl>;
4147
itemKey: InstanceType<ReturnType<typeof valueComp<string>>>;
4248
onEvent: InstanceType<ReturnType<typeof eventHandlerControl>>;
4349
items: InstanceType<ReturnType<typeof navListComp>>;
@@ -80,6 +86,14 @@ export class NavItemComp extends MultiBaseComp<ChildrenType> {
8086
return this.children.itemKey.getView();
8187
}
8288

89+
getCollapsed(): boolean {
90+
return this.children.collapsed.getView();
91+
}
92+
93+
setCollapsed(collapsed: boolean) {
94+
this.children.collapsed.dispatchChangeValueAction(collapsed);
95+
}
96+
8397
exposingNode(): RecordNode<NavItemExposing> {
8498
return fromRecord({
8599
label: this.children.label.exposingNode(),

0 commit comments

Comments
 (0)