Skip to content

Commit 1b07123

Browse files
committed
[Feat]: #1109 add menu tree + animation + fallback fixes
1 parent 63dc243 commit 1b07123

7 files changed

Lines changed: 71 additions & 123 deletions

File tree

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,12 @@ let NavTmpLayout = (function () {
204204
{
205205
label: trans("menuItem") + " 1",
206206
itemKey: genRandomKey(),
207+
items: [
208+
{
209+
label: trans("subMenuItem") + " 1",
210+
itemKey: genRandomKey(),
211+
},
212+
],
207213
},
208214
]),
209215
jsonItems: jsonControl(convertTreeData, jsonMenuItems),

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,12 @@ const MenuItem: React.FC<IMenuItemProps> = (props) => {
6666
visible={isConfigPopShow}
6767
setVisible={showConfigPop}
6868
>
69-
<MenuItemContent onClick={(e) => e.stopPropagation()}>
70-
{item.children.label.getView()}
69+
<MenuItemContent
70+
onClick={(e) => e.stopPropagation()}
71+
onMouseDown={(e) => e.stopPropagation()}
72+
onPointerDown={(e) => e.stopPropagation()}
73+
>
74+
{item.children.label.getView() || trans("untitled")}
7175
</MenuItemContent>
7276
</SimplePopover>
7377
<EditPopover

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

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ 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";
9-
const MAX_DEPTH = 3;
9+
const MAX_DEPTH = 10;
1010
const Wrapper = styled.div`
1111
.menu-title {
1212
display: flex;
@@ -56,9 +56,8 @@ const NavTreeItemComponent = React.forwardRef<
5656

5757
const handlers = useContext(MenuItemHandlersContext);
5858

59-
const hasChildren = item.children && item.children.length > 0;
6059
// allow adding sub-menu only if we are above the max depth (depth is 0-indexed)
61-
const canAddSubMenu = depth < MAX_DEPTH - 1;
60+
const canAddSubMenu = depth < MAX_DEPTH - 1 ;
6261

6362
const handleDelete = () => {
6463
handlers?.onDeleteItem(path);
@@ -78,13 +77,19 @@ const NavTreeItemComponent = React.forwardRef<
7877
item={item}
7978
depth={depth}
8079
collapsed={collapsed}
80+
81+
8182
>
82-
<TreeItemContent onClick={(e) => e.stopPropagation()}>
83+
<TreeItemContent
84+
onClick={(e) => e.stopPropagation()}
85+
onMouseDown={(e) => e.stopPropagation()}
86+
onPointerDown={(e) => e.stopPropagation()}
87+
>
8388
<MenuItem
8489
item={comp}
8590
onDelete={handleDelete}
8691
onAddSubMenu={handleAddSubMenu}
87-
showAddSubMenu={(!hasChildren || depth === 0) && canAddSubMenu}
92+
showAddSubMenu={canAddSubMenu}
8893
/>
8994
</TreeItemContent>
9095
</SimpleTreeItemWrapper>
@@ -99,7 +104,6 @@ interface IMenuItemListProps {
99104
onAddItem: (path: number[], value?: any) => number;
100105
onDeleteItem: (path: number[]) => void;
101106
onAddSubItem: (path: number[], value: any, unshift?: boolean) => number;
102-
onMoveItem: (path: number[], from: number, to: number) => void;
103107
onReorderItems: (newOrder: TreeItems<NavTreeItemData>) => void;
104108
}
105109

@@ -112,15 +116,12 @@ function convertToTreeItems(
112116
): TreeItems<NavTreeItemData> {
113117
return items.map((item, index) => {
114118
const path = [...basePath, index];
115-
// Use stable itemKey if available, fallback to path-based ID for backwards compatibility
116-
const itemKey = item.getItemKey?.() || "";
117-
const id = itemKey || path.join("_");
118119
const subItems = item.getView().items || [];
119120
// Read collapsed state from the item itself
120121
const collapsed = item.getCollapsed?.() ?? false;
121122

122123
return {
123-
id,
124+
id: path.join("_"),
124125
collapsed,
125126
comp: item,
126127
path: path,
@@ -170,6 +171,7 @@ function MenuItemList(props: IMenuItemListProps) {
170171
onItemsChanged={handleItemsChanged}
171172
TreeItemComponent={NavTreeItemComponent}
172173
indentationWidth={20}
174+
sortableProps={{ animateLayoutChanges: () => false }}
173175
/>
174176
</MenuItemHandlersContext.Provider>
175177
</ScrollBar>
@@ -237,9 +239,6 @@ export function menuPropertyView(itemsComp: NavListCompType) {
237239
item.addSubItem(value);
238240
return item.children.items.getView().length;
239241
}}
240-
onMoveItem={(path: number[], from: number, to: number) => {
241-
getItemListByPath(path).moveItem(from, to);
242-
}}
243242
onReorderItems={handleReorderItems}
244243
/>
245244
);

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

Lines changed: 36 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,6 @@ import {
2929
NavLayoutItemStyle,
3030
NavLayoutItemHoverStyle,
3131
NavLayoutItemActiveStyle,
32-
NavSubMenuItemStyle,
33-
NavSubMenuItemHoverStyle,
34-
NavSubMenuItemActiveStyle,
3532
} from "comps/controls/styleControlConstants";
3633
import { hiddenPropertyView, showDataLoadingIndicatorsPropertyView } from "comps/utils/propertyUtils";
3734
import { trans } from "i18n";
@@ -390,9 +387,7 @@ function renderAdvancedSection(children: any) {
390387
function renderStyleSections(
391388
children: any,
392389
styleSegment: MenuItemStyleOptionValue,
393-
setStyleSegment: (k: MenuItemStyleOptionValue) => void,
394-
subStyleSegment: MenuItemStyleOptionValue,
395-
setSubStyleSegment: (k: MenuItemStyleOptionValue) => void
390+
setStyleSegment: (k: MenuItemStyleOptionValue) => void
396391
) {
397392
const isHamburger = children.displayMode.getView() === 'hamburger';
398393
return (
@@ -415,19 +410,6 @@ function renderStyleSections(
415410
{styleSegment === "hover" && children.navItemHoverStyle.getPropertyView()}
416411
{styleSegment === "active" && children.navItemActiveStyle.getPropertyView()}
417412
</Section>
418-
<Section name={"Submenu Item Style"}>
419-
{controlItem({}, (
420-
<Segmented
421-
block
422-
options={menuItemStyleOptions as any}
423-
value={subStyleSegment}
424-
onChange={(k) => setSubStyleSegment(k as MenuItemStyleOptionValue)}
425-
/>
426-
))}
427-
{subStyleSegment === "normal" && children.subNavItemStyle.getPropertyView()}
428-
{subStyleSegment === "hover" && children.subNavItemHoverStyle.getPropertyView()}
429-
{subStyleSegment === "active" && children.subNavItemActiveStyle.getPropertyView()}
430-
</Section>
431413
{isHamburger && (
432414
<>
433415
<Section name={"Hamburger Button Style"}>
@@ -475,14 +457,30 @@ const childrenMap = {
475457
hamburgerButtonStyle: styleControl(HamburgerButtonStyle, 'hamburgerButtonStyle'),
476458
drawerContainerStyle: styleControl(DrawerContainerStyle, 'drawerContainerStyle'),
477459
animationStyle: styleControl(AnimationStyle, 'animationStyle'),
478-
subNavItemStyle: styleControl(NavSubMenuItemStyle, 'subNavItemStyle'),
479-
subNavItemHoverStyle: styleControl(NavSubMenuItemHoverStyle, 'subNavItemHoverStyle'),
480-
subNavItemActiveStyle: styleControl(NavSubMenuItemActiveStyle, 'subNavItemActiveStyle'),
481460
items: withDefault(migrateOldData(createNavItemsControl(), fixOldItemsData), {
482461
optionType: "manual",
483462
manual: [
484463
{
485464
label: trans("menuItem") + " 1",
465+
items: [
466+
{
467+
label: trans("subMenuItem") + " 1",
468+
items: [
469+
{
470+
label: trans("subMenuItem") + " 1-1",
471+
},
472+
{
473+
label: trans("subMenuItem") + " 1-2",
474+
},
475+
],
476+
},
477+
{
478+
label: trans("subMenuItem") + " 2",
479+
},
480+
{
481+
label: trans("subMenuItem") + " 3",
482+
},
483+
],
486484
},
487485
],
488486
}),
@@ -505,7 +503,7 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => {
505503
return null;
506504
}
507505

508-
const label = view?.label;
506+
const label = view?.label || trans("untitled");
509507
const icon = hasIcon(view?.icon) ? view.icon : undefined;
510508
const active = !!view?.active;
511509
const onEvent = view?.onEvent;
@@ -524,7 +522,7 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => {
524522
const children = buildSubMenuItems(subItem.getView()?.items, key);
525523
return {
526524
key,
527-
label: subItem.children.label.getView(),
525+
label: subItem.children.label.getView() || trans("untitled"),
528526
icon: subIcon,
529527
disabled: !!subItem.children.disabled.getView(),
530528
...(children.length > 0 ? { children } : {}),
@@ -566,7 +564,7 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => {
566564
);
567565
if (subMenuItems.length > 0) {
568566
const subMenu = (
569-
<ScrollBar style={{ height: "200px" }}>
567+
<ScrollBar style={{ height: "200px", minWidth: "200px" }}>
570568
<StyledMenu
571569
onClick={(e) => {
572570
if (disabled) return;
@@ -588,22 +586,22 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => {
588586
...item,
589587
icon: item.icon || undefined,
590588
}))}
591-
$color={(props.subNavItemStyle && props.subNavItemStyle.text) || props.style.text}
592-
$hoverColor={(props.subNavItemHoverStyle && props.subNavItemHoverStyle.text) || props.style.accent}
593-
$activeColor={(props.subNavItemActiveStyle && props.subNavItemActiveStyle.text) || props.style.accent}
594-
$bg={(props.subNavItemStyle && props.subNavItemStyle.background) || undefined}
595-
$hoverBg={(props.subNavItemHoverStyle && props.subNavItemHoverStyle.background) || undefined}
596-
$activeBg={(props.subNavItemActiveStyle && props.subNavItemActiveStyle.background) || undefined}
597-
$border={(props.subNavItemStyle && props.subNavItemStyle.border) || undefined}
598-
$hoverBorder={(props.subNavItemHoverStyle && props.subNavItemHoverStyle.border) || undefined}
599-
$activeBorder={(props.subNavItemActiveStyle && props.subNavItemActiveStyle.border) || undefined}
600-
$radius={(props.subNavItemStyle && props.subNavItemStyle.radius) || undefined}
589+
$color={props.style.text}
590+
$hoverColor={props.style.accent}
591+
$activeColor={props.style.accent}
592+
$bg={undefined}
593+
$hoverBg={undefined}
594+
$activeBg={undefined}
595+
$border={undefined}
596+
$hoverBorder={undefined}
597+
$activeBorder={undefined}
598+
$radius={undefined}
601599
$fontFamily={props.style.fontFamily}
602600
$fontStyle={props.style.fontStyle}
603601
$textWeight={props.style.textWeight}
604602
$textSize={props.style.textSize}
605-
$padding={(props.subNavItemStyle && props.subNavItemStyle.padding) || props.style.padding}
606-
$margin={(props.subNavItemStyle && props.subNavItemStyle.margin) || props.style.margin}
603+
$padding={props.style.padding}
604+
$margin={props.style.margin}
607605
$textTransform={props.style.textTransform}
608606
$textDecoration={props.style.textDecoration}
609607
/>
@@ -708,15 +706,14 @@ const NavCompBase = new UICompBuilder(childrenMap, (props) => {
708706
const showLogic = mode === "logic" || mode === "both";
709707
const showLayout = mode === "layout" || mode === "both";
710708
const [styleSegment, setStyleSegment] = useState<MenuItemStyleOptionValue>("normal");
711-
const [subStyleSegment, setSubStyleSegment] = useState<MenuItemStyleOptionValue>("normal");
712709

713710
return (
714711
<>
715712
{renderBasicSection(children)}
716713
{showLogic && renderInteractionSection(children)}
717714
{showLayout && renderLayoutSection(children)}
718715
{showLogic && renderAdvancedSection(children)}
719-
{showLayout && renderStyleSections(children, styleSegment, setStyleSegment, subStyleSegment, setSubStyleSegment)}
716+
{showLayout && renderStyleSections(children, styleSegment, setStyleSegment)}
720717
</>
721718
);
722719
})

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

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { BoolCodeControl, StringControl } from "comps/controls/codeControl";
22
import { BoolControl } from "comps/controls/boolControl";
33
import { clickEvent, eventHandlerControl } from "comps/controls/eventHandlerControl";
4-
import { valueComp, withPropertyViewFn } from "comps/generators";
4+
import { withPropertyViewFn } from "comps/generators";
55
import { list } from "comps/generators/list";
66
import { parseChildrenFromValueAndChildrenMap, ToViewReturn } from "comps/generators/multi";
77
import { migrateOldData, withDefault } from "comps/generators/simpleGenerators";
88
import { disabledPropertyView, hiddenPropertyView } from "comps/utils/propertyUtils";
9-
import { genRandomKey } from "comps/utils/idGenerator";
109
import { trans } from "i18n";
1110
import _ from "lodash";
1211
import { fromRecord, MultiBaseComp, Node, RecordNode, RecordNodeToValue } from "lowcoder-core";
@@ -25,7 +24,6 @@ const childrenMap = {
2524
disabled: BoolCodeControl,
2625
active: BoolCodeControl,
2726
collapsed: CollapsedControl, // tree editor collapsed state
28-
itemKey: valueComp<string>(""),
2927
onEvent: withDefault(eventHandlerControl(events), [
3028
{
3129
// name: "click",
@@ -44,7 +42,6 @@ type ChildrenType = {
4442
disabled: InstanceType<typeof BoolCodeControl>;
4543
active: InstanceType<typeof BoolCodeControl>;
4644
collapsed: InstanceType<typeof CollapsedControl>;
47-
itemKey: InstanceType<ReturnType<typeof valueComp<string>>>;
4845
onEvent: InstanceType<ReturnType<typeof eventHandlerControl>>;
4946
items: InstanceType<ReturnType<typeof navListComp>>;
5047
};
@@ -82,10 +79,6 @@ export class NavItemComp extends MultiBaseComp<ChildrenType> {
8279
this.children.items.addItem(value);
8380
}
8481

85-
getItemKey(): string {
86-
return this.children.itemKey.getView();
87-
}
88-
8982
getCollapsed(): boolean {
9083
return this.children.collapsed.getView();
9184
}
@@ -115,42 +108,31 @@ type NavItemExposing = {
115108
items: Node<RecordNodeToValue<NavItemExposing>[]>;
116109
};
117110

118-
// Migrate old nav items to ensure they have a stable itemKey
111+
// Migrate old nav items to strip out deprecated itemKey field
119112
function migrateNavItemData(oldData: any): any {
120113
if (!oldData) return oldData;
121114

122-
const migrated = {
123-
...oldData,
124-
itemKey: oldData.itemKey || genRandomKey(),
125-
};
115+
const { itemKey, ...rest } = oldData;
126116

127117
// Also migrate nested items recursively
128-
if (Array.isArray(oldData.items)) {
129-
migrated.items = oldData.items.map((item: any) => migrateNavItemData(item));
118+
if (Array.isArray(rest.items)) {
119+
rest.items = rest.items.map((item: any) => migrateNavItemData(item));
130120
}
131121

132-
return migrated;
122+
return rest;
133123
}
134124

135-
const NavItemCompMigrate = migrateOldData(NavItemComp, migrateNavItemData);
125+
const NavItemCompMigrated = migrateOldData(NavItemComp, migrateNavItemData);
136126

137127
export function navListComp() {
138-
const NavItemListCompBase = list(NavItemCompMigrate);
128+
const NavItemListCompBase = list(NavItemCompMigrated);
139129

140130
return class NavItemListComp extends NavItemListCompBase {
141131
addItem(value?: any) {
142132
const data = this.getView();
143133
this.dispatch(
144134
this.pushAction(
145-
value
146-
? {
147-
...value,
148-
itemKey: value.itemKey || genRandomKey(),
149-
}
150-
: {
151-
label: trans("menuItem") + " " + (data.length + 1),
152-
itemKey: genRandomKey(),
153-
}
135+
value || { label: trans("menuItem") + " " + (data.length + 1) }
154136
)
155137
);
156138
}

0 commit comments

Comments
 (0)