@@ -12,9 +12,9 @@ import { Section, controlItem, sectionNames } from "lowcoder-design";
12
12
import { trans } from "i18n" ;
13
13
import { EditorContainer , EmptyContent } from "pages/common/styledComponent" ;
14
14
import { useCallback , useEffect , useMemo , useState } from "react" ;
15
- import styled , { css } from "styled-components" ;
15
+ import styled from "styled-components" ;
16
16
import { isUserViewMode , useAppPathParam } from "util/hooks" ;
17
- import { StringControl } from "comps/controls/codeControl" ;
17
+ import { StringControl , jsonControl } from "comps/controls/codeControl" ;
18
18
import { styleControl } from "comps/controls/styleControl" ;
19
19
import {
20
20
NavLayoutStyle ,
@@ -27,30 +27,20 @@ import {
27
27
} from "comps/controls/styleControlConstants" ;
28
28
import { dropdownControl } from "comps/controls/dropdownControl" ;
29
29
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" ;
30
40
31
41
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
-
37
42
type MenuItemStyleOptionValue = "normal" | "hover" | "active" ;
38
43
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
-
54
44
const StyledSide = styled ( Layout . Sider ) `
55
45
max-height: calc(100vh - ${ TopHeaderHeight } );
56
46
overflow: auto;
@@ -143,19 +133,57 @@ const StyledMenu = styled(AntdMenu)<{
143
133
144
134
` ;
145
135
136
+ const StyledImage = styled . img `
137
+ height: 1em;
138
+ color: currentColor;
139
+ ` ;
140
+
146
141
const defaultStyle = {
147
142
radius : '0px' ,
148
143
margin : '0px' ,
149
144
padding : '0px' ,
150
145
}
151
146
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
+
152
177
let NavTmpLayout = ( function ( ) {
153
178
const childrenMap = {
179
+ dataOptionType : dropdownControl ( DataOptionType , DataOption . Manual ) ,
154
180
items : withDefault ( LayoutMenuItemListComp , [
155
181
{
156
182
label : trans ( "menuItem" ) + " 1" ,
183
+ itemKey : genRandomKey ( ) ,
157
184
} ,
158
185
] ) ,
186
+ jsonItems : jsonControl ( convertTreeData , jsonMenuItems ) ,
159
187
width : withDefault ( StringControl , DEFAULT_WIDTH ) ,
160
188
backgroundImage : withDefault ( StringControl , "" ) ,
161
189
mode : dropdownControl ( ModeOptions , "inline" ) ,
@@ -173,7 +201,17 @@ let NavTmpLayout = (function () {
173
201
return (
174
202
< div style = { { overflowY : 'auto' } } >
175
203
< 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
+ }
177
215
</ Section >
178
216
< Section name = { sectionNames . layout } >
179
217
{ children . width . propertyView ( {
@@ -199,7 +237,6 @@ let NavTmpLayout = (function () {
199
237
block
200
238
options = { menuItemStyleOptions }
201
239
value = { styleSegment }
202
- // className="comp-panel-tab"
203
240
onChange = { ( k ) => setStyleSegment ( k as MenuItemStyleOptionValue ) }
204
241
/>
205
242
) ) }
@@ -223,46 +260,97 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => {
223
260
const pathParam = useAppPathParam ( ) ;
224
261
const isViewMode = isUserViewMode ( pathParam ) ;
225
262
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 ( ) ;
233
270
const backgroundImage = comp . children . backgroundImage . getView ( ) ;
234
-
271
+ const jsonItems = comp . children . jsonItems . getView ( ) ;
272
+ const dataOptionType = comp . children . dataOptionType . getView ( ) ;
273
+
235
274
// filter out hidden. unauthorised items filtered by server
236
275
const filterItem = useCallback ( ( item : LayoutMenuItemComp ) : boolean => {
237
276
return ! item . children . hidden . getView ( ) ;
238
277
} , [ ] ) ;
239
278
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
+ } ) ;
246
290
}
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
+ )
251
302
252
303
const itemKeyRecord = useMemo ( ( ) => {
304
+ if ( dataOptionType === DataOption . Json ) {
305
+ return generateItemKeyRecord ( jsonItems )
306
+ }
253
307
return generateItemKeyRecord ( items )
254
- } , [ items ] ) ;
308
+ } , [ dataOptionType , jsonItems , items , generateItemKeyRecord ] ) ;
255
309
256
310
const onMenuItemClick = useCallback ( ( { key} : { key : string } ) => {
257
- const itemComp = itemKeyRecord [ key ] ;
311
+ const itemComp = itemKeyRecord [ key ]
312
+
258
313
const url = [
259
314
ALL_APPLICATIONS_URL ,
260
315
pathParam . applicationId ,
261
316
pathParam . viewMode ,
262
- itemComp . getItemKey ( ) ,
317
+ key ,
263
318
] . 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
+ )
266
354
267
355
const getMenuItem = useCallback (
268
356
( itemComps : LayoutMenuItemComp [ ] ) : MenuProps [ "items" ] => {
@@ -283,7 +371,11 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => {
283
371
[ onMenuItemClick , filterItem ]
284
372
) ;
285
373
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 ] ) ;
287
379
288
380
// Find by path itemKey
289
381
const findItemPathByKey = useCallback (
@@ -329,7 +421,60 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => {
329
421
[ filterItem ]
330
422
) ;
331
423
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
+
332
467
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
+
333
478
let itemPath : string [ ] ;
334
479
if ( pathParam . appPageId ) {
335
480
itemPath = findItemPathByKey ( items , pathParam . appPageId ) ;
@@ -350,14 +495,32 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => {
350
495
setSelectedKey ( selectedKey ) ;
351
496
} , [ pathParam . appPageId ] ) ;
352
497
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
+ }
359
509
}
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 ] )
361
524
362
525
const getVerticalMargin = ( margin : string [ ] ) => {
363
526
if ( margin . length === 1 ) return `${ margin [ 0 ] } ` ;
@@ -380,6 +543,7 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => {
380
543
if ( ! _ . isEmpty ( backgroundImage ) ) {
381
544
backgroundStyle = `center / cover url('${ backgroundImage } ') no-repeat, ${ backgroundStyle } ` ;
382
545
}
546
+
383
547
let content = (
384
548
< Layout >
385
549
< StyledSide theme = "light" width = { navWidth } >
0 commit comments