ui-soxo-bootstrap-core 2.4.24 → 2.4.25-dev.11

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.
Files changed (37) hide show
  1. package/.github/workflows/npm-publish.yml +37 -15
  2. package/core/components/extra-info/extra-info-details.js +109 -126
  3. package/core/components/landing-api/landing-api.js +22 -30
  4. package/core/lib/Store.js +20 -18
  5. package/core/lib/components/index.js +4 -1
  6. package/core/lib/components/sidemenu/sidemenu.js +153 -256
  7. package/core/lib/components/sidemenu/sidemenu.scss +39 -26
  8. package/core/lib/elements/basic/dragabble-wrapper/draggable-wrapper.js +119 -42
  9. package/core/lib/elements/basic/rangepicker/rangepicker.js +118 -29
  10. package/core/lib/elements/basic/switch/switch.js +35 -25
  11. package/core/lib/hooks/index.js +2 -12
  12. package/core/lib/hooks/use-otp-timer.js +99 -0
  13. package/core/lib/pages/login/login.js +255 -139
  14. package/core/lib/pages/login/login.scss +140 -32
  15. package/core/models/dashboard/dashboard.js +14 -0
  16. package/core/models/doctor/components/doctor-add/doctor-add.js +403 -0
  17. package/core/models/doctor/components/doctor-add/doctor-add.scss +32 -0
  18. package/core/models/menus/components/menu-add/menu-add.js +220 -267
  19. package/core/models/menus/components/menu-lists/menu-lists.js +366 -211
  20. package/core/models/menus/components/menu-lists/menu-lists.scss +6 -2
  21. package/core/models/menus/menus.js +256 -267
  22. package/core/models/roles/components/role-add/role-add.js +265 -228
  23. package/core/models/roles/components/role-list/role-list.js +326 -348
  24. package/core/models/roles/roles.js +191 -174
  25. package/core/models/staff/components/staff-add/staff-add.js +352 -0
  26. package/core/models/staff/components/staff-add/staff-add.scss +0 -0
  27. package/core/models/users/components/user-add/user-add.js +723 -367
  28. package/core/models/users/components/user-add/user-edit.js +90 -0
  29. package/core/models/users/users.js +318 -165
  30. package/core/modules/index.js +5 -8
  31. package/core/modules/reporting/components/index.js +5 -0
  32. package/core/modules/reporting/components/reporting-dashboard/reporting-dashboard.js +65 -2
  33. package/core/modules/steps/action-buttons.js +79 -0
  34. package/core/modules/steps/steps.js +553 -0
  35. package/core/modules/steps/steps.scss +158 -0
  36. package/core/modules/steps/timeline.js +49 -0
  37. package/package.json +2 -2
@@ -1,14 +1,13 @@
1
- import React, { useState, useRef, useEffect } from 'react';
2
- import { Typography, Modal, Space, Switch, Popconfirm, Skeleton, Input, Drawer, Collapse, Button, Card } from 'antd';
3
- import { ReloadOutlined, OrderedListOutlined, PicCenterOutlined, DeleteOutlined, EditOutlined, CopyOutlined, PlusCircleFilled } from '@ant-design/icons';
4
- import { Link, useParams, useLocation } from 'react-router-dom';
5
-
6
-
1
+ import React, { useState, useCallback, useEffect } from 'react';
2
+ import { Space, Popconfirm, Input, Drawer, Skeleton, Collapse, message } from 'antd';
3
+ import { ReloadOutlined, DeleteOutlined, EditOutlined, PlusCircleFilled, CopyOutlined } from '@ant-design/icons';
4
+ import { Button, Card, Switch, DraggableWrapper } from '../../../../lib';
7
5
  // for draggable menu list import { DndProvider } from "react-dnd";
8
6
  import { DndProvider } from 'react-dnd';
7
+ import { Link, useParams, useLocation } from 'react-router-dom';
9
8
  import { HTML5Backend } from 'react-dnd-html5-backend';
10
- import { DraggableWrapper } from '../../../../lib';
11
- import './menu-lists.scss'
9
+
10
+ import './menu-lists.scss';
12
11
 
13
12
  const { Search } = Input;
14
13
  const { Panel } = Collapse;
