ui-soxo-bootstrap-core 2.6.40-dev.12 → 2.6.40-dev.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/lib/components/sidemenu/sidemenu.js +66 -147
- package/core/models/menus/components/menu-add/menu-add.js +31 -4
- package/core/models/menus/components/menu-lists/menu-lists.js +35 -19
- package/core/models/roles/components/role-add/role-add.js +10 -2
- package/core/models/roles/roles.js +3 -0
- package/package.json +1 -1
|
@@ -385,6 +385,71 @@ export default function SideMenu({ loading, modules = [], callback, appSettings,
|
|
|
385
385
|
|
|
386
386
|
const rootSubmenuKeys = Menus.screenMenus(modules, 'order').map((m) => m.id || m.path || m.caption);
|
|
387
387
|
|
|
388
|
+
/**
|
|
389
|
+
* Recursively renders the menu tree to any depth. Any menu that has visible
|
|
390
|
+
* children is rendered as a <SubMenu>; everything else becomes a clickable
|
|
391
|
+
* leaf <Menu.Item>. This replaces the old hard-coded 3-level rendering so
|
|
392
|
+
* menus nested 4, 5, 6+ levels deep all show up.
|
|
393
|
+
*
|
|
394
|
+
* Keys are kept identical to the previous implementation
|
|
395
|
+
* (SubMenu => id || path || caption, leaf => path || caption) so the
|
|
396
|
+
* openKeys / selectedKeys / onOpenChange logic continues to work.
|
|
397
|
+
*/
|
|
398
|
+
const renderMenuTree = (items, { isRoot = false, parentKey } = {}) => {
|
|
399
|
+
const visibleItems = Menus.screenMenus(items, 'order').filter((record) => {
|
|
400
|
+
// Drop entries without a caption — they have no label to render.
|
|
401
|
+
const hasCaption = typeof record.caption === 'string' && record.caption.trim().length > 0;
|
|
402
|
+
if (!hasCaption) return false;
|
|
403
|
+
|
|
404
|
+
// The visibility / icon rule only applies to top-level menus, matching the
|
|
405
|
+
// previous behaviour; nested items just need a caption.
|
|
406
|
+
if (!isRoot) return true;
|
|
407
|
+
|
|
408
|
+
if (record.id) {
|
|
409
|
+
if (record.is_visible) return true;
|
|
410
|
+
return !!record.icon_name;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return true;
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
return visibleItems.map((menu, index) => {
|
|
417
|
+
const icon = menu.icon_name || 'fa-solid fas fa-user';
|
|
418
|
+
|
|
419
|
+
// Children that actually have something to show (a caption).
|
|
420
|
+
const children =
|
|
421
|
+
menu && menu.sub_menus
|
|
422
|
+
? Menus.screenMenus(menu.sub_menus, 'order').filter((s) => typeof s.caption === 'string' && s.caption.trim().length > 0)
|
|
423
|
+
: [];
|
|
424
|
+
|
|
425
|
+
if (children.length) {
|
|
426
|
+
return (
|
|
427
|
+
<SubMenu
|
|
428
|
+
className="popup"
|
|
429
|
+
style={{ color: state.theme.colors.leftSectionColor }}
|
|
430
|
+
key={menu.id || menu.path || menu.caption}
|
|
431
|
+
title={
|
|
432
|
+
<span>
|
|
433
|
+
<CollapsedIconMenu menu={menu} caption={menu.caption} icon={icon} collapsed={collapsed} />
|
|
434
|
+
</span>
|
|
435
|
+
}
|
|
436
|
+
>
|
|
437
|
+
{renderMenuTree(menu.sub_menus, { isRoot: false, parentKey: menu.path || menu.caption })}
|
|
438
|
+
</SubMenu>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return (
|
|
443
|
+
<Menu.Item
|
|
444
|
+
key={menu.path || menu.caption}
|
|
445
|
+
onClick={() => onMenuClick({ ...menu, parentKey: parentKey || menu.path || menu.caption, isRoot }, index)}
|
|
446
|
+
>
|
|
447
|
+
<CollapsedIconMenu menu={menu} caption={menu.caption} icon={icon} collapsed={collapsed} />
|
|
448
|
+
</Menu.Item>
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
};
|
|
452
|
+
|
|
388
453
|
return (
|
|
389
454
|
<div className="sidemenu">
|
|
390
455
|
{loading ? (
|
|
@@ -490,153 +555,7 @@ export default function SideMenu({ loading, modules = [], callback, appSettings,
|
|
|
490
555
|
})
|
|
491
556
|
} */}
|
|
492
557
|
|
|
493
|
-
{
|
|
494
|
-
|
|
495
|
-
.filter((record) => {
|
|
496
|
-
icon = record;
|
|
497
|
-
|
|
498
|
-
// Drop entries without a caption — they have no permission/label
|
|
499
|
-
// to render, so showing just an icon is misleading.
|
|
500
|
-
const hasCaption = typeof record.caption === 'string' && record.caption.trim().length > 0;
|
|
501
|
-
if (!hasCaption) return false;
|
|
502
|
-
|
|
503
|
-
if (record.id) {
|
|
504
|
-
if (record.is_visible) {
|
|
505
|
-
return true;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
if (record.icon_name) {
|
|
509
|
-
return true;
|
|
510
|
-
} else {
|
|
511
|
-
return false;
|
|
512
|
-
}
|
|
513
|
-
} else {
|
|
514
|
-
return true;
|
|
515
|
-
}
|
|
516
|
-
})
|
|
517
|
-
.map((menu, index) => {
|
|
518
|
-
// return <MenuItem menu={menu} index={index} />
|
|
519
|
-
|
|
520
|
-
let sub_menus = menu && menu.sub_menus
|
|
521
|
-
? Menus.screenMenus(menu.sub_menus).filter((s) => typeof s.caption === 'string' && s.caption.trim().length > 0)
|
|
522
|
-
: [];
|
|
523
|
-
|
|
524
|
-
if (menu && sub_menus && sub_menus.length) {
|
|
525
|
-
// let randomIndex = parseInt(Math.random() * 10000000000);
|
|
526
|
-
|
|
527
|
-
return (
|
|
528
|
-
<SubMenu
|
|
529
|
-
className="popup"
|
|
530
|
-
style={{ color: state.theme.colors.leftSectionColor }}
|
|
531
|
-
// key={`first-level-${randomIndex}-${menu.caption}`}
|
|
532
|
-
|
|
533
|
-
key={menu.id || menu.path || menu.caption}
|
|
534
|
-
title={
|
|
535
|
-
<>
|
|
536
|
-
<CollapsedIconMenu
|
|
537
|
-
menu={menu}
|
|
538
|
-
caption={menu.caption}
|
|
539
|
-
icon={menu.icon_name || 'fa-solid fas fa-user'}
|
|
540
|
-
collapsed={collapsed}
|
|
541
|
-
/>
|
|
542
|
-
</>
|
|
543
|
-
}
|
|
544
|
-
>
|
|
545
|
-
{sub_menus.map((submenu, innerIndex) => {
|
|
546
|
-
// let randomIndex = parseInt(Math.random() * 10000000000);
|
|
547
|
-
|
|
548
|
-
let third_menus = submenu && submenu.sub_menus ? Menus.screenMenus(submenu.sub_menus) : [];
|
|
549
|
-
|
|
550
|
-
if (third_menus && third_menus.length) {
|
|
551
|
-
return (
|
|
552
|
-
<SubMenu
|
|
553
|
-
className="popup"
|
|
554
|
-
// key={`second-level-${randomIndex}-${submenu.id}`}
|
|
555
|
-
|
|
556
|
-
key={submenu.id || submenu.path || submenu.caption}
|
|
557
|
-
title={
|
|
558
|
-
<span>
|
|
559
|
-
<CollapsedIconMenu
|
|
560
|
-
menu={menu}
|
|
561
|
-
caption={submenu.caption}
|
|
562
|
-
icon={submenu.icon_name || 'fa-solid fas fa-user'}
|
|
563
|
-
collapsed={collapsed}
|
|
564
|
-
/>
|
|
565
|
-
</span>
|
|
566
|
-
}
|
|
567
|
-
>
|
|
568
|
-
{third_menus.map((menu) => {
|
|
569
|
-
// let randomIndex = parseInt(Math.random() * 10000000000);
|
|
570
|
-
|
|
571
|
-
return (
|
|
572
|
-
<Menu.Item
|
|
573
|
-
// onClick={() => {
|
|
574
|
-
// onMenuClick(menu, index);
|
|
575
|
-
// }}
|
|
576
|
-
onClick={() => {
|
|
577
|
-
onMenuClick({ ...menu, parentKey: submenu.path || submenu.caption }, index);
|
|
578
|
-
}}
|
|
579
|
-
// key={`second-level-${randomIndex}-${index}`}
|
|
580
|
-
|
|
581
|
-
key={menu.path || menu.caption}
|
|
582
|
-
>
|
|
583
|
-
<CollapsedIconMenu
|
|
584
|
-
menu={menu}
|
|
585
|
-
caption={menu.caption}
|
|
586
|
-
icon={menu.icon_name || 'fa-solid fas fa-user'}
|
|
587
|
-
collapsed={collapsed}
|
|
588
|
-
/>
|
|
589
|
-
</Menu.Item>
|
|
590
|
-
);
|
|
591
|
-
})}
|
|
592
|
-
</SubMenu>
|
|
593
|
-
);
|
|
594
|
-
} else {
|
|
595
|
-
// let randomIndex = parseInt(Math.random() * 10000000000);
|
|
596
|
-
|
|
597
|
-
return (
|
|
598
|
-
<Menu.Item
|
|
599
|
-
// onClick={() => {
|
|
600
|
-
// onMenuClick(submenu, index);
|
|
601
|
-
// }}
|
|
602
|
-
onClick={() => {
|
|
603
|
-
onMenuClick({ ...submenu, parentKey: menu.path || menu.caption }, index);
|
|
604
|
-
}}
|
|
605
|
-
// key={`first-level-${randomIndex}-${innerIndex}`}
|
|
606
|
-
key={submenu.path || submenu.caption}
|
|
607
|
-
>
|
|
608
|
-
<CollapsedIconMenu
|
|
609
|
-
menu={menu}
|
|
610
|
-
caption={submenu.caption}
|
|
611
|
-
icon={submenu.icon_name || 'fa-solid fas fa-user'}
|
|
612
|
-
collapsed={collapsed}
|
|
613
|
-
/>
|
|
614
|
-
</Menu.Item>
|
|
615
|
-
);
|
|
616
|
-
}
|
|
617
|
-
})}
|
|
618
|
-
</SubMenu>
|
|
619
|
-
);
|
|
620
|
-
} else {
|
|
621
|
-
// let randomIndex = parseInt(Math.random() * 10000000000);
|
|
622
|
-
|
|
623
|
-
return (
|
|
624
|
-
<Menu.Item
|
|
625
|
-
// onClick={() => {
|
|
626
|
-
// onMenuClick(menu, index);
|
|
627
|
-
// }}
|
|
628
|
-
|
|
629
|
-
onClick={() => {
|
|
630
|
-
onMenuClick({ ...menu, parentKey: menu.path || menu.caption, isRoot: true }, index);
|
|
631
|
-
}}
|
|
632
|
-
// key={`${menu.id}-${randomIndex}`}
|
|
633
|
-
key={menu.path || menu.caption}
|
|
634
|
-
>
|
|
635
|
-
<CollapsedIconMenu menu={menu} caption={menu.caption} icon={menu.icon_name || 'fa-solid fas fa-user'} collapsed={collapsed} />
|
|
636
|
-
</Menu.Item>
|
|
637
|
-
);
|
|
638
|
-
}
|
|
639
|
-
})}
|
|
558
|
+
{renderMenuTree(modules, { isRoot: true })}
|
|
640
559
|
|
|
641
560
|
{loading ? (
|
|
642
561
|
<div class="skeleton-wrapper"></div>
|
|
@@ -116,6 +116,13 @@ const MenuAdd = ({ model, callback, edit, history, formContent, match, additiona
|
|
|
116
116
|
values.step = formContent.step || step;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// Cleared selects come back as undefined; send null so the backend clears them
|
|
120
|
+
['model_id', 'page_id', 'header_id'].forEach((field) => {
|
|
121
|
+
if (values[field] === undefined) {
|
|
122
|
+
values[field] = null;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
119
126
|
if (values.attributes && typeof values === 'object') {
|
|
120
127
|
values = {
|
|
121
128
|
...values,
|
|
@@ -183,7 +190,7 @@ const MenuAdd = ({ model, callback, edit, history, formContent, match, additiona
|
|
|
183
190
|
{/* Model */}
|
|
184
191
|
<Form.Item label="Models" name={'model_id'}>
|
|
185
192
|
{/* <ReferenceSelect value={model.id} label={model.name} model={Models} /> */}
|
|
186
|
-
<Select showSearch style={{ width: '100%' }} placeholder="Select a Page" optionFilterProp="label">
|
|
193
|
+
<Select showSearch style={{ width: '100%' }} placeholder="Select a Page" optionFilterProp="label" allowClear>
|
|
187
194
|
{models.map((model, key) => (
|
|
188
195
|
<Option key={key} label={model.name} value={model.id}>
|
|
189
196
|
{model.name}
|
|
@@ -198,7 +205,7 @@ const MenuAdd = ({ model, callback, edit, history, formContent, match, additiona
|
|
|
198
205
|
<Form.Item label="Pages" name={'page_id'}>
|
|
199
206
|
{/* <ReferenceSelect value={pages.id} label={pages.name} model={Pages} /> */}
|
|
200
207
|
|
|
201
|
-
<Select showSearch optionFilterProp="children" style={{ width: '100%' }}>
|
|
208
|
+
<Select showSearch optionFilterProp="children" style={{ width: '100%' }} allowClear>
|
|
202
209
|
{pages.map((model, key) => (
|
|
203
210
|
<Option key={key} value={model.id}>
|
|
204
211
|
{model.name}
|
|
@@ -229,13 +236,33 @@ const MenuAdd = ({ model, callback, edit, history, formContent, match, additiona
|
|
|
229
236
|
{/* Pages Ends */}
|
|
230
237
|
|
|
231
238
|
{/* Path */}
|
|
232
|
-
<Form.Item
|
|
239
|
+
<Form.Item
|
|
240
|
+
name="path"
|
|
241
|
+
label="Path"
|
|
242
|
+
rules={[
|
|
243
|
+
{ required: true, message: 'Path is required' },
|
|
244
|
+
{
|
|
245
|
+
pattern: /^\//,
|
|
246
|
+
message: 'Path must start with /',
|
|
247
|
+
},
|
|
248
|
+
]}
|
|
249
|
+
>
|
|
233
250
|
<Input placeholder="Enter path" />
|
|
234
251
|
</Form.Item>
|
|
235
252
|
{/* Path Ends */}
|
|
236
253
|
|
|
237
254
|
{/* Route */}
|
|
238
|
-
<Form.Item
|
|
255
|
+
<Form.Item
|
|
256
|
+
name="route"
|
|
257
|
+
label="Route"
|
|
258
|
+
rules={[
|
|
259
|
+
{ required: false, message: 'Route is required' },
|
|
260
|
+
{
|
|
261
|
+
pattern: /^\//,
|
|
262
|
+
message: 'Route must start with /',
|
|
263
|
+
},
|
|
264
|
+
]}
|
|
265
|
+
>
|
|
239
266
|
<Input placeholder="Enter route" />
|
|
240
267
|
</Form.Item>
|
|
241
268
|
{/* Route Ends */}
|
|
@@ -30,8 +30,9 @@ const MenuLists = ({ model, match, relativeAdd = false, additional_queries = [],
|
|
|
30
30
|
const [query, setQuery] = useState('');
|
|
31
31
|
const [dragMode, setDragMode] = useState(false);
|
|
32
32
|
const [orderChanged, setOrderChanged] = useState(false);
|
|
33
|
-
// keys of
|
|
34
|
-
|
|
33
|
+
// keys of currently-expanded top-level panels. Seeded from the search results
|
|
34
|
+
// when the query changes, but the user can freely toggle panels afterwards.
|
|
35
|
+
const [openKeys, setOpenKeys] = useState([]);
|
|
35
36
|
|
|
36
37
|
const [nextId, setNextId] = useState(10000);
|
|
37
38
|
|
|
@@ -183,6 +184,16 @@ const MenuLists = ({ model, match, relativeAdd = false, additional_queries = [],
|
|
|
183
184
|
return;
|
|
184
185
|
}
|
|
185
186
|
|
|
187
|
+
// Prevent moving a menu into one of its own descendants
|
|
188
|
+
// (e.g. A > B, C — A cannot be dropped under B or C).
|
|
189
|
+
if (targetParentId) {
|
|
190
|
+
const draggedNode = findMenuById(records, draggedItem.id);
|
|
191
|
+
if (draggedNode && findMenuById(draggedNode.sub_menus || [], targetParentId)) {
|
|
192
|
+
message.warning('Cannot move a menu into its own sub menu');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
186
197
|
// Remove item from original location
|
|
187
198
|
const { newItems: itemsAfterRemove, foundItem } = findAndRemoveItem(records, draggedItem.id);
|
|
188
199
|
|
|
@@ -196,7 +207,7 @@ const MenuLists = ({ model, match, relativeAdd = false, additional_queries = [],
|
|
|
196
207
|
|
|
197
208
|
setRecords(finalItems);
|
|
198
209
|
setOrderChanged(true);
|
|
199
|
-
message.success(`Moved "${foundItem.
|
|
210
|
+
message.success(`Moved "${foundItem.caption}" to level ${targetLevel}`);
|
|
200
211
|
},
|
|
201
212
|
[records]
|
|
202
213
|
);
|
|
@@ -234,10 +245,13 @@ const MenuLists = ({ model, match, relativeAdd = false, additional_queries = [],
|
|
|
234
245
|
const searchActive = !!query;
|
|
235
246
|
const visibleItems = filterMenus(records, query);
|
|
236
247
|
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
|
|
248
|
+
// When the query changes, seed the open panels from the search results (every
|
|
249
|
+
// branch on the path to a match) so the matched sub-menu is revealed. After
|
|
250
|
+
// that the user can collapse/expand freely via the Collapse onChange below.
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
setOpenKeys(query ? expandableKeys(filterMenus(records, query)) : []);
|
|
253
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
254
|
+
}, [query]);
|
|
241
255
|
|
|
242
256
|
const onSearch = (event) => {
|
|
243
257
|
setQuery(event.target.value);
|
|
@@ -395,8 +409,8 @@ const MenuLists = ({ model, match, relativeAdd = false, additional_queries = [],
|
|
|
395
409
|
<DndProvider backend={HTML5Backend}>
|
|
396
410
|
<Collapse
|
|
397
411
|
accordion={!searchActive}
|
|
398
|
-
activeKey={
|
|
399
|
-
onChange={(keys) =>
|
|
412
|
+
activeKey={openKeys}
|
|
413
|
+
onChange={(keys) => setOpenKeys(Array.isArray(keys) ? keys : keys ? [keys] : [])}
|
|
400
414
|
>
|
|
401
415
|
{visibleItems && visibleItems.length > 0 ? (
|
|
402
416
|
visibleItems.map((item, index) => (
|
|
@@ -549,13 +563,21 @@ function NestedMenu({
|
|
|
549
563
|
onChange,
|
|
550
564
|
}) {
|
|
551
565
|
const [localItems, setLocalItems] = useState(items);
|
|
552
|
-
// keys of
|
|
553
|
-
|
|
566
|
+
// keys of currently-expanded sub-panels. Seeded from the search results when
|
|
567
|
+
// the query changes, but the user can freely toggle panels afterwards.
|
|
568
|
+
const [openKeys, setOpenKeys] = useState([]);
|
|
554
569
|
|
|
555
570
|
useEffect(() => {
|
|
556
571
|
setLocalItems(items);
|
|
557
572
|
}, [items]);
|
|
558
573
|
|
|
574
|
+
// Seed the open sub-panels from the search path when the query changes; keep
|
|
575
|
+
// the user's toggles otherwise so a search-opened panel can be closed.
|
|
576
|
+
useEffect(() => {
|
|
577
|
+
setOpenKeys(searchActive ? (items || []).filter((c) => c.sub_menus && c.sub_menus.length > 0).map((c) => String(c.id)) : []);
|
|
578
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
579
|
+
}, [query, searchActive]);
|
|
580
|
+
|
|
559
581
|
const moveSubMenu = useCallback(
|
|
560
582
|
(from, to) => {
|
|
561
583
|
if (!dragMode || from === to) return;
|
|
@@ -574,17 +596,11 @@ function NestedMenu({
|
|
|
574
596
|
return null;
|
|
575
597
|
}
|
|
576
598
|
|
|
577
|
-
// While searching, derive the open sub-panels from the items so the path to a
|
|
578
|
-
// match stays expanded; otherwise use the user's manually-expanded panels.
|
|
579
|
-
const activeKeys = searchActive
|
|
580
|
-
? localItems.filter((c) => c.sub_menus && c.sub_menus.length > 0).map((c) => String(c.id))
|
|
581
|
-
: manualOpenKeys;
|
|
582
|
-
|
|
583
599
|
return (
|
|
584
600
|
<Collapse
|
|
585
601
|
accordion={!searchActive}
|
|
586
|
-
activeKey={
|
|
587
|
-
onChange={(keys) =>
|
|
602
|
+
activeKey={openKeys}
|
|
603
|
+
onChange={(keys) => setOpenKeys(Array.isArray(keys) ? keys : keys ? [keys] : [])}
|
|
588
604
|
>
|
|
589
605
|
{localItems.map((child, index) => (
|
|
590
606
|
<Panel
|
|
@@ -145,11 +145,19 @@ const RoleAdd = ({ model, callback, edit, formContent = {}, match, additional_qu
|
|
|
145
145
|
});
|
|
146
146
|
|
|
147
147
|
try {
|
|
148
|
-
await RolesAPI.createRole(payload); // single API
|
|
148
|
+
const res = await RolesAPI.createRole(payload); // single API
|
|
149
|
+
|
|
150
|
+
// it via success: false — show that message as a warning.
|
|
151
|
+
if (res?.success === false) {
|
|
152
|
+
message.warning(res.message || 'Failed to save role');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
149
156
|
message.success(formContent?.id ? 'Role Updated' : 'Role Added');
|
|
150
157
|
callback();
|
|
151
158
|
} catch (err) {
|
|
152
|
-
//
|
|
159
|
+
// Surface any unexpected backend error message as a warning.
|
|
160
|
+
message.warning(err?.message || err?.result?.message || 'Failed to save role');
|
|
153
161
|
} finally {
|
|
154
162
|
onSubmittingChange?.(false);
|
|
155
163
|
}
|
|
@@ -163,6 +163,9 @@ class RolesAPI extends BaseAPI {
|
|
|
163
163
|
return ApiUtils.post({
|
|
164
164
|
url: `core-roles/save-core-role`,
|
|
165
165
|
formBody,
|
|
166
|
+
// Let the caller surface the backend message (as a warning) instead of
|
|
167
|
+
// the default red error toast from the http layer.
|
|
168
|
+
hideError: true,
|
|
166
169
|
});
|
|
167
170
|
};
|
|
168
171
|
|