ui-soxo-bootstrap-core 2.6.37 → 2.6.40-dev.0
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/elements/basic/dragabble-wrapper/draggable-wrapper.js +91 -24
- package/core/lib/elements/basic/table/table.js +1 -6
- package/core/lib/elements/complex/qrscanner/qrscanner.js +1 -1
- package/core/models/roles/components/role-add/menu-label.js +14 -0
- package/core/models/roles/components/role-add/menu-tree.js +127 -0
- package/core/models/roles/components/role-add/role-add.js +44 -167
- package/core/models/roles/components/role-list/role-list.js +20 -0
- package/core/models/users/components/assign-role/assign-role.js +23 -8
- package/core/modules/steps/narration.js +192 -0
- package/core/modules/steps/progress-storage.js +140 -0
- package/core/modules/steps/steps.js +203 -220
- package/core/modules/steps/steps.scss +41 -6
- package/package.json +1 -1
|
@@ -1,26 +1,27 @@
|
|
|
1
|
-
import React, { useRef } from 'react';
|
|
1
|
+
import React, { useRef, useEffect } from 'react';
|
|
2
2
|
import { useDrag, useDrop } from 'react-dnd';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
window.scrollBy(0, -SPEED);
|
|
17
|
-
}
|
|
4
|
+
// Walk up the DOM to find the nearest vertically-scrollable ancestor.
|
|
5
|
+
// Returns null when the page (window) is the scroller.
|
|
6
|
+
function getScrollableAncestor(node) {
|
|
7
|
+
let el = node?.parentElement;
|
|
8
|
+
while (el) {
|
|
9
|
+
const { overflowY } = window.getComputedStyle(el);
|
|
10
|
+
const scrollable = overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay';
|
|
11
|
+
if (scrollable && el.scrollHeight > el.clientHeight) return el;
|
|
12
|
+
el = el.parentElement;
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
export default function DraggableWrapper({ id, index, movePanel, item, dragEnabled, level, parentId, onCrossLevelMove, canAcceptChildren }) {
|
|
18
|
+
// Root DOM node, used to locate the scrollable ancestor at drag time
|
|
19
|
+
const rootRef = useRef(null);
|
|
20
|
+
// Latest pointer Y, fed by a document-level `dragover` listener (see effect below)
|
|
21
|
+
const pointerY = useRef(0);
|
|
22
|
+
const rafRef = useRef(null);
|
|
23
|
+
// The scrollable container to auto-scroll (resolved when a drag starts)
|
|
24
|
+
const scrollContainerRef = useRef(null);
|
|
24
25
|
|
|
25
26
|
const [{ isDragging }, drag] = useDrag({
|
|
26
27
|
type: 'PANEL',
|
|
@@ -31,12 +32,78 @@ export default function DraggableWrapper({ id, index, movePanel, item, dragEnabl
|
|
|
31
32
|
}),
|
|
32
33
|
});
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Continuous edge auto-scroll while THIS item is being dragged.
|
|
37
|
+
*
|
|
38
|
+
* Driven by a document-level `dragover` listener + requestAnimationFrame loop
|
|
39
|
+
* rather than the drop target's `hover` handler. `hover` only fires while the
|
|
40
|
+
* pointer is over a droppable panel and only on movement, so scrolling UP
|
|
41
|
+
* failed: the top of the viewport is the (non-droppable) header, and holding
|
|
42
|
+
* the pointer still at an edge produced no events. The rAF loop keeps
|
|
43
|
+
* scrolling from the last known pointer position regardless of what's beneath.
|
|
44
|
+
*
|
|
45
|
+
* The list may scroll inside an overflow container rather than the window, so
|
|
46
|
+
* we resolve the nearest scrollable ancestor and scroll that (falling back to
|
|
47
|
+
* the window), computing the edges from the container's own bounds.
|
|
48
|
+
*/
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!isDragging) return;
|
|
51
|
+
|
|
52
|
+
const EDGE = 80;
|
|
53
|
+
const SPEED = 20;
|
|
54
|
+
|
|
55
|
+
scrollContainerRef.current = getScrollableAncestor(rootRef.current);
|
|
56
|
+
|
|
57
|
+
const onDragOver = (e) => {
|
|
58
|
+
pointerY.current = e.clientY;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const tick = () => {
|
|
62
|
+
const y = pointerY.current;
|
|
63
|
+
|
|
64
|
+
if (y > 0) {
|
|
65
|
+
const container = scrollContainerRef.current;
|
|
66
|
+
|
|
67
|
+
if (container) {
|
|
68
|
+
const rect = container.getBoundingClientRect();
|
|
69
|
+
// near container top
|
|
70
|
+
if (y < rect.top + EDGE) {
|
|
71
|
+
container.scrollTop -= SPEED;
|
|
72
|
+
}
|
|
73
|
+
// near container bottom
|
|
74
|
+
else if (y > rect.bottom - EDGE) {
|
|
75
|
+
container.scrollTop += SPEED;
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
const viewportHeight = window.innerHeight;
|
|
79
|
+
// near viewport top
|
|
80
|
+
if (y < EDGE) {
|
|
81
|
+
window.scrollBy(0, -SPEED);
|
|
82
|
+
}
|
|
83
|
+
// near viewport bottom
|
|
84
|
+
else if (y > viewportHeight - EDGE) {
|
|
85
|
+
window.scrollBy(0, SPEED);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
window.addEventListener('dragover', onDragOver);
|
|
94
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
95
|
+
|
|
96
|
+
return () => {
|
|
97
|
+
window.removeEventListener('dragover', onDragOver);
|
|
98
|
+
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
99
|
+
pointerY.current = 0;
|
|
100
|
+
scrollContainerRef.current = null;
|
|
101
|
+
};
|
|
102
|
+
}, [isDragging]);
|
|
103
|
+
|
|
34
104
|
const [{ isOver, canDrop }, drop] = useDrop({
|
|
35
105
|
accept: 'PANEL',
|
|
36
106
|
hover: (dragItem, monitor) => {
|
|
37
|
-
// THIS FIXES BOTTOM → TOP
|
|
38
|
-
autoScrollWindow(monitor);
|
|
39
|
-
|
|
40
107
|
if (dragItem.index === index) return;
|
|
41
108
|
|
|
42
109
|
if (dragItem.level === level && dragItem.parentId === parentId) {
|
|
@@ -85,7 +152,7 @@ export default function DraggableWrapper({ id, index, movePanel, item, dragEnabl
|
|
|
85
152
|
const childZoneBackgroundColor = isOverChild && canDropChild ? '#d4f4dd' : 'transparent';
|
|
86
153
|
|
|
87
154
|
return (
|
|
88
|
-
<div style={{ width: '100%', display: 'flex' }}>
|
|
155
|
+
<div ref={rootRef} style={{ width: '100%', display: 'flex' }}>
|
|
89
156
|
{/* HEADER DROP — reorder only */}
|
|
90
157
|
<div ref={drop}>
|
|
91
158
|
<div
|
|
@@ -58,12 +58,7 @@ const TableComponent = ({ columns, dataSource, loading, fixed, scroll, summary,
|
|
|
58
58
|
|
|
59
59
|
return (
|
|
60
60
|
<Table
|
|
61
|
-
title={() =>
|
|
62
|
-
// <div style={{ fontWeight: 'bold', fontSize: 16, padding: '8px 16px' }}>
|
|
63
|
-
title ? title : ''
|
|
64
|
-
// </div>
|
|
65
|
-
}
|
|
66
|
-
// columns={updatedColumns}
|
|
61
|
+
title={title ? () => title : undefined}
|
|
67
62
|
scroll={scroll}
|
|
68
63
|
dataSource={dataSource}
|
|
69
64
|
loading={loading}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// ------------------------
|
|
2
|
+
// Menu caption + path label
|
|
3
|
+
// ------------------------
|
|
4
|
+
const MenuLabel = ({ menu }) => {
|
|
5
|
+
const path = menu.path || menu.route;
|
|
6
|
+
return (
|
|
7
|
+
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: 8, lineHeight: 1.3 }}>
|
|
8
|
+
<span>{menu.caption}</span>
|
|
9
|
+
{path ? <span style={{ color: '#999', fontSize: 12 }}>{path}</span> : null}
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default MenuLabel;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Collapse, Checkbox, Tag } from 'antd';
|
|
2
|
+
|
|
3
|
+
import MenuLabel from './menu-label';
|
|
4
|
+
|
|
5
|
+
const { Panel } = Collapse;
|
|
6
|
+
|
|
7
|
+
// ------------------------
|
|
8
|
+
// Recursive Nested Menu Component
|
|
9
|
+
// ------------------------
|
|
10
|
+
const MenuTree = ({ menus, selectedMenus, toggleMenu, parentId = null, searchActive = false }) => {
|
|
11
|
+
// Helper: check if parent should be checked
|
|
12
|
+
const isParentChecked = (menu) => {
|
|
13
|
+
if (!menu.sub_menus || menu.sub_menus.length === 0) {
|
|
14
|
+
return selectedMenus.includes(menu.id);
|
|
15
|
+
}
|
|
16
|
+
const allChildIds = menu.sub_menus.map((c) => c.id);
|
|
17
|
+
return allChildIds.every((id) => selectedMenus.includes(id));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Helper: check if parent is indeterminate
|
|
21
|
+
const isParentIndeterminate = (menu) => {
|
|
22
|
+
if (!menu.sub_menus || menu.sub_menus.length === 0) return false;
|
|
23
|
+
const allChildIds = menu.sub_menus.map((c) => c.id);
|
|
24
|
+
const checkedCount = allChildIds.filter((id) => selectedMenus.includes(id)).length;
|
|
25
|
+
return checkedCount > 0 && checkedCount < allChildIds.length;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
{menus.map((menu) => {
|
|
31
|
+
const children = menu.sub_menus || [];
|
|
32
|
+
const parentChecked = isParentChecked(menu);
|
|
33
|
+
const parentIndeterminate = isParentIndeterminate(menu);
|
|
34
|
+
|
|
35
|
+
const onParentChange = (checked) => {
|
|
36
|
+
toggleMenu(menu.id, checked);
|
|
37
|
+
// toggle children recursively
|
|
38
|
+
children.forEach((c) => toggleMenuRecursive(c, checked));
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const toggleMenuRecursive = (menu, checked) => {
|
|
42
|
+
toggleMenu(menu.id, checked);
|
|
43
|
+
if (menu.sub_menus && menu.sub_menus.length > 0) {
|
|
44
|
+
menu.sub_menus.forEach((c) => toggleMenuRecursive(c, checked));
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (children.length === 0) {
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
style={{
|
|
52
|
+
justifyContent: 'space-between',
|
|
53
|
+
display: 'flex',
|
|
54
|
+
alignItems: 'center',
|
|
55
|
+
border: '1px solid rgba(198, 195, 195, 0.85)',
|
|
56
|
+
// borderRadius: 6,
|
|
57
|
+
padding: '12px 16px',
|
|
58
|
+
marginBottom: 6,
|
|
59
|
+
background: '#fff',
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<div
|
|
63
|
+
key={menu.id}
|
|
64
|
+
style={{
|
|
65
|
+
display: 'flex',
|
|
66
|
+
alignItems: 'center',
|
|
67
|
+
gap: 8,
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<Checkbox
|
|
71
|
+
checked={selectedMenus.includes(menu.id)}
|
|
72
|
+
onChange={(e) => {
|
|
73
|
+
const checked = e.target.checked;
|
|
74
|
+
|
|
75
|
+
toggleMenu(menu.id, checked);
|
|
76
|
+
|
|
77
|
+
// FORCE parent selection
|
|
78
|
+
if (checked && parentId) {
|
|
79
|
+
toggleMenu(parentId, true);
|
|
80
|
+
}
|
|
81
|
+
}}
|
|
82
|
+
/>
|
|
83
|
+
<MenuLabel menu={menu} />
|
|
84
|
+
</div>
|
|
85
|
+
<Tag color={menu.is_visible === true ? 'green' : 'blue'}>{menu.is_visible === true ? 'VISIBLE' : 'HIDDEN'}</Tag>{' '}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Collapse
|
|
92
|
+
// remount when search toggles so panels open while searching and collapse when cleared
|
|
93
|
+
key={`${menu.id}-${searchActive}`}
|
|
94
|
+
style={{ marginBottom: 6 }}
|
|
95
|
+
defaultActiveKey={searchActive ? [menu.id] : undefined}
|
|
96
|
+
>
|
|
97
|
+
<Panel
|
|
98
|
+
key={menu.id}
|
|
99
|
+
header={
|
|
100
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
101
|
+
<div
|
|
102
|
+
style={{
|
|
103
|
+
display: 'flex',
|
|
104
|
+
alignItems: 'center',
|
|
105
|
+
gap: 8,
|
|
106
|
+
}}
|
|
107
|
+
onClick={(e) => e.stopPropagation()}
|
|
108
|
+
>
|
|
109
|
+
<Checkbox checked={parentChecked} indeterminate={parentIndeterminate} onChange={(e) => onParentChange(e.target.checked)} />
|
|
110
|
+
<MenuLabel menu={menu} />
|
|
111
|
+
</div>
|
|
112
|
+
<Tag color={menu.is_visible === true ? 'green' : 'blue'}>{menu.is_visible === true ? 'VISIBLE' : 'HIDDEN'}</Tag>{' '}
|
|
113
|
+
</div>
|
|
114
|
+
}
|
|
115
|
+
>
|
|
116
|
+
<div style={{ paddingLeft: 20 }}>
|
|
117
|
+
<MenuTree menus={children} selectedMenus={selectedMenus} toggleMenu={toggleMenu} parentId={menu.id} searchActive={searchActive} />
|
|
118
|
+
</div>
|
|
119
|
+
</Panel>
|
|
120
|
+
</Collapse>
|
|
121
|
+
);
|
|
122
|
+
})}
|
|
123
|
+
</>
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export default MenuTree;
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import React, { useState, useEffect, useContext } from 'react';
|
|
2
2
|
import { useLocation, useParams } from 'react-router-dom';
|
|
3
|
-
import { Skeleton, Typography, message, Form, Input
|
|
3
|
+
import { Skeleton, Typography, message, Form, Input } from 'antd';
|
|
4
4
|
import { GlobalContext } from './../../../../lib';
|
|
5
|
-
import { Button } from './../../../../lib';
|
|
6
5
|
import { ModelsAPI, PagesAPI, RolesAPI, MenusAPI } from '../../..';
|
|
6
|
+
import MenuTree from './menu-tree';
|
|
7
7
|
import './role-add.scss';
|
|
8
8
|
|
|
9
9
|
const { Title } = Typography;
|
|
10
|
-
const { Panel } = Collapse;
|
|
11
10
|
|
|
12
|
-
const RoleAdd = ({ model, callback, edit, formContent = {}, match, additional_queries = [] }) => {
|
|
11
|
+
const RoleAdd = ({ model, callback, edit, formContent = {}, match, additional_queries = [], onSubmittingChange, formRef }) => {
|
|
13
12
|
let mode = 'Add';
|
|
14
13
|
if (formContent.id) mode = 'Edit';
|
|
15
14
|
else if (formContent.copy) mode = 'copy';
|
|
@@ -31,8 +30,10 @@ const RoleAdd = ({ model, callback, edit, formContent = {}, match, additional_qu
|
|
|
31
30
|
|
|
32
31
|
const isEdit = !!(formContent.id || formContent.copy);
|
|
33
32
|
const [initialLoading, setInitialLoading] = useState(isEdit);
|
|
34
|
-
const [loading, setLoading] = useState(false);
|
|
35
33
|
const [form] = Form.useForm();
|
|
34
|
+
|
|
35
|
+
// expose the form instance so the Drawer footer Save button can submit it
|
|
36
|
+
if (formRef) formRef.current = form;
|
|
36
37
|
const [pages, setPages] = useState([]);
|
|
37
38
|
const [models, setModels] = useState([]);
|
|
38
39
|
const [menuList, setMenuList] = useState([]);
|
|
@@ -43,6 +44,9 @@ const RoleAdd = ({ model, callback, edit, formContent = {}, match, additional_qu
|
|
|
43
44
|
// Selected menus (array of IDs)
|
|
44
45
|
const [selectedMenus, setSelectedMenus] = useState([]);
|
|
45
46
|
|
|
47
|
+
// Search term for filtering the menu list
|
|
48
|
+
const [menuSearch, setMenuSearch] = useState('');
|
|
49
|
+
|
|
46
50
|
const location = useLocation();
|
|
47
51
|
const query = new URLSearchParams(location.search);
|
|
48
52
|
let step = parseInt(query.get('step')) || 1;
|
|
@@ -97,40 +101,28 @@ const RoleAdd = ({ model, callback, edit, formContent = {}, match, additional_qu
|
|
|
97
101
|
setSelectedMenus((prev) => (checked ? [...prev, id] : prev.filter((m) => m !== id)));
|
|
98
102
|
};
|
|
99
103
|
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// additional_queries.forEach(({ field, value }) => {
|
|
121
|
-
// payload[field] = value;
|
|
122
|
-
// });
|
|
123
|
-
// RolesAPI.createRole(payload)
|
|
124
|
-
// .then(() => {
|
|
125
|
-
// message.success('Role Added');
|
|
126
|
-
// setLoading(false);
|
|
127
|
-
// callback();
|
|
128
|
-
// })
|
|
129
|
-
// .catch(() => setLoading(false));
|
|
130
|
-
// }
|
|
131
|
-
// };
|
|
104
|
+
// Recursively filter menus by caption. A menu is kept if its caption matches
|
|
105
|
+
// the search term, or if any of its descendants match (ancestors are kept so
|
|
106
|
+
// the matching child stays reachable).
|
|
107
|
+
const filterMenus = (menus, term) => {
|
|
108
|
+
if (!term) return menus;
|
|
109
|
+
const lower = term.toLowerCase();
|
|
110
|
+
|
|
111
|
+
return menus.reduce((acc, menu) => {
|
|
112
|
+
const children = filterMenus(menu.sub_menus || [], term);
|
|
113
|
+
const selfMatch = (menu.caption || '').toLowerCase().includes(lower);
|
|
114
|
+
|
|
115
|
+
if (selfMatch || children.length > 0) {
|
|
116
|
+
acc.push({ ...menu, sub_menus: children });
|
|
117
|
+
}
|
|
118
|
+
return acc;
|
|
119
|
+
}, []);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const filteredMenuList = filterMenus(menuList, menuSearch);
|
|
123
|
+
|
|
132
124
|
const onSubmit = async (values) => {
|
|
133
|
-
|
|
125
|
+
onSubmittingChange?.(true);
|
|
134
126
|
|
|
135
127
|
// Find menus that were originally selected but now deselected
|
|
136
128
|
const deselectedMenus = originalMenus.filter((id) => !selectedMenus.includes(id));
|
|
@@ -159,7 +151,7 @@ const RoleAdd = ({ model, callback, edit, formContent = {}, match, additional_qu
|
|
|
159
151
|
} catch (err) {
|
|
160
152
|
// message.error('Something went wrong');
|
|
161
153
|
} finally {
|
|
162
|
-
|
|
154
|
+
onSubmittingChange?.(false);
|
|
163
155
|
}
|
|
164
156
|
};
|
|
165
157
|
|
|
@@ -194,6 +186,7 @@ const RoleAdd = ({ model, callback, edit, formContent = {}, match, additional_qu
|
|
|
194
186
|
display: 'flex',
|
|
195
187
|
justifyContent: 'space-between',
|
|
196
188
|
alignItems: 'center',
|
|
189
|
+
gap: 16,
|
|
197
190
|
marginBottom: 16,
|
|
198
191
|
}}
|
|
199
192
|
>
|
|
@@ -204,14 +197,20 @@ const RoleAdd = ({ model, callback, edit, formContent = {}, match, additional_qu
|
|
|
204
197
|
<p style={{ color: '#999', marginBottom: 0 }}>Choose menus and set permissions</p>
|
|
205
198
|
</div>
|
|
206
199
|
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
200
|
+
<Input.Search
|
|
201
|
+
allowClear
|
|
202
|
+
placeholder="Search menus"
|
|
203
|
+
value={menuSearch}
|
|
204
|
+
onChange={(e) => setMenuSearch(e.target.value)}
|
|
205
|
+
style={{ maxWidth: 260 }}
|
|
206
|
+
/>
|
|
212
207
|
</div>
|
|
213
208
|
|
|
214
|
-
|
|
209
|
+
{filteredMenuList.length > 0 ? (
|
|
210
|
+
<MenuTree menus={filteredMenuList} selectedMenus={selectedMenus} toggleMenu={toggleMenu} searchActive={!!menuSearch} />
|
|
211
|
+
) : (
|
|
212
|
+
<p style={{ color: '#999', textAlign: 'center', padding: '16px 0' }}>No menus found</p>
|
|
213
|
+
)}
|
|
215
214
|
</div>
|
|
216
215
|
)}
|
|
217
216
|
</Form>
|
|
@@ -221,125 +220,3 @@ const RoleAdd = ({ model, callback, edit, formContent = {}, match, additional_qu
|
|
|
221
220
|
};
|
|
222
221
|
|
|
223
222
|
export default RoleAdd;
|
|
224
|
-
|
|
225
|
-
// ------------------------
|
|
226
|
-
// Recursive Nested Menu Component
|
|
227
|
-
// ------------------------
|
|
228
|
-
// ------------------------
|
|
229
|
-
// Recursive Nested Menu Component
|
|
230
|
-
// ------------------------
|
|
231
|
-
const MenuTree = ({ menus, selectedMenus, toggleMenu, parentId = null }) => {
|
|
232
|
-
// Helper: check if parent should be checked
|
|
233
|
-
const isParentChecked = (menu) => {
|
|
234
|
-
if (!menu.sub_menus || menu.sub_menus.length === 0) {
|
|
235
|
-
return selectedMenus.includes(menu.id);
|
|
236
|
-
}
|
|
237
|
-
const allChildIds = menu.sub_menus.map((c) => c.id);
|
|
238
|
-
return allChildIds.every((id) => selectedMenus.includes(id));
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
// Helper: check if parent is indeterminate
|
|
242
|
-
const isParentIndeterminate = (menu) => {
|
|
243
|
-
if (!menu.sub_menus || menu.sub_menus.length === 0) return false;
|
|
244
|
-
const allChildIds = menu.sub_menus.map((c) => c.id);
|
|
245
|
-
const checkedCount = allChildIds.filter((id) => selectedMenus.includes(id)).length;
|
|
246
|
-
return checkedCount > 0 && checkedCount < allChildIds.length;
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
return (
|
|
250
|
-
<>
|
|
251
|
-
{menus.map((menu) => {
|
|
252
|
-
const children = menu.sub_menus || [];
|
|
253
|
-
const parentChecked = isParentChecked(menu);
|
|
254
|
-
const parentIndeterminate = isParentIndeterminate(menu);
|
|
255
|
-
|
|
256
|
-
const onParentChange = (checked) => {
|
|
257
|
-
toggleMenu(menu.id, checked);
|
|
258
|
-
// toggle children recursively
|
|
259
|
-
children.forEach((c) => toggleMenuRecursive(c, checked));
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
const toggleMenuRecursive = (menu, checked) => {
|
|
263
|
-
toggleMenu(menu.id, checked);
|
|
264
|
-
if (menu.sub_menus && menu.sub_menus.length > 0) {
|
|
265
|
-
menu.sub_menus.forEach((c) => toggleMenuRecursive(c, checked));
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
if (children.length === 0) {
|
|
270
|
-
return (
|
|
271
|
-
<div
|
|
272
|
-
style={{
|
|
273
|
-
justifyContent: 'space-between',
|
|
274
|
-
display: 'flex',
|
|
275
|
-
alignItems: 'center',
|
|
276
|
-
border: '1px solid rgba(198, 195, 195, 0.85)',
|
|
277
|
-
// borderRadius: 6,
|
|
278
|
-
padding: '12px 16px',
|
|
279
|
-
marginBottom: 6,
|
|
280
|
-
background: '#fff',
|
|
281
|
-
}}
|
|
282
|
-
>
|
|
283
|
-
<div
|
|
284
|
-
key={menu.id}
|
|
285
|
-
style={{
|
|
286
|
-
display: 'flex',
|
|
287
|
-
alignItems: 'center',
|
|
288
|
-
gap: 8,
|
|
289
|
-
}}
|
|
290
|
-
>
|
|
291
|
-
<Checkbox
|
|
292
|
-
checked={selectedMenus.includes(menu.id)}
|
|
293
|
-
onChange={(e) => {
|
|
294
|
-
const checked = e.target.checked;
|
|
295
|
-
|
|
296
|
-
toggleMenu(menu.id, checked);
|
|
297
|
-
|
|
298
|
-
// FORCE parent selection
|
|
299
|
-
if (checked && parentId) {
|
|
300
|
-
toggleMenu(parentId, true);
|
|
301
|
-
}
|
|
302
|
-
}}
|
|
303
|
-
/>
|
|
304
|
-
<span>{menu.caption}</span>
|
|
305
|
-
</div>
|
|
306
|
-
<Tag color={menu.is_visible === true ? 'green' : 'blue'}>{menu.is_visible === true ? 'VISIBLE' : 'HIDDEN'}</Tag>{' '}
|
|
307
|
-
</div>
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return (
|
|
312
|
-
<Collapse
|
|
313
|
-
key={menu.id}
|
|
314
|
-
style={{ marginBottom: 6 }}
|
|
315
|
-
// defaultActiveKey={[menu.id]}
|
|
316
|
-
>
|
|
317
|
-
<Panel
|
|
318
|
-
key={menu.id}
|
|
319
|
-
header={
|
|
320
|
-
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
321
|
-
<div
|
|
322
|
-
style={{
|
|
323
|
-
display: 'flex',
|
|
324
|
-
alignItems: 'center',
|
|
325
|
-
gap: 8,
|
|
326
|
-
}}
|
|
327
|
-
onClick={(e) => e.stopPropagation()}
|
|
328
|
-
>
|
|
329
|
-
<Checkbox checked={parentChecked} indeterminate={parentIndeterminate} onChange={(e) => onParentChange(e.target.checked)} />
|
|
330
|
-
<span>{menu.caption}</span>
|
|
331
|
-
</div>
|
|
332
|
-
<Tag color={menu.is_visible === true ? 'green' : 'blue'}>{menu.is_visible === true ? 'VISIBLE' : 'HIDDEN'}</Tag>{' '}
|
|
333
|
-
</div>
|
|
334
|
-
}
|
|
335
|
-
>
|
|
336
|
-
<div style={{ paddingLeft: 20 }}>
|
|
337
|
-
<MenuTree menus={children} selectedMenus={selectedMenus} toggleMenu={toggleMenu} parentId={menu.id} />
|
|
338
|
-
</div>
|
|
339
|
-
</Panel>
|
|
340
|
-
</Collapse>
|
|
341
|
-
);
|
|
342
|
-
})}
|
|
343
|
-
</>
|
|
344
|
-
);
|
|
345
|
-
};
|
|
@@ -40,6 +40,12 @@ const RoleList = ({ model, match, relativeAdd = false, additional_queries = [],
|
|
|
40
40
|
|
|
41
41
|
const [single, setSingle] = useState({});
|
|
42
42
|
|
|
43
|
+
// tracks RoleAdd form submission so the Drawer footer button shows loading
|
|
44
|
+
const [submitting, setSubmitting] = useState(false);
|
|
45
|
+
|
|
46
|
+
// holds the RoleAdd form instance so the Drawer footer Save button can submit it
|
|
47
|
+
const formRef = useRef(null);
|
|
48
|
+
|
|
43
49
|
var [queryValue, setQueryValue] = useState('');
|
|
44
50
|
|
|
45
51
|
const { Search } = Input;
|
|
@@ -360,12 +366,26 @@ const RoleList = ({ model, match, relativeAdd = false, additional_queries = [],
|
|
|
360
366
|
destroyOnClose
|
|
361
367
|
onClose={closeModal}
|
|
362
368
|
bodyStyle={{ paddingBottom: 80 }}
|
|
369
|
+
footer={
|
|
370
|
+
<div style={{ textAlign: 'right' }}>
|
|
371
|
+
<Space size="small">
|
|
372
|
+
<Button type="default" onClick={closeModal}>
|
|
373
|
+
Cancel
|
|
374
|
+
</Button>
|
|
375
|
+
<Button type="primary" loading={submitting} onClick={() => formRef.current?.submit()}>
|
|
376
|
+
Save
|
|
377
|
+
</Button>
|
|
378
|
+
</Space>
|
|
379
|
+
</div>
|
|
380
|
+
}
|
|
363
381
|
>
|
|
364
382
|
<model.ModalAddComponent
|
|
365
383
|
match={match}
|
|
366
384
|
model={model}
|
|
367
385
|
additional_queries={additional_queries}
|
|
368
386
|
formContent={single}
|
|
387
|
+
formRef={formRef}
|
|
388
|
+
onSubmittingChange={setSubmitting}
|
|
369
389
|
callback={(event) => {
|
|
370
390
|
closeModal();
|
|
371
391
|
getRecords();
|