@@ -18,34 +17,45 @@ const MenuLists = ({ model, match, relativeAdd = false, additional_queries = [],
18
17
 
19
18
  match = useParams();
20
19
  const id = match.id;
21
- const location = useLocation();
22
-
20
+ // const location = useLocation();
23
21
  const step = parseInt(new URLSearchParams(location.search).get('step')) || 1;
24
22
 
25
23
  const [records, setRecords] = useState([]);
24
+ const [originalRecords, setOriginalRecords] = useState([]);
26
25
  const [loading, setLoading] = useState(false);
27
-
28
26
  const [drawerVisible, setDrawerVisible] = useState(false);
29
-
30
- const [view, setView] = useState(false);
31
-
32
- const [visible, setVisible] = useState(false);
33
-
34
27
  const [single, setSingle] = useState({});
35
28
  const [drawerTitle, setDrawerTitle] = useState('');
36
29
  const [selectedRecord, setSelectedRecord] = useState(null);
37
-
38
30
  const [query, setQuery] = useState('');
31
+ const [dragMode, setDragMode] = useState(false);
32
+ const [orderChanged, setOrderChanged] = useState(false);
39
33
 
40
- // ... existing states
41
- const [dragMode, setDragMode] = useState(true); // NEW STATE
42
-
43
- const toggleDragMode = (checked) => setDragMode(checked);
44
-
34
+ const [nextId, setNextId] = useState(10000);
45
35
 
46
36
  useEffect(() => {
47
37
  loadMenus();
48
38
  }, [id]);
39
+
40
+ const loadMenus = () => {
41
+ setLoading(true);
42
+
43
+ model
44
+ .getCoreMenuLists
45
+ // {
46
+ // queries: [...additional_queries, { field: 'step', value: step }, { field: 'header_id', value: null }],
47
+ // }
48
+ ()
49
+ .then((res) => {
50
+ const sorted = (res.result || []).sort((a, b) => a.order - b.order);
51
+ setRecords(sorted);
52
+ setOriginalRecords(sorted);
53
+ setLoading(false);
54
+ })
55
+ .catch(() => setLoading(false));
56
+ };
57
+
58
+ console.log('record', records);
49
59
  /**
50
60
  *
51
61
  */
@@ -79,7 +89,7 @@ const MenuLists = ({ model, match, relativeAdd = false, additional_queries = [],
79
89
  var result = res.result;
80
90
 
81
91
  if (Array.isArray(result)) {
82
- setRecords(result);
92
+ setRecords(result.sort((a, b) => a.order - b.order));
83
93
  // } else {
84
94
  // setRecords([])
85
95
  }
@@ -93,195 +103,321 @@ const MenuLists = ({ model, match, relativeAdd = false, additional_queries = [],
93
103
  });
94
104
  };
95
105
 
96
- function changeView(result) {
97
- setView(result);
98
- }
99
- const loadMenus = () => {
100
- setLoading(true);
106
+ const toggleDragMode = (checked) => {
107
+ setDragMode(checked);
108
+ if (checked) {
109
+ message.info('Drag mode enabled - Drag items anywhere, including the "Drop here" zones!');
110
+ }
111
+ };
101
112
 
102
- model
103
- .get({
104
- queries: [...additional_queries, { field: 'step', value: step }, { field: 'header_id', value: null }],
105
- })
106
- .then((res) => {
107
- setRecords(res.result || []);
108
- setLoading(false);
109
- })
110
- .catch(() => setLoading(false));
113
+ // Function to find and remove an item from anywhere in the tree
114
+ const findAndRemoveItem = (items, itemId) => {
115
+ let foundItem = null;
116
+
117
+ const removeRecursive = (arr) => {
118
+ for (let i = 0; i < arr.length; i++) {
119
+ if (arr[i].id === itemId) {
120
+ foundItem = { ...arr[i] };
121
+ arr.splice(i, 1);
122
+ return true;
123
+ }
124
+ if (arr[i].sub_menus && arr[i].sub_menus.length > 0) {
125
+ if (removeRecursive(arr[i].sub_menus)) {
126
+ return true;
127
+ }
128
+ }
129
+ }
130
+ return false;
131
+ };
132
+
133
+ const newItems = JSON.parse(JSON.stringify(items));
134
+ removeRecursive(newItems);
135
+ return { newItems, foundItem };
111
136
  };
112
137
 
138
+ // Function to insert item at specific location
139
+ const insertItemAt = (items, item, targetLevel, targetParentId, targetIndex) => {
140
+ const newItems = JSON.parse(JSON.stringify(items));
141
+
142
+ // Update item properties for new location
143
+ const updatedItem = {
144
+ ...item,
145
+ step: targetLevel,
146
+ header_id: targetParentId,
147
+ };
148
+
149
+ if (targetLevel === 1) {
150
+ // Insert at main level
151
+ newItems.splice(targetIndex, 0, updatedItem);
152
+ } else {
153
+ // Insert into nested level
154
+ const insertRecursive = (arr) => {
155
+ for (let i = 0; i < arr.length; i++) {
156
+ if (arr[i].id === targetParentId) {
157
+ if (!arr[i].sub_menus) arr[i].sub_menus = [];
158
+ arr[i].sub_menus.splice(targetIndex, 0, updatedItem);
159
+ return true;
160
+ }
161
+ if (arr[i].sub_menus && arr[i].sub_menus.length > 0) {
162
+ if (insertRecursive(arr[i].sub_menus)) {
163
+ return true;
164
+ }
165
+ }
166
+ }
167
+ return false;
168
+ };
169
+ insertRecursive(newItems);
170
+ }
171
+
172
+ return newItems;
173
+ };
174
+
175
+ const handleCrossLevelMove = useCallback(
176
+ (draggedItem, dropTarget) => {
177
+ const { targetLevel, targetParentId, targetIndex } = dropTarget;
178
+
179
+ // Prevent moving item into itself or its children
180
+ if (draggedItem.id === targetParentId) {
181
+ message.warning('Cannot move item into itself');
182
+ return;
183
+ }
184
+
185
+ // Remove item from original location
186
+ const { newItems: itemsAfterRemove, foundItem } = findAndRemoveItem(records, draggedItem.id);
187
+
188
+ if (!foundItem) {
189
+ message.error('Could not find item to move');
190
+ return;
191
+ }
192
+
193
+ // Insert item at new location
194
+ const finalItems = insertItemAt(itemsAfterRemove, foundItem, targetLevel, targetParentId, targetIndex);
195
+
196
+ setRecords(finalItems);
197
+ setOrderChanged(true);
198
+ message.success(`Moved "${foundItem.name}" to level ${targetLevel}`);
199
+ },
200
+ [records]
201
+ );
202
+
113
203
  const deleteRecord = (rec) => {
114
204
  model.delete(rec).then(loadMenus);
115
205
  };
116
206
 
117
- const filtered = records.filter((r) => r.name?.toUpperCase().includes(query.toUpperCase())); /**
118
- * Function to store search value
119
- */
207
+ const filtered = records.filter((r) => r.name?.toUpperCase().includes(query.toUpperCase()));
208
+
209
+ const visibleItems = dragMode ? records : filtered;
210
+
120
211
  const onSearch = (event) => {
121
- setQuery(event.target.value); // <-- use setQuery
212
+ setQuery(event.target.value);
122
213
  };
123
214
 
124
215
  // ------------------------
125
216
  // Recursive function to build payload
126
217
  // ------------------------
127
- const buildOrderPayload = (menus) => {
128
- return menus.map((menu, index) => {
129
- const payload = {
130
- id: menu.id,
131
- order: index + 1,
132
- };
218
+ const buildUpdatedOrderPayload = (menus, originalMenus) => {
219
+ const result = [];
133
220
 
134
- if (menu.sub_menus && menu.sub_menus.length) {
135
- payload.sub_menus = buildOrderPayload(menu.sub_menus);
221
+ menus.forEach((menu, index) => {
222
+ const original = findMenuById(originalMenus, menu.id);
223
+
224
+ const newOrder = index + 1;
225
+ const oldOrder = original ? original.order : null;
226
+
227
+ const orderChanged = oldOrder !== newOrder;
228
+ const parentChanged = original?.header_id !== menu.header_id;
229
+
230
+ // check sub menus
231
+ let updatedSubMenus = [];
232
+ if (menu.sub_menus?.length) {
233
+ updatedSubMenus = buildUpdatedOrderPayload(menu.sub_menus, originalMenus);
136
234
  }
137
235
 
138
- return payload;
236
+ // include only if changed OR has changed children
237
+ if (orderChanged || parentChanged || updatedSubMenus.length) {
238
+ result.push({
239
+ id: menu.id,
240
+ order: newOrder,
241
+ header_id: menu.header_id,
242
+ name: menu.name,
243
+ ...(updatedSubMenus.length && { sub_menus: updatedSubMenus }),
244
+ });
245
+ }
139
246
  });
247
+
248
+ return result;
249
+ };
250
+
251
+ // ------------------------
252
+ // Find menu by id (recursive)
253
+ // ------------------------
254
+ const findMenuById = (menus, id) => {
255
+ for (const menu of menus || []) {
256
+ if (menu.id === id) return menu;
257
+
258
+ if (menu.sub_menus?.length) {
259
+ const found = findMenuById(menu.sub_menus, id);
260
+ if (found) return found;
261
+ }
262
+ }
263
+ return null;
140
264
  };
141
265
 
142
266
  // ------------------------
143
267
  // Save order function
144
268
  // ------------------------
145
269
  const saveOrder = () => {
146
- const payload = { menus: buildOrderPayload(records) };
270
+ const updatedMenus = buildUpdatedOrderPayload(records, originalRecords);
271
+
272
+ if (!updatedMenus.length) {
273
+ message.info('No order changes to save');
274
+ return;
275
+ }
276
+
277
+ const payload = { menus: updatedMenus };
278
+
147
279
  model
148
- .post('/update-order', payload) // Adjust endpoint to your backend
280
+ .saveOrder({ formBody: payload })
149
281
  .then(() => {
150
- message.success('Order saved successfully');
151
- loadMenus(); // reload menus with new order
282
+ setOrderChanged(false);
283
+ loadMenus(); // refresh from backend
152
284
  })
153
- .catch((err) => {
154
- console.error(err);
155
- message.error('Failed to save order');
156
- });
285
+ .catch(console.error);
157
286
  };
158
287
 
288
+ const movePanel = useCallback(
289
+ (from, to) => {
290
+ if (!dragMode) return;
291
+ if (from === to) return;
159
292
 
293
+ setRecords((prevRecords) => {
294
+ const updated = [...prevRecords];
295
+ const [moved] = updated.splice(from, 1);
296
+ updated.splice(to, 0, moved);
297
+ return updated;
298
+ });
160
299
 
300
+ setOrderChanged(true);
301
+ },
302
+ [dragMode]
303
+ );
161
304
 
162
305
  return (
163
306
  <Card className="generic-list">
164
- <div className="table-header">
165
- <div className="table-title">
307
+ <div style={{ marginBottom: 16 }}>
308
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
166
309
  <Search placeholder="Enter Search Value" allowClear style={{ width: 300, marginBottom: '0px' }} onChange={onSearch} />
167
- {/* <Title level={4}>{model.name}</Title> */}
168
310
 
169
- {/* <p>{loading ? 'Loading records' : `${record.length} records`}</p> */}
311
+ <Space size="small">
312
+ <Button onClick={getRecords} size={'small'} type="default">
313
+ <ReloadOutlined />
314
+ </Button>
315
+
316
+ <Switch checked={dragMode} onChange={toggleDragMode} checkedChildren="Order On" unCheckedChildren="Order Off" />
317
+
318
+ {dragMode && orderChanged && (
319
+ <Button onClick={saveOrder} type="primary" size="small">
320
+ Save Order
321
+ </Button>
322
+ )}
323
+
324
+ <Button
325
+ type="primary"
326
+ size="small"
327
+ onClick={() => {
328
+ setSelectedRecord({ step: 1, header_id: null });
329
+ setDrawerTitle('Create New Menu');
330
+ setDrawerVisible(true);
331
+ }}
332
+ >
333
+ Create New Menu
334
+ </Button>
335
+ </Space>
170
336
  </div>
171
337
 
172
- <div className="table-bar">
173
- {/* Table Filters */}
174
- <div className="table-filters">
175
- <Space direction="vertical" size={12}></Space>
176
- </div>
177
- {/* Table Filters Ends */}
178
-
179
- <div className="table-actions">
180
- <div className="button-container">
181
- <Space size="small">
182
- <Button onClick={getRecords} size={'small'}>
183
- <ReloadOutlined />
184
- </Button>
185
-
186
- {/* <Switch
187
- defaultChecked
188
- onChange={changeView}
189
- checked={view}
190
- checkedChildren={<OrderedListOutlined />}
191
- unCheckedChildren={<PicCenterOutlined />}
192
- /> */}
193
- {/* NEW SWITCH: Drag Mode */}
194
- <Switch
195
- checked={dragMode}
196
- onChange={toggleDragMode}
197
- checkedChildren="Order On"
198
- unCheckedChildren="Order Off"
199
- />
200
-
201
- {dragMode && (
202
- <Button
203
- type="primary"
204
- size="small"
205
- // onClick={saveOrder}
206
- >
207
- Save Order
208
- </Button>
209
- )}
210
-
211
-
212
-
213
- {disableAddModal || !model.ModalAddComponent ? null : (
214
- <Button
215
- type="primary"
216
- size='small'
217
- onClick={() => {
218
- setSelectedRecord({}); // empty record for creation
219
- setDrawerTitle('Create New Menu');
220
- setDrawerVisible(true); // this controls Drawer visibility
221
- }}
222
- >
223
- Create New Menu
224
- </Button>
225
- )}
226
- </Space>
227
- </div>
338
+ {dragMode && (
339
+ <div
340
+ style={{
341
+ padding: '8px 12px',
342
+ backgroundColor: '#e6f7ff',
343
+ borderRadius: 4,
344
+ border: '1px solid #91d5ff',
345
+ fontSize: 13,
346
+ }}
347
+ >
348
+ <strong>💡 Tips:</strong>
349
+ <ul style={{ margin: '4px 0', paddingLeft: 20 }}>
350
+ <li>Drag between items to reorder at the same level</li>
351
+ <li>Drop on the green "↳ Drop here" zone to make it a child of that item</li>
352
+ <li>Works at all levels - unlimited nesting supported!</li>
353
+ </ul>
228
354
  </div>
229
- </div>
355
+ )}
230
356
  </div>
231
-
232
357
  {loading ? (
233
358
  <Skeleton active />
234
359
  ) : (
235
360
  <>
236
361
  <>
237
- {!view ? (
238
- <Card>
239
- <DndProvider backend={HTML5Backend}>
240
- <Collapse accordion className="custom-collapse">
241
- {filtered.map((item, index) => (
242
- <Panel
243
- key={item.id}
244
- className="custom-panel"
245
- header={
246
- <DraggableWrapper
247
- id={item.id}
248
- index={index}
249
- movePanel={(from, to) => {
250
- if (!dragMode) return; // prevent moving if drag mode off
251
- const updated = [...filtered];
252
- const [moved] = updated.splice(from, 1);
253
- updated.splice(to, 0, moved);
254
- setRecords(updated);
255
- }}
256
- title={item.name}
257
- dragEnabled={dragMode} // pass dragMode to wrapper
258
-
259
- />
260
- }
261
- extra={panelActions(item, model, setSelectedRecord, setDrawerTitle, setDrawerVisible, deleteRecord)}
262
- >
362
+ {/* {!view ? ( */}
363
+ <Card>
364
+ <DndProvider backend={HTML5Backend}>
365
+ <Collapse accordion>
366
+ {visibleItems.map((item, index) => (
367
+ <Panel
368
+ key={item.id}
369
+ header={
370
+ <DraggableWrapper
371
+ id={item.id}
372
+ index={index}
373
+ movePanel={movePanel}
374
+ item={item}
375
+ dragEnabled={dragMode}
376
+ level={1}
377
+ parentId={null}
378
+ onCrossLevelMove={handleCrossLevelMove}
379
+ canAcceptChildren={true}
380
+ />
381
+ }
382
+ // only show arrow if sub_menus exist
383
+ showArrow={item.sub_menus && item.sub_menus.length > 0}
384
+ // disable panel
385
+ collapsible={item.sub_menus && item.sub_menus.length > 0 ? 'header' : 'disabled'}
386
+ extra={panelActions(item, model, setSelectedRecord, setDrawerTitle, setDrawerVisible, deleteRecord)}
387
+ >
388
+ {item.sub_menus && item.sub_menus.length > 0 && (
263
389
  <NestedMenu
264
390
  parentId={item.id}
265
- step={step + 1}
391
+ step={item.step + 1}
392
+ items={item.sub_menus || []}
266
393
  model={model}
394
+ dragMode={dragMode}
267
395
  setSelectedRecord={setSelectedRecord}
268
396
  setDrawerTitle={setDrawerTitle}
269
397
  setDrawerVisible={setDrawerVisible}
270
398
  deleteRecord={deleteRecord}
399
+ level={2}
400
+ onCrossLevelMove={handleCrossLevelMove}
401
+ onChange={(subMenus) => {
402
+ const updated = records.map((r) => (r.id === item.id ? { ...r, sub_menus: subMenus } : r));
403
+ setRecords(updated);
404
+ setOrderChanged(true);
405
+ }}
271
406
  />
272
- </Panel>
273
- ))}
274
- </Collapse>
275
- </DndProvider>
276
- </Card>
277
- ) : (
407
+ )}
408
+ </Panel>
409
+ ))}
410
+ </Collapse>
411
+ </DndProvider>
412
+ </Card>
413
+ {/* ) : (
278
414
  <CardList model={model} data={filtered ? filtered : records} />
279
- )}
415
+ )} */}
280
416
  </>
281
417
  </>
282
418
  )}
283
- {/* DRAWER */}
284
- <Drawer title={drawerTitle} open={drawerVisible} width="50%" destroyOnClose onClose={() => setDrawerVisible(false)}>
419
+
420
+ <Drawer title={drawerTitle} open={drawerVisible} width="80%" destroyOnClose onClose={() => setDrawerVisible(false)}>
285
421
  {model.ModalAddComponent && (
286
422
  <model.ModalAddComponent
287
423
  formContent={selectedRecord}
@@ -298,15 +434,8 @@ const MenuLists = ({ model, match, relativeAdd = false, additional_queries = [],
298
434
  </Card>
299
435
  );
300
436
  };
301
- function CardList({ model, data, url, ...props }) {
302
- return data.map((record, index) => {
303
- return <model.Card record={record} model={model} index={index} key={index} {...record} {...props} />;
304
- });
305
- }
306
- export default MenuLists;
307
-
308
437
  /* -----------------------------------------------------------------------
309
- PANEL ACTIONS (NOW FIXED)
438
+ PANEL ACTIONS
310
439
  ------------------------------------------------------------------------ */
311
440
  function panelActions(item, model, setSelectedRecord, setDrawerTitle, setDrawerVisible, deleteRecord) {
312
441
  return (
@@ -317,21 +446,24 @@ function panelActions(item, model, setSelectedRecord, setDrawerTitle, setDrawerV
317
446
  type="dashed"
318
447
  onClick={() => {
319
448
  setSelectedRecord({
320
- header_id: item.id, // parent menu id
449
+ // parent menu id
450
+ header_id: item.id,
321
451
  step: item.step + 1,
322
- id: null, // new record
452
+ // new record
453
+ id: null,
323
454
  });
324
455
  setDrawerTitle(`Add Submenu to "${item.name}"`);
325
456
  setDrawerVisible(true);
326
457
  }}
327
458
  >
328
459
  <PlusCircleFilled />
329
- add submenu
460
+ Add Sub Menu
330
461
  </Button>
331
462
 
332
463
  {model.ModalAddComponent && (
333
464
  <Button
334
465
  size="small"
466
+ type="default"
335
467
  onClick={() => {
336
468
  setSelectedRecord(item);
337
469
  setDrawerTitle('Edit Menu');
@@ -344,6 +476,7 @@ function panelActions(item, model, setSelectedRecord, setDrawerTitle, setDrawerV
344
476
 
345
477
  <Button
346
478
  size="small"
479
+ type="default"
347
480
  onClick={() => {
348
481
  setSelectedRecord({ ...item, id: null, copy: true });
349
482
  setDrawerTitle('Copy Menu');
@@ -354,7 +487,7 @@ function panelActions(item, model, setSelectedRecord, setDrawerTitle, setDrawerV
354
487
  </Button>
355
488
 
356
489
  <Popconfirm title="Are you sure?" onConfirm={() => deleteRecord(item)}>
357
- <Button danger size="small">
490
+ <Button danger size="small" type="default">
358
491
  <DeleteOutlined />
359
492
  </Button>
360
493
  </Popconfirm>
@@ -362,42 +495,50 @@ function panelActions(item, model, setSelectedRecord, setDrawerTitle, setDrawerV
362
495
  );
363
496
  }
364
497
 
365
- // ------------------------
366
- // NestedMenu: Pass sub_menus recursively
367
- // ------------------------
368
- function NestedMenu({ parentId, step, model, setSelectedRecord, setDrawerTitle, setDrawerVisible, deleteRecord }) {
369
- const [items, setItems] = useState([]);
370
- const [loading, setLoading] = useState(true);
498
+ function NestedMenu({
499
+ parentId,
500
+ step,
501
+ items,
502
+ model,
503
+ dragMode,
504
+ setSelectedRecord,
505
+ setDrawerTitle,
506
+ setDrawerVisible,
507
+ deleteRecord,
508
+ level,
509
+ onCrossLevelMove,
510
+ onChange,
511
+ }) {
512
+ // do not render Collapse
513
+ if (!items || items.length === 0) return null;
514
+
515
+ const [localItems, setLocalItems] = useState(items);
371
516
 
372
517
  useEffect(() => {
373
- model
374
- .get({
375
- queries: [
376
- { field: 'header_id', value: parentId },
377
- { field: 'step', value: step },
378
- ],
379
- })
380
- .then((res) => {
381
- const menusWithSub = res.result.map((menu) => ({ ...menu, sub_menus: [] }));
382
- setItems(menusWithSub || []);
383
- setLoading(false);
384
- })
385
- .catch(() => setLoading(false));
386
- }, [parentId]);
518
+ setLocalItems(items);
519
+ }, [items]);
387
520
 
388
- const moveSubMenu = (from, to) => {
389
- const updated = [...items];
390
- const [moved] = updated.splice(from, 1);
391
- updated.splice(to, 0, moved);
392
- setItems(updated);
393
- };
521
+ const moveSubMenu = useCallback(
522
+ (from, to) => {
523
+ if (!dragMode || from === to) return;
524
+
525
+ const updated = [...localItems];
526
+ const [moved] = updated.splice(from, 1);
527
+ updated.splice(to, 0, moved);
394
528
 
395
- if (loading) return <Skeleton active />;
396
- if (!items.length) return <p style={{ paddingLeft: 20 }}>No submenu</p>;
529
+ setLocalItems(updated);
530
+ onChange?.(updated);
531
+ },
532
+ [dragMode, localItems, onChange]
533
+ );
534
+ if (!localItems || localItems.length === 0) {
535
+ // <-- don’t render anything if no submenus
536
+ return null;
537
+ }
397
538
 
398
539
  return (
399
- <Collapse accordion className="nested-collapse">
400
- {items.map((child, index) => (
540
+ <Collapse accordion>
541
+ {localItems.map((child, index) => (
401
542
  <Panel
402
543
  key={child.id}
403
544
  header={
@@ -405,25 +546,39 @@ function NestedMenu({ parentId, step, model, setSelectedRecord, setDrawerTitle,
405
546
  id={child.id}
406
547
  index={index}
407
548
  movePanel={moveSubMenu}
408
- title={child.name}
409
- dragEnabled={true}
549
+ item={child}
550
+ dragEnabled={dragMode}
551
+ level={level}
552
+ parentId={parentId}
553
+ onCrossLevelMove={onCrossLevelMove}
554
+ canAcceptChildren={true}
410
555
  />
411
556
  }
557
+ // only show arrow if sub_menus exist
558
+ showArrow={child.sub_menus && child.sub_menus.length > 0}
412
559
  extra={panelActions(child, model, setSelectedRecord, setDrawerTitle, setDrawerVisible, deleteRecord)}
413
560
  >
414
- {child.hasChildren && (
415
- <NestedMenu
416
- parentId={child.id}
417
- step={step + 1}
418
- model={model}
419
- setSelectedRecord={setSelectedRecord}
420
- setDrawerTitle={setDrawerTitle}
421
- setDrawerVisible={setDrawerVisible}
422
- deleteRecord={deleteRecord}
423
- />
424
- )}
561
+ <NestedMenu
562
+ parentId={child.id}
563
+ step={step + 1}
564
+ items={child.sub_menus || []}
565
+ dragMode={dragMode}
566
+ setSelectedRecord={setSelectedRecord}
567
+ setDrawerTitle={setDrawerTitle}
568
+ setDrawerVisible={setDrawerVisible}
569
+ deleteRecord={deleteRecord}
570
+ level={level + 1}
571
+ onCrossLevelMove={onCrossLevelMove}
572
+ onChange={(submenus) => {
573
+ const updated = localItems.map((item) => (item.id === child.id ? { ...item, sub_menus: submenus } : item));
574
+ setLocalItems(updated);
575
+ onChange?.(updated);
576
+ }}
577
+ />
425
578
  </Panel>
426
579
  ))}
427
580
  </Collapse>
428
581
  );
429
582
  }
583
+
584
+ export default MenuLists;