@@ -12,9 +12,9 @@ import { Section, controlItem, sectionNames } from "lowcoder-design";
1212import { trans } from "i18n" ;
1313import { EditorContainer , EmptyContent } from "pages/common/styledComponent" ;
1414import { useCallback , useEffect , useMemo , useState } from "react" ;
15- import styled , { css } from "styled-components" ;
15+ import styled from "styled-components" ;
1616import { isUserViewMode , useAppPathParam } from "util/hooks" ;
17- import { StringControl } from "comps/controls/codeControl" ;
17+ import { StringControl , jsonControl } from "comps/controls/codeControl" ;
1818import { styleControl } from "comps/controls/styleControl" ;
1919import {
2020 NavLayoutStyle ,
@@ -27,30 +27,20 @@ import {
2727} from "comps/controls/styleControlConstants" ;
2828import { dropdownControl } from "comps/controls/dropdownControl" ;
2929import _ 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
3141const DEFAULT_WIDTH = 240 ;
32- const ModeOptions = [
33- { label : trans ( "navLayout.modeInline" ) , value : "inline" } ,
34- { label : trans ( "navLayout.modeVertical" ) , value : "vertical" } ,
35- ] as const ;
36-
3742type 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-
5444const 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+
146141const 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+
152177let 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