ui-soxo-bootstrap-core 2.4.25-dev.31 → 2.4.25-dev.32
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.
|
@@ -1,5 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* MenuTree Component
|
|
3
|
+
*
|
|
4
|
+
* @description
|
|
5
|
+
* Renders a hierarchical menu structure with optional checkboxes.
|
|
6
|
+
* Supports:
|
|
7
|
+
* - Recursive menu rendering
|
|
8
|
+
* - Parent–child checkbox synchronization
|
|
9
|
+
* - Indeterminate state for partially selected parents
|
|
10
|
+
*
|
|
11
|
+
* Common use cases:
|
|
12
|
+
* - Role-based menu preview
|
|
13
|
+
* - Permission assignment trees
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} props
|
|
16
|
+
* @param {Array<Object>} props.menus - Menu tree data
|
|
17
|
+
* @param {Array<number>} props.selectedMenus - Selected menu IDs
|
|
18
|
+
* @param {(menuId:number, checked:boolean) => void} props.toggleMenu - Callback for menu selection
|
|
19
|
+
* @param {number|null} props.parentId - Parent menu ID (used internally for recursion)
|
|
20
|
+
* @param {boolean} props.showCheckbox - Whether to display checkboxes
|
|
21
|
+
*
|
|
22
|
+
* @returns {JSX.Element}
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import React from 'react';
|
|
26
|
+
import { Checkbox, Collapse } from 'antd';
|
|
3
27
|
|
|
4
28
|
const { Panel } = Collapse;
|
|
5
29
|
|
|
@@ -8,6 +32,12 @@ export const MenuTree = ({ menus, selectedMenus = [], toggleMenu, parentId = nul
|
|
|
8
32
|
return menuList.map((menu) => {
|
|
9
33
|
const children = menu.sub_menus || [];
|
|
10
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Toggle menu selection recursively for all child menus
|
|
37
|
+
*
|
|
38
|
+
* @param {Object} m - Menu object
|
|
39
|
+
* @param {boolean} checked - Checkbox state
|
|
40
|
+
*/
|
|
11
41
|
const toggleMenuRecursive = (m, checked) => {
|
|
12
42
|
toggleMenu && toggleMenu(m.id, checked);
|
|
13
43
|
if (m.sub_menus && m.sub_menus.length > 0) {
|
|
@@ -15,49 +45,57 @@ export const MenuTree = ({ menus, selectedMenus = [], toggleMenu, parentId = nul
|
|
|
15
45
|
}
|
|
16
46
|
};
|
|
17
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Handle parent checkbox change
|
|
50
|
+
* - Updates parent menu
|
|
51
|
+
* - Cascades selection to all children
|
|
52
|
+
*
|
|
53
|
+
* @param {boolean} checked
|
|
54
|
+
*/
|
|
55
|
+
|
|
18
56
|
const onParentChange = (checked) => {
|
|
19
57
|
toggleMenu && toggleMenu(menu.id, checked);
|
|
20
58
|
children.forEach((c) => toggleMenuRecursive(c, checked));
|
|
21
59
|
};
|
|
22
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Leaf menu (no sub-menus)
|
|
63
|
+
*/
|
|
64
|
+
|
|
23
65
|
if (children.length === 0) {
|
|
24
66
|
return (
|
|
25
67
|
<div
|
|
26
68
|
key={menu.id}
|
|
27
69
|
style={{
|
|
28
|
-
border:
|
|
29
|
-
padding:
|
|
70
|
+
border: '1px solid rgba(198, 195, 195, 0.85)',
|
|
71
|
+
padding: '12px 16px',
|
|
30
72
|
marginBottom: 6,
|
|
31
|
-
background:
|
|
32
|
-
display:
|
|
33
|
-
alignItems:
|
|
73
|
+
background: '#fff',
|
|
74
|
+
display: 'flex',
|
|
75
|
+
alignItems: 'center',
|
|
34
76
|
gap: 8,
|
|
35
77
|
}}
|
|
36
78
|
>
|
|
37
|
-
{showCheckbox && (
|
|
38
|
-
<Checkbox
|
|
39
|
-
checked={selectedMenus.includes(menu.id)}
|
|
40
|
-
onChange={(e) => onParentChange(e.target.checked)}
|
|
41
|
-
/>
|
|
42
|
-
)}
|
|
79
|
+
{showCheckbox && <Checkbox checked={selectedMenus.includes(menu.id)} onChange={(e) => onParentChange(e.target.checked)} />}
|
|
43
80
|
<span>{menu.title || menu.caption}</span>
|
|
44
81
|
</div>
|
|
45
82
|
);
|
|
46
83
|
}
|
|
47
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Parent menu (with sub-menus)
|
|
87
|
+
*/
|
|
88
|
+
|
|
48
89
|
return (
|
|
49
90
|
<Collapse key={menu.id} style={{ marginBottom: 6 }}>
|
|
50
91
|
<Panel
|
|
51
92
|
key={menu.id}
|
|
52
93
|
header={
|
|
53
|
-
<div style={{ display:
|
|
94
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
54
95
|
{showCheckbox && (
|
|
55
96
|
<Checkbox
|
|
56
97
|
checked={children.every((c) => selectedMenus.includes(c.id))}
|
|
57
|
-
indeterminate={
|
|
58
|
-
children.some((c) => selectedMenus.includes(c.id)) &&
|
|
59
|
-
!children.every((c) => selectedMenus.includes(c.id))
|
|
60
|
-
}
|
|
98
|
+
indeterminate={children.some((c) => selectedMenus.includes(c.id)) && !children.every((c) => selectedMenus.includes(c.id))}
|
|
61
99
|
onChange={(e) => onParentChange(e.target.checked)}
|
|
62
100
|
/>
|
|
63
101
|
)}
|
|
@@ -65,9 +103,7 @@ export const MenuTree = ({ menus, selectedMenus = [], toggleMenu, parentId = nul
|
|
|
65
103
|
</div>
|
|
66
104
|
}
|
|
67
105
|
>
|
|
68
|
-
<div style={{ paddingLeft: 20 }}>
|
|
69
|
-
{renderTree(children, menu.id)}
|
|
70
|
-
</div>
|
|
106
|
+
<div style={{ paddingLeft: 20 }}>{renderTree(children, menu.id)}</div>
|
|
71
107
|
</Panel>
|
|
72
108
|
</Collapse>
|
|
73
109
|
);
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AssignRole Component
|
|
3
|
+
*
|
|
4
|
+
* @description
|
|
5
|
+
* Allows administrators to:
|
|
6
|
+
* - View user details
|
|
7
|
+
* - Assign one or more roles to a user
|
|
8
|
+
* - View menus associated with a selected role (read-only)
|
|
9
|
+
*
|
|
10
|
+
* UI Layout:
|
|
11
|
+
* - Left Panel: User info + role list with search and checkboxes
|
|
12
|
+
* - Right Panel: Menu tree preview for the selected role
|
|
13
|
+
*
|
|
14
|
+
* @returns {JSX.Element}
|
|
15
|
+
*/
|
|
1
16
|
import React, { useMemo, useState, useEffect } from 'react';
|
|
2
17
|
import { Checkbox, Input, Typography, Empty, Avatar, List, Card, message, Skeleton } from 'antd';
|
|
3
18
|
|
|
@@ -13,24 +28,44 @@ const { Text } = Typography;
|
|
|
13
28
|
const { Search } = Input;
|
|
14
29
|
|
|
15
30
|
export default function AssignRole() {
|
|
31
|
+
/**
|
|
32
|
+
* User ID from route params
|
|
33
|
+
* @type {{ id: string }}
|
|
34
|
+
*/
|
|
16
35
|
const { id } = useParams();
|
|
17
|
-
|
|
36
|
+
// for user
|
|
18
37
|
const [user, setUser] = useState(null);
|
|
38
|
+
// for roles
|
|
19
39
|
const [roles, setRoles] = useState([]);
|
|
40
|
+
// for active roles
|
|
20
41
|
const [activeRole, setActiveRole] = useState(null);
|
|
21
42
|
const [modules, setModules] = useState([]);
|
|
43
|
+
// loading
|
|
22
44
|
const [selectedRoles, setSelectedRoles] = useState([]);
|
|
23
|
-
|
|
24
|
-
|
|
45
|
+
const [loadingUser, setLoadingUser] = useState(false);
|
|
46
|
+
const [loadingRoles, setLoadingRoles] = useState(false);
|
|
25
47
|
const [loadingMenus, setLoadingMenus] = useState(false);
|
|
48
|
+
|
|
26
49
|
const [selectedMenus, setSelectedMenus] = useState([]);
|
|
27
50
|
const [search, setSearch] = useState('');
|
|
28
51
|
|
|
29
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Load user details and assigned roles when user ID changes
|
|
54
|
+
*/
|
|
30
55
|
useEffect(() => {
|
|
31
56
|
if (id) loadUser();
|
|
32
57
|
}, [id]);
|
|
33
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Fetch user details and assigned roles
|
|
61
|
+
*
|
|
62
|
+
* - Extracts user info from first record
|
|
63
|
+
* - Deduplicates role IDs
|
|
64
|
+
*
|
|
65
|
+
* @async
|
|
66
|
+
* @returns {Promise<void>}
|
|
67
|
+
*/
|
|
68
|
+
|
|
34
69
|
const loadUser = async () => {
|
|
35
70
|
setLoadingUser(true);
|
|
36
71
|
try {
|
|
@@ -43,21 +78,15 @@ export default function AssignRole() {
|
|
|
43
78
|
return;
|
|
44
79
|
}
|
|
45
80
|
|
|
46
|
-
//
|
|
81
|
+
// Extract user from first record
|
|
47
82
|
const userInfo = list[0].user;
|
|
48
83
|
setUser({
|
|
49
84
|
id: userInfo.id,
|
|
50
85
|
name: userInfo.name,
|
|
51
86
|
});
|
|
52
87
|
|
|
53
|
-
//
|
|
54
|
-
const roleIds = [
|
|
55
|
-
...new Set(
|
|
56
|
-
list
|
|
57
|
-
.filter((item) => item.role_id) // ignore null role_id
|
|
58
|
-
.map((item) => Number(item.role_id))
|
|
59
|
-
),
|
|
60
|
-
];
|
|
88
|
+
// Extract VALID role IDs (ignore nulls & duplicates)
|
|
89
|
+
const roleIds = [...new Set(list.filter((item) => item.role_id).map((item) => Number(item.role_id)))];
|
|
61
90
|
|
|
62
91
|
setSelectedRoles(roleIds);
|
|
63
92
|
} catch (e) {
|
|
@@ -68,11 +97,20 @@ export default function AssignRole() {
|
|
|
68
97
|
}
|
|
69
98
|
};
|
|
70
99
|
|
|
71
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Load active roles on component mount
|
|
102
|
+
*/
|
|
72
103
|
useEffect(() => {
|
|
73
104
|
getRoles();
|
|
74
105
|
}, []);
|
|
75
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Fetch all active roles
|
|
109
|
+
*
|
|
110
|
+
* @async
|
|
111
|
+
* @returns {Promise<void>}
|
|
112
|
+
*/
|
|
113
|
+
|
|
76
114
|
const getRoles = async () => {
|
|
77
115
|
setLoadingRoles(true);
|
|
78
116
|
try {
|
|
@@ -87,6 +125,14 @@ export default function AssignRole() {
|
|
|
87
125
|
}
|
|
88
126
|
};
|
|
89
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Recursively filter menus by allowed menu IDs
|
|
130
|
+
*
|
|
131
|
+
* @param {Array<Object>} menus - Full menu tree
|
|
132
|
+
* @param {Array<number>} allowedIds - Menu IDs assigned to role
|
|
133
|
+
* @returns {Array<Object>}
|
|
134
|
+
*/
|
|
135
|
+
|
|
90
136
|
const filterAndSortMenus = (menus, allowedIds) => {
|
|
91
137
|
return menus
|
|
92
138
|
.filter((m) => allowedIds.includes(m.id))
|
|
@@ -96,7 +142,14 @@ export default function AssignRole() {
|
|
|
96
142
|
}));
|
|
97
143
|
};
|
|
98
144
|
|
|
99
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* Load menus for a selected role
|
|
147
|
+
*
|
|
148
|
+
* @param {Object} role - Selected role object
|
|
149
|
+
* @async
|
|
150
|
+
* @returns {Promise<void>}
|
|
151
|
+
*/
|
|
152
|
+
|
|
100
153
|
const loadRoleMenus = async (role) => {
|
|
101
154
|
setActiveRole(role);
|
|
102
155
|
setSelectedMenus(role.menu_ids || []);
|
|
@@ -114,7 +167,12 @@ export default function AssignRole() {
|
|
|
114
167
|
}
|
|
115
168
|
};
|
|
116
169
|
|
|
117
|
-
/**
|
|
170
|
+
/**
|
|
171
|
+
* Toggle menu selection (currently read-only usage)
|
|
172
|
+
*
|
|
173
|
+
* @param {number} menuId
|
|
174
|
+
* @param {boolean} checked
|
|
175
|
+
*/
|
|
118
176
|
const toggleMenu = (menuId, checked) => {
|
|
119
177
|
setSelectedMenus((prev) => (checked ? [...prev, menuId] : prev.filter((id) => id !== menuId)));
|
|
120
178
|
};
|
|
@@ -125,12 +183,24 @@ export default function AssignRole() {
|
|
|
125
183
|
return roles.filter((r) => r.name.toLowerCase().includes(search.toLowerCase()));
|
|
126
184
|
}, [roles, search]);
|
|
127
185
|
|
|
128
|
-
/**
|
|
186
|
+
/**
|
|
187
|
+
* Toggle role selection
|
|
188
|
+
*
|
|
189
|
+
* @param {number} roleId
|
|
190
|
+
* @param {boolean} checked
|
|
191
|
+
*/
|
|
192
|
+
|
|
129
193
|
const toggleRole = (roleId, checked) => {
|
|
130
194
|
const next = checked ? [...selectedRoles, roleId] : selectedRoles.filter((v) => v !== roleId);
|
|
131
195
|
setSelectedRoles(next);
|
|
132
196
|
};
|
|
133
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Save selected roles for the user
|
|
200
|
+
*
|
|
201
|
+
* @async
|
|
202
|
+
* @returns {Promise<void>}
|
|
203
|
+
*/
|
|
134
204
|
const handleSaveUserRole = async () => {
|
|
135
205
|
if (!id || !selectedRoles.length) {
|
|
136
206
|
message.warning('User or roles missing');
|
|
@@ -87,4 +87,31 @@
|
|
|
87
87
|
gap: 8px;
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
|
|
91
|
+
/* ===============================
|
|
92
|
+
📱 iPad Mini (481px – 768px)
|
|
93
|
+
=============================== */
|
|
94
|
+
@media (max-width: 768px) {
|
|
95
|
+
.assign-role {
|
|
96
|
+
flex-direction: column;
|
|
97
|
+
gap: 12px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.assign-role .left-panel {
|
|
101
|
+
width: 100%;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.assign-role .right-panel {
|
|
105
|
+
width: 100%;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* Better spacing for tablets */
|
|
109
|
+
.assign-role .right-panel {
|
|
110
|
+
padding: 14px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.assign-role .footer-actions {
|
|
114
|
+
justify-content: flex-end;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
90
117
|
}
|