Skip to content

Commit b06fa7e

Browse files
feat: allow json data for nav layout
1 parent 4be209b commit b06fa7e

File tree

5 files changed

+299
-55
lines changed

5 files changed

+299
-55
lines changed

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

+1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ const LayoutMenuItemCompMigrate = migrateOldData(LayoutMenuItemComp, (oldData: a
9696
export class LayoutMenuItemListComp extends list(LayoutMenuItemCompMigrate) {
9797
addItem(value?: any) {
9898
const data = this.getView();
99+
99100
this.dispatch(
100101
this.pushAction(
101102
value

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

+219-55
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import { Section, controlItem, sectionNames } from "lowcoder-design";
1212
import { trans } from "i18n";
1313
import { EditorContainer, EmptyContent } from "pages/common/styledComponent";
1414
import { useCallback, useEffect, useMemo, useState } from "react";
15-
import styled, { css } from "styled-components";
15+
import styled from "styled-components";
1616
import { isUserViewMode, useAppPathParam } from "util/hooks";
17-
import { StringControl } from "comps/controls/codeControl";
17+
import { StringControl, jsonControl } from "comps/controls/codeControl";
1818
import { styleControl } from "comps/controls/styleControl";
1919
import {
2020
NavLayoutStyle,
@@ -27,30 +27,20 @@ import {
2727
} from "comps/controls/styleControlConstants";
2828
import { dropdownControl } from "comps/controls/dropdownControl";
2929
import _ from "lodash";
30+
import { check } from "util/convertUtils";
31+
import { genRandomKey } from "comps/utils/idGenerator";
32+
import history from "util/history";
33+
import {
34+
DataOption,
35+
DataOptionType,
36+
ModeOptions,
37+
jsonMenuItems,
38+
menuItemStyleOptions
39+
} from "./navLayoutConstants";
3040

3141
const DEFAULT_WIDTH = 240;
32-
const ModeOptions = [
33-
{ label: trans("navLayout.modeInline"), value: "inline" },
34-
{ label: trans("navLayout.modeVertical"), value: "vertical" },
35-
] as const;
36-
3742
type MenuItemStyleOptionValue = "normal" | "hover" | "active";
3843

39-
const menuItemStyleOptions = [
40-
{
41-
value: "normal",
42-
label: "Normal",
43-
},
44-
{
45-
value: "hover",
46-
label: "Hover",
47-
},
48-
{
49-
value: "active",
50-
label: "Active",
51-
}
52-
]
53-
5444
const StyledSide = styled(Layout.Sider)`
5545
max-height: calc(100vh - ${TopHeaderHeight});
5646
overflow: auto;
@@ -143,19 +133,57 @@ const StyledMenu = styled(AntdMenu)<{
143133
144134
`;
145135

136+
const StyledImage = styled.img`
137+
height: 1em;
138+
color: currentColor;
139+
`;
140+
146141
const defaultStyle = {
147142
radius: '0px',
148143
margin: '0px',
149144
padding: '0px',
150145
}
151146

147+
type UrlActionType = {
148+
url?: string;
149+
newTab?: boolean;
150+
}
151+
152+
export type MenuItemNode = {
153+
label: string;
154+
key: string;
155+
hidden?: boolean;
156+
icon?: any;
157+
action?: UrlActionType,
158+
children?: MenuItemNode[];
159+
}
160+
161+
function checkDataNodes(value: any, key?: string): MenuItemNode[] | undefined {
162+
return check(value, ["array", "undefined"], key, (node, k) => {
163+
check(node, ["object"], k);
164+
check(node["label"], ["string"], "label");
165+
check(node["hidden"], ["boolean", "undefined"], "hidden");
166+
check(node["icon"], ["string", "undefined"], "icon");
167+
check(node["action"], ["object", "undefined"], "action");
168+
checkDataNodes(node["children"], "children");
169+
return node;
170+
});
171+
}
172+
173+
function convertTreeData(data: any) {
174+
return data === "" ? [] : checkDataNodes(data) ?? [];
175+
}
176+
152177
let NavTmpLayout = (function () {
153178
const childrenMap = {
179+
dataOptionType: dropdownControl(DataOptionType, DataOption.Manual),
154180
items: withDefault(LayoutMenuItemListComp, [
155181
{
156182
label: trans("menuItem") + " 1",
183+
itemKey: genRandomKey(),
157184
},
158185
]),
186+
jsonItems: jsonControl(convertTreeData, jsonMenuItems),
159187
width: withDefault(StringControl, DEFAULT_WIDTH),
160188
backgroundImage: withDefault(StringControl, ""),
161189
mode: dropdownControl(ModeOptions, "inline"),
@@ -173,7 +201,17 @@ let NavTmpLayout = (function () {
173201
return (
174202
<div style={{overflowY: 'auto'}}>
175203
<Section name={trans("menu")}>
176-
{menuPropertyView(children.items)}
204+
{children.dataOptionType.propertyView({
205+
radioButton: true,
206+
type: "oneline",
207+
})}
208+
{
209+
children.dataOptionType.getView() === DataOption.Manual
210+
? menuPropertyView(children.items)
211+
: children.jsonItems.propertyView({
212+
label: "Json Data",
213+
})
214+
}
177215
</Section>
178216
<Section name={sectionNames.layout}>
179217
{ children.width.propertyView({
@@ -199,7 +237,6 @@ let NavTmpLayout = (function () {
199237
block
200238
options={menuItemStyleOptions}
201239
value={styleSegment}
202-
// className="comp-panel-tab"
203240
onChange={(k) => setStyleSegment(k as MenuItemStyleOptionValue)}
204241
/>
205242
))}
@@ -223,46 +260,97 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => {
223260
const pathParam = useAppPathParam();
224261
const isViewMode = isUserViewMode(pathParam);
225262
const [selectedKey, setSelectedKey] = useState("");
226-
const items = useMemo(() => comp.children.items.getView(), [comp.children.items]);
227-
const navWidth = useMemo(() => comp.children.width.getView(), [comp.children.width]);
228-
const navMode = useMemo(() => comp.children.mode.getView(), [comp.children.mode]);
229-
const navStyle = useMemo(() => comp.children.navStyle.getView(), [comp.children.navStyle]);
230-
const navItemStyle = useMemo(() => comp.children.navItemStyle.getView(), [comp.children.navItemStyle]);
231-
const navItemHoverStyle = useMemo(() => comp.children.navItemHoverStyle.getView(), [comp.children.navItemHoverStyle]);
232-
const navItemActiveStyle = useMemo(() => comp.children.navItemActiveStyle.getView(), [comp.children.navItemActiveStyle]);
263+
const items = comp.children.items.getView();
264+
const navWidth = comp.children.width.getView();
265+
const navMode = comp.children.mode.getView();
266+
const navStyle = comp.children.navStyle.getView();
267+
const navItemStyle = comp.children.navItemStyle.getView();
268+
const navItemHoverStyle = comp.children.navItemHoverStyle.getView();
269+
const navItemActiveStyle = comp.children.navItemActiveStyle.getView();
233270
const backgroundImage = comp.children.backgroundImage.getView();
234-
271+
const jsonItems = comp.children.jsonItems.getView();
272+
const dataOptionType = comp.children.dataOptionType.getView();
273+
235274
// filter out hidden. unauthorised items filtered by server
236275
const filterItem = useCallback((item: LayoutMenuItemComp): boolean => {
237276
return !item.children.hidden.getView();
238277
}, []);
239278

240-
const generateItemKeyRecord = (items: LayoutMenuItemComp[]) => {
241-
const result: Record<string, LayoutMenuItemComp> = {};
242-
items.forEach((item) => {
243-
const subItems = item.children.items.getView();
244-
if (subItems.length > 0) {
245-
Object.assign(result, generateItemKeyRecord(subItems))
279+
const generateItemKeyRecord = useCallback(
280+
(items: LayoutMenuItemComp[] | MenuItemNode[]) => {
281+
const result: Record<string, LayoutMenuItemComp | MenuItemNode> = {};
282+
if(dataOptionType === DataOption.Manual) {
283+
(items as LayoutMenuItemComp[])?.forEach((item) => {
284+
const subItems = item.children.items.getView();
285+
if (subItems.length > 0) {
286+
Object.assign(result, generateItemKeyRecord(subItems))
287+
}
288+
result[item.getItemKey()] = item;
289+
});
246290
}
247-
result[item.getItemKey()] = item;
248-
});
249-
return result;
250-
}
291+
if(dataOptionType === DataOption.Json) {
292+
(items as MenuItemNode[])?.forEach((item) => {
293+
if (item.children?.length) {
294+
Object.assign(result, generateItemKeyRecord(item.children))
295+
}
296+
result[item.key] = item;
297+
})
298+
}
299+
return result;
300+
}, [dataOptionType]
301+
)
251302

252303
const itemKeyRecord = useMemo(() => {
304+
if(dataOptionType === DataOption.Json) {
305+
return generateItemKeyRecord(jsonItems)
306+
}
253307
return generateItemKeyRecord(items)
254-
}, [items]);
308+
}, [dataOptionType, jsonItems, items, generateItemKeyRecord]);
255309

256310
const onMenuItemClick = useCallback(({key}: {key: string}) => {
257-
const itemComp = itemKeyRecord[key];
311+
const itemComp = itemKeyRecord[key]
312+
258313
const url = [
259314
ALL_APPLICATIONS_URL,
260315
pathParam.applicationId,
261316
pathParam.viewMode,
262-
itemComp.getItemKey(),
317+
key,
263318
].join("/");
264-
itemComp.children.action.act(url);
265-
}, [pathParam.applicationId, pathParam.viewMode, itemKeyRecord])
319+
320+
// handle manual menu item action
321+
if(dataOptionType === DataOption.Manual) {
322+
(itemComp as LayoutMenuItemComp).children.action.act(url);
323+
return;
324+
}
325+
// handle json menu item action
326+
if((itemComp as MenuItemNode).action?.newTab) {
327+
return window.open((itemComp as MenuItemNode).action?.url, '_blank')
328+
}
329+
history.push(url);
330+
}, [pathParam.applicationId, pathParam.viewMode, dataOptionType, itemKeyRecord])
331+
332+
const getJsonMenuItem = useCallback(
333+
(items: MenuItemNode[]): MenuProps["items"] => {
334+
return items?.map((item: MenuItemNode) => {
335+
const {
336+
label,
337+
key,
338+
hidden,
339+
icon,
340+
children,
341+
} = item;
342+
return {
343+
label,
344+
key,
345+
hidden,
346+
icon: <StyledImage src={icon} />,
347+
onTitleClick: onMenuItemClick,
348+
onClick: onMenuItemClick,
349+
...(children?.length && { children: getJsonMenuItem(children) }),
350+
}
351+
})
352+
}, [onMenuItemClick]
353+
)
266354

267355
const getMenuItem = useCallback(
268356
(itemComps: LayoutMenuItemComp[]): MenuProps["items"] => {
@@ -283,7 +371,11 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => {
283371
[onMenuItemClick, filterItem]
284372
);
285373

286-
const menuItems = useMemo(() => getMenuItem(items), [items, getMenuItem]);
374+
const menuItems = useMemo(() => {
375+
if(dataOptionType === DataOption.Json) return getJsonMenuItem(jsonItems)
376+
377+
return getMenuItem(items)
378+
}, [dataOptionType, jsonItems, getJsonMenuItem, items, getMenuItem]);
287379

288380
// Find by path itemKey
289381
const findItemPathByKey = useCallback(
@@ -329,7 +421,60 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => {
329421
[filterItem]
330422
);
331423

424+
// Find by path itemKey
425+
const findItemPathByKeyJson = useCallback(
426+
(itemComps: MenuItemNode[], itemKey: string): string[] => {
427+
for (let item of itemComps) {
428+
const subItems = item.children;
429+
if (subItems?.length) {
430+
// have subMenus
431+
const childPath = findItemPathByKeyJson(subItems, itemKey);
432+
if (childPath.length > 0) {
433+
return [item.key, ...childPath];
434+
}
435+
} else {
436+
if (item.key === itemKey) {
437+
return [item.key];
438+
}
439+
}
440+
}
441+
return [];
442+
},
443+
[]
444+
);
445+
446+
// Get the first visible menu
447+
const findFirstItemPathJson = useCallback(
448+
(itemComps: MenuItemNode[]): string[] => {
449+
for (let item of itemComps) {
450+
if (!item.hidden) {
451+
const subItems = item.children;
452+
if (subItems?.length) {
453+
// have subMenus
454+
const childPath = findFirstItemPathJson(subItems);
455+
if (childPath.length > 0) {
456+
return [item.key, ...childPath];
457+
}
458+
} else {
459+
return [item.key];
460+
}
461+
}
462+
}
463+
return [];
464+
}, []
465+
);
466+
332467
const defaultOpenKeys = useMemo(() => {
468+
if(dataOptionType === DataOption.Json) {
469+
let itemPath: string[];
470+
if (pathParam.appPageId) {
471+
itemPath = findItemPathByKeyJson(jsonItems, pathParam.appPageId);
472+
} else {
473+
itemPath = findFirstItemPathJson(jsonItems);
474+
}
475+
return itemPath.slice(0, itemPath.length - 1);
476+
}
477+
333478
let itemPath: string[];
334479
if (pathParam.appPageId) {
335480
itemPath = findItemPathByKey(items, pathParam.appPageId);
@@ -350,14 +495,32 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => {
350495
setSelectedKey(selectedKey);
351496
}, [pathParam.appPageId]);
352497

353-
let pageView = <EmptyContent text="" style={{ height: "100%" }} />;
354-
const selectedItem = itemKeyRecord[selectedKey];
355-
if (selectedItem && !selectedItem.children.hidden.getView()) {
356-
const compView = selectedItem.children.action.getView();
357-
if (compView) {
358-
pageView = compView;
498+
const pageView = useMemo(() => {
499+
let pageView = <EmptyContent text="" style={{ height: "100%" }} />;
500+
501+
if(dataOptionType === DataOption.Manual) {
502+
const selectedItem = (itemKeyRecord[selectedKey] as LayoutMenuItemComp);
503+
if (selectedItem && !selectedItem.children.hidden.getView()) {
504+
const compView = selectedItem.children.action.getView();
505+
if (compView) {
506+
pageView = compView;
507+
}
508+
}
359509
}
360-
}
510+
if(dataOptionType === DataOption.Json) {
511+
const item = (itemKeyRecord[selectedKey] as MenuItemNode)
512+
if(item?.action?.url) {
513+
pageView = <iframe
514+
title={item?.action?.url}
515+
src={item?.action?.url}
516+
width="100%"
517+
height="100%"
518+
style={{ border: "none", marginBottom: "-6px" }}
519+
/>
520+
}
521+
}
522+
return pageView;
523+
}, [dataOptionType, itemKeyRecord, selectedKey])
361524

362525
const getVerticalMargin = (margin: string[]) => {
363526
if(margin.length === 1) return `${margin[0]}`;
@@ -380,6 +543,7 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => {
380543
if(!_.isEmpty(backgroundImage)) {
381544
backgroundStyle = `center / cover url('${backgroundImage}') no-repeat, ${backgroundStyle}`;
382545
}
546+
383547
let content = (
384548
<Layout>
385549
<StyledSide theme="light" width={navWidth}>

0 commit comments

Comments
 (0)