ui-soxo-bootstrap-core 2.5.6 → 2.5.7

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,248 +1,312 @@
1
1
  import React, { useState, useEffect, useContext } from 'react';
2
- import { useLocation,useParams } from 'react-router-dom';
3
- import { Skeleton, Typography, message, Form, Input, Select, Collapse, Checkbox } from 'antd';
4
- import { Table, Card, Button, JSONInput, Switch, GlobalContext } from './../../../../lib';
5
-
6
- import { ModelsAPI, PagesAPI, RolesAPI,MenusAPI } from '../../..';
2
+ import { useLocation, useParams } from 'react-router-dom';
3
+ import { Skeleton, Typography, message, Form, Input, Collapse, Checkbox } from 'antd';
4
+ import { GlobalContext } from './../../../../lib';
5
+ import { Button } from './../../../../lib';
6
+ import { ModelsAPI, PagesAPI, RolesAPI, MenusAPI } from '../../..';
7
+ import './role-add.scss';
7
8
 
8
9
  const { Title } = Typography;
9
- const { Option } = Select;
10
10
  const { Panel } = Collapse;
11
11
 
12
- const RoleAdd = ({ model, callback, edit, history, formContent, match, additional_queries, ...props }) => {
13
-
14
- let mode = 'Add';
15
- if (formContent.id) mode = 'Edit';
16
- else if (formContent.copy) mode = 'copy';
17
-
18
- // Parse attributes safely
19
- if (formContent.attributes) {
20
- if (typeof formContent.attributes === 'string' && formContent.attributes !== '') {
21
- formContent.attributes = JSON.parse(formContent.attributes);
22
- } else {
23
- formContent.attributes = {};
24
- }
25
- } else {
12
+ const RoleAdd = ({ model, callback, edit, formContent = {}, match, additional_queries = [] }) => {
13
+ let mode = 'Add';
14
+ if (formContent.id) mode = 'Edit';
15
+ else if (formContent.copy) mode = 'copy';
16
+
17
+ // Parse attributes safely
18
+ if (formContent.attributes) {
19
+ if (typeof formContent.attributes === 'string' && formContent.attributes !== '') {
20
+ try {
21
+ formContent.attributes = JSON.parse(formContent.attributes);
22
+ } catch {
26
23
  formContent.attributes = {};
24
+ }
25
+ } else {
26
+ formContent.attributes = {};
27
+ }
28
+ } else {
29
+ formContent.attributes = {};
30
+ }
31
+
32
+ const [loading, setLoading] = useState(true);
33
+ const [form] = Form.useForm();
34
+ const [pages, setPages] = useState([]);
35
+ const [models, setModels] = useState([]);
36
+ const [menuList, setMenuList] = useState([]);
37
+ const [showMenus, setShowMenus] = useState(false);
38
+ // for deselected menu for editing
39
+ const [originalMenus, setOriginalMenus] = useState([]);
40
+
41
+ // Selected menus (array of IDs)
42
+ const [selectedMenus, setSelectedMenus] = useState([]);
43
+
44
+ const location = useLocation();
45
+ const query = new URLSearchParams(location.search);
46
+ let step = parseInt(query.get('step')) || 1;
47
+
48
+ const { user = {} } = useContext(GlobalContext);
49
+
50
+ // On component mount
51
+ useEffect(() => {
52
+ getPages();
53
+ getModels();
54
+ loadMenus();
55
+ setShowMenus(true);
56
+
57
+ // Preselect menus when editing
58
+ if (formContent && formContent.menu_ids) {
59
+ let menus = formContent.menu_ids;
60
+
61
+ setSelectedMenus(menus);
62
+ // keep original copy for deselect comparison
63
+ setOriginalMenus(menus);
27
64
  }
28
65
 
29
- const [loading, setLoading] = useState(true);
30
- const [form] = Form.useForm();
31
- const [pages, setPages] = useState([]);
32
- const [models, setModels] = useState([]);
33
- const [body, setBody] = useState(formContent);
34
-
35
- const [showMenus, setShowMenus] = useState(false);
36
- const [menuList, setMenuList] = useState([]);
37
-
38
- const { params } = match;
39
- const { dispatch, user = {} } = useContext(GlobalContext);
40
-
41
- const location = useLocation();
42
- const query = new URLSearchParams(location.search);
43
-
44
- let step = parseInt(query.get('step')) || 1;
45
-
46
- match = useParams();
47
- const id = match.id;
48
-
49
- useEffect(() => {
50
- getPages();
51
- getModels();
52
- loadMenus();
53
-
54
- setLoading(false);
55
- }, []);
56
- useEffect(() => {
57
-
58
- loadMenus();
59
- setShowMenus(true)
60
-
61
- // setLoading(false);
62
- }, [id]);
63
-
64
- // Load pages
65
- const getPages = () => {
66
- PagesAPI.getPages().then((result) => {
67
- setPages(result.result);
68
- });
69
- };
70
-
71
- // Load models
72
- const getModels = () => {
73
- ModelsAPI.get().then((result) => {
74
- console.log(result);
75
- setModels(result.result);
76
- });
66
+ setLoading(false);
67
+ }, [formContent]);
68
+
69
+ // Load pages
70
+ const getPages = () => {
71
+ PagesAPI.getPages().then((res) => setPages(res.result || []));
72
+ };
73
+
74
+ // Load models
75
+ const getModels = () => {
76
+ ModelsAPI.get().then((res) => setModels(res.result || []));
77
+ };
78
+
79
+ // Load top-level menus
80
+ const loadMenus = () => {
81
+ MenusAPI.getCoreMenuLists()
82
+ .then((res) => setMenuList(res.result || []))
83
+ .catch(console.error);
84
+ };
85
+
86
+ // Toggle menu selection
87
+ const toggleMenu = (id, checked) => {
88
+ setSelectedMenus((prev) => (checked ? [...prev, id] : prev.filter((m) => m !== id)));
89
+ };
90
+
91
+ // Submit handler
92
+ // const onSubmit = (values) => {
93
+ // setLoading(true);
94
+
95
+ // const payload = {
96
+ // ...values,
97
+ // menu_ids
98
+ // : selectedMenus,
99
+ // attributes: JSON.stringify(values.attributes || {}),
100
+ // };
101
+
102
+ // if (formContent.id) {
103
+ // RolesAPI.updateRole({ id: formContent.id, formBody: payload })
104
+ // .then(() => {
105
+ // message.success('Role Updated');
106
+ // setLoading(false);
107
+ // callback();
108
+ // })
109
+ // .catch(() => setLoading(false));
110
+ // } else {
111
+ // additional_queries.forEach(({ field, value }) => {
112
+ // payload[field] = value;
113
+ // });
114
+ // RolesAPI.createRole(payload)
115
+ // .then(() => {
116
+ // message.success('Role Added');
117
+ // setLoading(false);
118
+ // callback();
119
+ // })
120
+ // .catch(() => setLoading(false));
121
+ // }
122
+ // };
123
+ const onSubmit = async (values) => {
124
+ setLoading(true);
125
+
126
+ // Find menus that were originally selected but now deselected
127
+ const deselectedMenus = originalMenus.filter((id) => !selectedMenus.includes(id));
128
+
129
+ const payload = {
130
+ ...values,
131
+ menu_ids: selectedMenus,
132
+ deselect_array: deselectedMenus, // add deselected menus when edit time
133
+ attributes: JSON.stringify(values.attributes || {}),
77
134
  };
78
135
 
79
- // Load menus dynamically
80
- const loadMenus = () => {
81
- MenusAPI.get({
82
- queries: [
83
- { field: 'step', value: 1 },
84
- { field: 'header_id', value: null }
85
- ]
86
- })
87
- .then(res => setMenuList(res.result || []))
88
- .catch(console.error);
89
- };
90
-
91
-
92
-
93
- // Submit
94
- const onSubmit = (values) => {
95
- setLoading(true);
96
- let id = formContent.id;
97
-
98
- if (values.attributes && typeof values === 'object') {
99
- values = {
100
- ...values,
101
- attributes: JSON.stringify(values.attributes)
102
- };
103
- }
104
-
105
- if (id) {
106
- RolesAPI.updateRole({ id, formBody: values }).then(() => {
107
- message.success('Role Updated');
108
- setLoading(false);
109
- callback();
110
- });
111
- } else {
112
- additional_queries.forEach(({ field, value }) => {
113
- values[field] = value;
114
- });
115
-
116
- RolesAPI.createRole(values).then(() => {
117
- message.success('Role Added');
118
- setLoading(false);
119
- callback();
120
- });
121
- }
122
- };
136
+ // include id ONLY for edit
137
+ if (formContent?.id) {
138
+ payload.id = formContent.id;
139
+ }
123
140
 
124
- return (
125
- <section className="collection-add">
126
-
127
- {loading ? (
128
- <Skeleton />
129
- ) : (
130
- <Form initialValues={{ ...body }} form={form} layout="vertical" onFinish={onSubmit}>
131
-
132
- {/* Role Name */}
133
- <Form.Item name="name" label="Enter Role Name" required>
134
- <Input placeholder="Enter name" />
135
- </Form.Item>
136
- {/* Name */}
137
- {/* <Form.Item name={"identifier"} label="Identifier" required>
138
- <Input placeholder="Enter identifier" />
139
- </Form.Item> */}
140
- {/* Name Ends */}
141
-
142
- {/* Path */}
143
- {/* <Form.Item name="weight" label="Weight" required>
144
- <Input placeholder="Enter weight" />
145
- </Form.Item> */}
146
- {/* Path Ends */}
147
-
148
- {/* Description */}
149
- <Form.Item name="description" label="Enter Description" required>
150
- <Input placeholder="Enter description" />
151
- </Form.Item>
152
-
153
- {/* MENUS COLLAPSE */}
154
- {showMenus && menuList.length > 0 && (
155
- <div style={{ marginTop: 30 }}>
156
- <Title level={5} style={{ marginBottom: 16 }}>Menu List</Title>
157
- <p style={{ marginTop: -10, marginBottom: 20, color: "#999" }}>
158
- Choose menus and set permissions
159
- </p>
160
-
161
- <Collapse expandIconPosition="left">
162
- {menuList.map(item => (
163
- <Panel
164
- key={item.id}
165
- header={
166
- <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
167
- <Checkbox />
168
- <span>{item.title || item.caption}</span>
169
- </div>
170
- }
171
- >
172
- {/* Load submenus recursively */}
173
- <NestedMenu parentId={item.id} step={step + 1} />
174
- </Panel>
175
- ))}
176
- </Collapse>
177
- </div>
178
- )}
179
- {/* <Button type="primary" onClick={loadMenus()}>
180
- Get Menus
181
- </Button> */}
182
-
183
-
184
- {/* Submit Button */}
185
- <Form.Item>
186
- <Button loading={loading} htmlType="submit" type="primary">
187
- Save
188
- </Button>
189
- </Form.Item>
190
-
191
- </Form>
192
- )}
193
-
194
- </section>
195
- );
141
+ // include additional queries for both cases
142
+ additional_queries.forEach(({ field, value }) => {
143
+ payload[field] = value;
144
+ });
145
+
146
+ try {
147
+ await RolesAPI.createRole(payload); // single API
148
+ message.success(formContent?.id ? 'Role Updated' : 'Role Added');
149
+ callback();
150
+ } catch (err) {
151
+ // message.error('Something went wrong');
152
+ } finally {
153
+ setLoading(false);
154
+ }
155
+ };
156
+
157
+ return (
158
+ <section className="collection-add">
159
+ {loading ? (
160
+ <Skeleton />
161
+ ) : (
162
+ <Form initialValues={{ ...formContent }} form={form} layout="vertical" onFinish={onSubmit}>
163
+ {/* Role Name */}
164
+ <Form.Item name="name" label="Enter Role Name" rules={[{ required: true, message: 'Role name is required' }]}>
165
+ <Input placeholder="Enter name" />
166
+ </Form.Item>
167
+
168
+ {/* Description */}
169
+ <Form.Item
170
+ name="description"
171
+ label="Enter Description"
172
+ rules={[
173
+ { required: true, message: 'Description is required' },
174
+ { max: 250, message: 'Description cannot exceed 255 characters' },
175
+ ]}
176
+ >
177
+ <Input placeholder="Enter description" />
178
+ </Form.Item>
179
+
180
+ {/* MENU TREE */}
181
+ {showMenus && menuList.length > 0 && (
182
+ <div style={{ marginTop: 30 }}>
183
+ <Title level={5}>Menu List</Title>
184
+ <p style={{ color: '#999' }}>Choose menus and set permissions</p>
185
+
186
+ <MenuTree menus={menuList} selectedMenus={selectedMenus} toggleMenu={toggleMenu} />
187
+ </div>
188
+ )}
189
+
190
+ {/* Submit Button */}
191
+ <Form.Item style={{ marginTop: 20 }}>
192
+ <Button loading={loading} htmlType="submit" type="primary">
193
+ Save
194
+ </Button>
195
+ </Form.Item>
196
+ </Form>
197
+ )}
198
+ </section>
199
+ );
196
200
  };
197
201
 
198
202
  export default RoleAdd;
199
203
 
200
-
201
204
  // ------------------------
202
205
  // Recursive Nested Menu Component
203
206
  // ------------------------
204
- const NestedMenu = ({ parentId, step, api, additional_queries }) => {
205
- const [items, setItems] = useState([]);
206
- const [loading, setLoading] = useState(true);
207
-
208
- useEffect(() => {
209
- MenusAPI.get({
210
- queries: [
211
- // ...additional_queries,
212
- { field: 'header_id', value: parentId },
213
- { field: 'step', value: step }
214
- ],
215
- })
216
- .then(res => {
217
- setItems(res.result || []);
218
- setLoading(false);
219
- })
220
- .catch(() => setLoading(false));
221
- }, [parentId]);
222
-
223
- if (loading) return <Skeleton active />;
224
- if (!items.length) return null;
225
-
226
- return (
227
- <Collapse ghost style={{ marginLeft: 20 }}>
228
- {items.map(menu => (
229
- <Panel
230
- key={menu.id}
231
- header={
232
- <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
233
- <Checkbox />
234
- <span>{menu.title || menu.caption}</span>
235
- </div>
236
- }
207
+ // ------------------------
208
+ // Recursive Nested Menu Component
209
+ // ------------------------
210
+ const MenuTree = ({ menus, selectedMenus, toggleMenu, parentId = null }) => {
211
+ // Helper: check if parent should be checked
212
+ const isParentChecked = (menu) => {
213
+ if (!menu.sub_menus || menu.sub_menus.length === 0) {
214
+ return selectedMenus.includes(menu.id);
215
+ }
216
+ const allChildIds = menu.sub_menus.map((c) => c.id);
217
+ return allChildIds.every((id) => selectedMenus.includes(id));
218
+ };
219
+
220
+ // Helper: check if parent is indeterminate
221
+ const isParentIndeterminate = (menu) => {
222
+ if (!menu.sub_menus || menu.sub_menus.length === 0) return false;
223
+ const allChildIds = menu.sub_menus.map((c) => c.id);
224
+ const checkedCount = allChildIds.filter((id) => selectedMenus.includes(id)).length;
225
+ return checkedCount > 0 && checkedCount < allChildIds.length;
226
+ };
227
+
228
+ return (
229
+ <>
230
+ {menus.map((menu) => {
231
+ const children = menu.sub_menus || [];
232
+ const parentChecked = isParentChecked(menu);
233
+ const parentIndeterminate = isParentIndeterminate(menu);
234
+
235
+ const onParentChange = (checked) => {
236
+ toggleMenu(menu.id, checked);
237
+ // toggle children recursively
238
+ children.forEach((c) => toggleMenuRecursive(c, checked));
239
+ };
240
+
241
+ const toggleMenuRecursive = (menu, checked) => {
242
+ toggleMenu(menu.id, checked);
243
+ if (menu.sub_menus && menu.sub_menus.length > 0) {
244
+ menu.sub_menus.forEach((c) => toggleMenuRecursive(c, checked));
245
+ }
246
+ };
247
+
248
+ if (children.length === 0) {
249
+ return (
250
+ <div
251
+ key={menu.id}
252
+ style={{
253
+ border: '1px solid rgba(198, 195, 195, 0.85)',
254
+ // borderRadius: 6,
255
+ padding: '12px 16px',
256
+ marginBottom: 6,
257
+ background: '#fff',
258
+ display: 'flex',
259
+ alignItems: 'center',
260
+ gap: 8,
261
+ }}
262
+ >
263
+ <Checkbox
264
+ checked={selectedMenus.includes(menu.id)}
265
+ onChange={(e) => {
266
+ const checked = e.target.checked;
267
+
268
+ toggleMenu(menu.id, checked);
269
+
270
+ // ✅ FORCE parent selection
271
+ if (checked && parentId) {
272
+ toggleMenu(parentId, true);
273
+ }
274
+ }}
275
+ />
276
+ <span>{menu.title || menu.caption}</span>
277
+ </div>
278
+ );
279
+ }
280
+
281
+ return (
282
+ <Collapse
283
+ key={menu.id}
284
+ style={{ marginBottom: 6 }}
285
+ // defaultActiveKey={[menu.id]}
286
+ >
287
+ <Panel
288
+ key={menu.id}
289
+ header={
290
+ <div
291
+ style={{
292
+ display: 'flex',
293
+ alignItems: 'center',
294
+ gap: 8,
295
+ }}
296
+ onClick={(e) => e.stopPropagation()}
237
297
  >
238
- <NestedMenu
239
- parentId={menu.id}
240
- step={step + 1}
241
- api={api}
242
- additional_queries={additional_queries}
243
- />
244
- </Panel>
245
- ))}
246
- </Collapse>
247
- );
298
+ <Checkbox checked={parentChecked} indeterminate={parentIndeterminate} onChange={(e) => onParentChange(e.target.checked)} />
299
+ <span>{menu.title || menu.caption}</span>
300
+ </div>
301
+ }
302
+ >
303
+ <div style={{ paddingLeft: 20 }}>
304
+ <MenuTree menus={children} selectedMenus={selectedMenus} toggleMenu={toggleMenu} parentId={menu.id} />
305
+ </div>
306
+ </Panel>
307
+ </Collapse>
308
+ );
309
+ })}
310
+ </>
311
+ );
248
312
  };
@@ -0,0 +1,4 @@
1
+ .ant-checkbox-indeterminate .ant-checkbox-inner::after {
2
+ height: 2.5px;
3
+ opacity: 0.85;
4
+ }