ui-soxo-bootstrap-core 2.6.40 → 2.6.42
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/lib/elements/basic/dragabble-wrapper/draggable-wrapper.js +91 -24
- package/core/lib/models/menus/components/menu-add/menu-add.js +2 -2
- package/core/models/menus/components/menu-add/menu-add.js +31 -4
- package/core/models/menus/components/menu-lists/menu-lists.js +77 -14
- 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 +54 -169
- package/core/models/roles/components/role-list/role-list.js +20 -0
- package/core/models/roles/roles.js +3 -0
- package/core/models/users/components/assign-role/assign-role.js +23 -8
- package/core/modules/steps/steps.js +40 -12
- package/core/modules/steps/steps.scss +166 -52
- package/package.json +1 -1
|
@@ -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));
|
|
@@ -153,13 +145,21 @@ const RoleAdd = ({ model, callback, edit, formContent = {}, match, additional_qu
|
|
|
153
145
|
});
|
|
154
146
|
|
|
155
147
|
try {
|
|
156
|
-
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
|
+
|
|
157
156
|
message.success(formContent?.id ? 'Role Updated' : 'Role Added');
|
|
158
157
|
callback();
|
|
159
158
|
} catch (err) {
|
|
160
|
-
//
|
|
159
|
+
// Surface any unexpected backend error message as a warning.
|
|
160
|
+
message.warning(err?.message || err?.result?.message || 'Failed to save role');
|
|
161
161
|
} finally {
|
|
162
|
-
|
|
162
|
+
onSubmittingChange?.(false);
|
|
163
163
|
}
|
|
164
164
|
};
|
|
165
165
|
|
|
@@ -194,6 +194,7 @@ const RoleAdd = ({ model, callback, edit, formContent = {}, match, additional_qu
|
|
|
194
194
|
display: 'flex',
|
|
195
195
|
justifyContent: 'space-between',
|
|
196
196
|
alignItems: 'center',
|
|
197
|
+
gap: 16,
|
|
197
198
|
marginBottom: 16,
|
|
198
199
|
}}
|
|
199
200
|
>
|
|
@@ -204,14 +205,20 @@ const RoleAdd = ({ model, callback, edit, formContent = {}, match, additional_qu
|
|
|
204
205
|
<p style={{ color: '#999', marginBottom: 0 }}>Choose menus and set permissions</p>
|
|
205
206
|
</div>
|
|
206
207
|
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
<Input.Search
|
|
209
|
+
allowClear
|
|
210
|
+
placeholder="Search menus"
|
|
211
|
+
value={menuSearch}
|
|
212
|
+
onChange={(e) => setMenuSearch(e.target.value)}
|
|
213
|
+
style={{ maxWidth: 260 }}
|
|
214
|
+
/>
|
|
212
215
|
</div>
|
|
213
216
|
|
|
214
|
-
|
|
217
|
+
{filteredMenuList.length > 0 ? (
|
|
218
|
+
<MenuTree menus={filteredMenuList} selectedMenus={selectedMenus} toggleMenu={toggleMenu} searchActive={!!menuSearch} />
|
|
219
|
+
) : (
|
|
220
|
+
<p style={{ color: '#999', textAlign: 'center', padding: '16px 0' }}>No menus found</p>
|
|
221
|
+
)}
|
|
215
222
|
</div>
|
|
216
223
|
)}
|
|
217
224
|
</Form>
|
|
@@ -221,125 +228,3 @@ const RoleAdd = ({ model, callback, edit, formContent = {}, match, additional_qu
|
|
|
221
228
|
};
|
|
222
229
|
|
|
223
230
|
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();
|
|
@@ -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
|
|
|
@@ -160,7 +160,7 @@ export default function AssignRole() {
|
|
|
160
160
|
const role_id = role.id;
|
|
161
161
|
try {
|
|
162
162
|
const res = await MenusAPI.getCoreMenuByRoleId(role_id);
|
|
163
|
-
const allMenus = res.result
|
|
163
|
+
const allMenus = Array.isArray(res?.result) ? res.result : [];
|
|
164
164
|
|
|
165
165
|
setModules(allMenus);
|
|
166
166
|
} catch (e) {
|
|
@@ -264,11 +264,12 @@ export default function AssignRole() {
|
|
|
264
264
|
const handleViewAll = async () => {
|
|
265
265
|
setLoadingMenus(true);
|
|
266
266
|
setModules([]);
|
|
267
|
-
setActiveRole({ name: 'All Roles' });
|
|
267
|
+
setActiveRole({ name: 'All Roles', isViewAll: true });
|
|
268
268
|
|
|
269
269
|
try {
|
|
270
270
|
const res = await MenusAPI.getMenubyUser(id);
|
|
271
|
-
|
|
271
|
+
const userMenus = Array.isArray(res?.result?.menus) ? res.result.menus : [];
|
|
272
|
+
setModules(userMenus);
|
|
272
273
|
setLoadingMenus(false);
|
|
273
274
|
} catch (err) {
|
|
274
275
|
setLoadingMenus(false);
|
|
@@ -277,6 +278,18 @@ export default function AssignRole() {
|
|
|
277
278
|
}
|
|
278
279
|
};
|
|
279
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Context-specific empty-state message for the right (menus) panel:
|
|
283
|
+
* - View Access for a user with no roles → no role assigned
|
|
284
|
+
* - A selected role with no menus → no menus assigned
|
|
285
|
+
* - Nothing selected yet → prompt to pick a role
|
|
286
|
+
*/
|
|
287
|
+
const emptyMenusMessage = !activeRole
|
|
288
|
+
? 'Select a role to view menus'
|
|
289
|
+
: activeRole.isViewAll
|
|
290
|
+
? 'No role has been assigned to this user.'
|
|
291
|
+
: 'No menus have been assigned to this role.';
|
|
292
|
+
|
|
280
293
|
return (
|
|
281
294
|
<section className="assign-role">
|
|
282
295
|
{/* LEFT PANEL */}
|
|
@@ -383,17 +396,19 @@ export default function AssignRole() {
|
|
|
383
396
|
padding: 16,
|
|
384
397
|
}}
|
|
385
398
|
>
|
|
386
|
-
|
|
387
|
-
<div className="
|
|
388
|
-
|
|
389
|
-
|
|
399
|
+
{modules.length > 0 && (
|
|
400
|
+
<div className="menus-header">
|
|
401
|
+
<div className="title">Menus {activeRole ? `– ${activeRole.name}` : ''}</div>
|
|
402
|
+
<div className="sub-text">You don’t have permission to edit this here. Update it in Role Settings.</div>
|
|
403
|
+
</div>
|
|
404
|
+
)}
|
|
390
405
|
|
|
391
406
|
<div className="menus-content">
|
|
392
407
|
{loadingMenus ? (
|
|
393
408
|
<Skeleton active paragraph={{ rows: 6 }} />
|
|
394
409
|
) : modules.length === 0 ? (
|
|
395
410
|
<div className="empty-state">
|
|
396
|
-
<Empty description=
|
|
411
|
+
<Empty description={emptyMenusMessage} />
|
|
397
412
|
</div>
|
|
398
413
|
) : (
|
|
399
414
|
<MenuTree menus={modules} selectedMenus={selectedMenus} toggleMenu={toggleMenu} showCheckbox={false} />
|
|
@@ -1290,7 +1290,27 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
1290
1290
|
</div>
|
|
1291
1291
|
)} */}
|
|
1292
1292
|
|
|
1293
|
-
<div className=
|
|
1293
|
+
<div className={`steps-top-bar${isStepFullscreen ? ' is-fullscreen' : ''}`}>
|
|
1294
|
+
{/*
|
|
1295
|
+
Full-screen toggle is anchored to the far left of the bar so the
|
|
1296
|
+
step tabs can stretch across the middle and the primary CTA sits
|
|
1297
|
+
flush right (see design).
|
|
1298
|
+
*/}
|
|
1299
|
+
<Button
|
|
1300
|
+
className="steps-fullscreen-toggle"
|
|
1301
|
+
type="dashed"
|
|
1302
|
+
icon={isStepFullscreen ? <CompressOutlined /> : <ExpandOutlined />}
|
|
1303
|
+
onClick={toggleStepFullscreen}
|
|
1304
|
+
aria-label={isStepFullscreen ? 'Exit Full Screen' : 'Full Screen'}
|
|
1305
|
+
title={isStepFullscreen ? 'Exit Full Screen' : 'Full Screen'}
|
|
1306
|
+
>
|
|
1307
|
+
{isStepFullscreen ? 'Exit Full Screen' : 'Full Screen'}
|
|
1308
|
+
</Button>
|
|
1309
|
+
|
|
1310
|
+
{/*
|
|
1311
|
+
Breadcrumb strip stays visible in fullscreen mode too so the
|
|
1312
|
+
user keeps the step timeline / navigation while presenting.
|
|
1313
|
+
*/}
|
|
1294
1314
|
<div className="steps-breadcrumb-strip">
|
|
1295
1315
|
{steps.length ? (
|
|
1296
1316
|
steps.map((stepItem, stepIndex) => {
|
|
@@ -1315,10 +1335,6 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
1315
1335
|
</div>
|
|
1316
1336
|
|
|
1317
1337
|
<div className="steps-nav-actions">
|
|
1318
|
-
<Button type="dashed" icon={isStepFullscreen ? <CompressOutlined /> : <ExpandOutlined />} onClick={toggleStepFullscreen}>
|
|
1319
|
-
{isStepFullscreen ? 'Exit Full Screen' : 'Full Screen'}
|
|
1320
|
-
</Button>
|
|
1321
|
-
|
|
1322
1338
|
{/*
|
|
1323
1339
|
Previous-process button.
|
|
1324
1340
|
- Only relevant at the start of a process (`activeStep === 0`)
|
|
@@ -1333,7 +1349,13 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
1333
1349
|
)}
|
|
1334
1350
|
|
|
1335
1351
|
{activeStep > 0 && (
|
|
1336
|
-
<Button
|
|
1352
|
+
<Button
|
|
1353
|
+
type="default"
|
|
1354
|
+
icon={<ArrowLeftOutlined />}
|
|
1355
|
+
onClick={handlePrevious}
|
|
1356
|
+
aria-label="Back"
|
|
1357
|
+
title="Back"
|
|
1358
|
+
>
|
|
1337
1359
|
Back
|
|
1338
1360
|
</Button>
|
|
1339
1361
|
)}
|
|
@@ -1366,7 +1388,13 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
1366
1388
|
)}
|
|
1367
1389
|
</>
|
|
1368
1390
|
) : (
|
|
1369
|
-
<Button
|
|
1391
|
+
<Button
|
|
1392
|
+
type="primary"
|
|
1393
|
+
disabled={!isStepCompleted}
|
|
1394
|
+
onClick={handleNext}
|
|
1395
|
+
aria-label={activeStep === 0 ? (FIRST_STEP_LABELS[processName?.trim().toLowerCase()] ?? 'Next') : 'Next'}
|
|
1396
|
+
title={activeStep === 0 ? (FIRST_STEP_LABELS[processName?.trim().toLowerCase()] ?? 'Next') : 'Next'}
|
|
1397
|
+
>
|
|
1370
1398
|
{/*
|
|
1371
1399
|
First-step label is resolved via FIRST_STEP_LABELS using
|
|
1372
1400
|
the process name (lowercased + trimmed) as the key. Known
|
|
@@ -1464,10 +1492,10 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
1464
1492
|
</button>
|
|
1465
1493
|
</>
|
|
1466
1494
|
) : null}
|
|
1467
|
-
<div
|
|
1495
|
+
{/* <div
|
|
1468
1496
|
key={`${currentProcessId}_${activeStep}`}
|
|
1469
1497
|
className={`steps-chat-step-card ${stepSlideDirection === 'backward' ? 'slide-backward' : 'slide-forward'}`}
|
|
1470
|
-
>
|
|
1498
|
+
> */}
|
|
1471
1499
|
{/* <div className="steps-chat-step-top">
|
|
1472
1500
|
<span className="steps-index-pill">
|
|
1473
1501
|
Step {Math.min(activeStep + 1, steps.length || 1)} of {steps.length || 1}
|
|
@@ -1476,15 +1504,15 @@ export default function ProcessStepsPage({ match, CustomComponents = {}, ...prop
|
|
|
1476
1504
|
{currentStep?.step_description ? <p className="steps-description">{currentStep.step_description}</p> : null}
|
|
1477
1505
|
</div> */}
|
|
1478
1506
|
|
|
1479
|
-
<div className="steps-chat-step-component">
|
|
1507
|
+
{/* <div className="steps-chat-step-component"> */}
|
|
1480
1508
|
{loading ? (
|
|
1481
1509
|
<div className="steps-chat-loading">
|
|
1482
1510
|
<Spin />
|
|
1483
1511
|
</div>
|
|
1484
1512
|
) : null}
|
|
1485
1513
|
{!loading ? renderDynamicStep() : null}
|
|
1486
|
-
</div>
|
|
1487
|
-
</div>
|
|
1514
|
+
{/* </div> */}
|
|
1515
|
+
{/* </div> */}
|
|
1488
1516
|
</div>
|
|
1489
1517
|
</div>
|
|
1490
1518
|
|