ui-soxo-bootstrap-core 2.6.40-dev.12 → 2.6.40-dev.16

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.
@@ -385,6 +385,71 @@ export default function SideMenu({ loading, modules = [], callback, appSettings,
385
385
 
386
386
  const rootSubmenuKeys = Menus.screenMenus(modules, 'order').map((m) => m.id || m.path || m.caption);
387
387
 
388
+ /**
389
+ * Recursively renders the menu tree to any depth. Any menu that has visible
390
+ * children is rendered as a <SubMenu>; everything else becomes a clickable
391
+ * leaf <Menu.Item>. This replaces the old hard-coded 3-level rendering so
392
+ * menus nested 4, 5, 6+ levels deep all show up.
393
+ *
394
+ * Keys are kept identical to the previous implementation
395
+ * (SubMenu => id || path || caption, leaf => path || caption) so the
396
+ * openKeys / selectedKeys / onOpenChange logic continues to work.
397
+ */
398
+ const renderMenuTree = (items, { isRoot = false, parentKey } = {}) => {
399
+ const visibleItems = Menus.screenMenus(items, 'order').filter((record) => {
400
+ // Drop entries without a caption — they have no label to render.
401
+ const hasCaption = typeof record.caption === 'string' && record.caption.trim().length > 0;
402
+ if (!hasCaption) return false;
403
+
404
+ // The visibility / icon rule only applies to top-level menus, matching the
405
+ // previous behaviour; nested items just need a caption.
406
+ if (!isRoot) return true;
407
+
408
+ if (record.id) {
409
+ if (record.is_visible) return true;
410
+ return !!record.icon_name;
411
+ }
412
+
413
+ return true;
414
+ });
415
+
416
+ return visibleItems.map((menu, index) => {
417
+ const icon = menu.icon_name || 'fa-solid fas fa-user';
418
+
419
+ // Children that actually have something to show (a caption).
420
+ const children =
421
+ menu && menu.sub_menus
422
+ ? Menus.screenMenus(menu.sub_menus, 'order').filter((s) => typeof s.caption === 'string' && s.caption.trim().length > 0)
423
+ : [];
424
+
425
+ if (children.length) {
426
+ return (
427
+ <SubMenu
428
+ className="popup"
429
+ style={{ color: state.theme.colors.leftSectionColor }}
430
+ key={menu.id || menu.path || menu.caption}
431
+ title={
432
+ <span>
433
+ <CollapsedIconMenu menu={menu} caption={menu.caption} icon={icon} collapsed={collapsed} />
434
+ </span>
435
+ }
436
+ >
437
+ {renderMenuTree(menu.sub_menus, { isRoot: false, parentKey: menu.path || menu.caption })}
438
+ </SubMenu>
439
+ );
440
+ }
441
+
442
+ return (
443
+ <Menu.Item
444
+ key={menu.path || menu.caption}
445
+ onClick={() => onMenuClick({ ...menu, parentKey: parentKey || menu.path || menu.caption, isRoot }, index)}
446
+ >
447
+ <CollapsedIconMenu menu={menu} caption={menu.caption} icon={icon} collapsed={collapsed} />
448
+ </Menu.Item>
449
+ );
450
+ });
451
+ };
452
+
388
453
  return (
389
454
  <div className="sidemenu">
390
455
  {loading ? (
@@ -490,153 +555,7 @@ export default function SideMenu({ loading, modules = [], callback, appSettings,
490
555
  })
491
556
  } */}
492
557
 
493
- {Menus.screenMenus(modules, 'order')
494
-
495
- .filter((record) => {
496
- icon = record;
497
-
498
- // Drop entries without a caption — they have no permission/label
499
- // to render, so showing just an icon is misleading.
500
- const hasCaption = typeof record.caption === 'string' && record.caption.trim().length > 0;
501
- if (!hasCaption) return false;
502
-
503
- if (record.id) {
504
- if (record.is_visible) {
505
- return true;
506
- }
507
-
508
- if (record.icon_name) {
509
- return true;
510
- } else {
511
- return false;
512
- }
513
- } else {
514
- return true;
515
- }
516
- })
517
- .map((menu, index) => {
518
- // return <MenuItem menu={menu} index={index} />
519
-
520
- let sub_menus = menu && menu.sub_menus
521
- ? Menus.screenMenus(menu.sub_menus).filter((s) => typeof s.caption === 'string' && s.caption.trim().length > 0)
522
- : [];
523
-
524
- if (menu && sub_menus && sub_menus.length) {
525
- // let randomIndex = parseInt(Math.random() * 10000000000);
526
-
527
- return (
528
- <SubMenu
529
- className="popup"
530
- style={{ color: state.theme.colors.leftSectionColor }}
531
- // key={`first-level-${randomIndex}-${menu.caption}`}
532
-
533
- key={menu.id || menu.path || menu.caption}
534
- title={
535
- <>
536
- <CollapsedIconMenu
537
- menu={menu}
538
- caption={menu.caption}
539
- icon={menu.icon_name || 'fa-solid fas fa-user'}
540
- collapsed={collapsed}
541
- />
542
- </>
543
- }
544
- >
545
- {sub_menus.map((submenu, innerIndex) => {
546
- // let randomIndex = parseInt(Math.random() * 10000000000);
547
-
548
- let third_menus = submenu && submenu.sub_menus ? Menus.screenMenus(submenu.sub_menus) : [];
549
-
550
- if (third_menus && third_menus.length) {
551
- return (
552
- <SubMenu
553
- className="popup"
554
- // key={`second-level-${randomIndex}-${submenu.id}`}
555
-
556
- key={submenu.id || submenu.path || submenu.caption}
557
- title={
558
- <span>
559
- <CollapsedIconMenu
560
- menu={menu}
561
- caption={submenu.caption}
562
- icon={submenu.icon_name || 'fa-solid fas fa-user'}
563
- collapsed={collapsed}
564
- />
565
- </span>
566
- }
567
- >
568
- {third_menus.map((menu) => {
569
- // let randomIndex = parseInt(Math.random() * 10000000000);
570
-
571
- return (
572
- <Menu.Item
573
- // onClick={() => {
574
- // onMenuClick(menu, index);
575
- // }}
576
- onClick={() => {
577
- onMenuClick({ ...menu, parentKey: submenu.path || submenu.caption }, index);
578
- }}
579
- // key={`second-level-${randomIndex}-${index}`}
580
-
581
- key={menu.path || menu.caption}
582
- >
583
- <CollapsedIconMenu
584
- menu={menu}
585
- caption={menu.caption}
586
- icon={menu.icon_name || 'fa-solid fas fa-user'}
587
- collapsed={collapsed}
588
- />
589
- </Menu.Item>
590
- );
591
- })}
592
- </SubMenu>
593
- );
594
- } else {
595
- // let randomIndex = parseInt(Math.random() * 10000000000);
596
-
597
- return (
598
- <Menu.Item
599
- // onClick={() => {
600
- // onMenuClick(submenu, index);
601
- // }}
602
- onClick={() => {
603
- onMenuClick({ ...submenu, parentKey: menu.path || menu.caption }, index);
604
- }}
605
- // key={`first-level-${randomIndex}-${innerIndex}`}
606
- key={submenu.path || submenu.caption}
607
- >
608
- <CollapsedIconMenu
609
- menu={menu}
610
- caption={submenu.caption}
611
- icon={submenu.icon_name || 'fa-solid fas fa-user'}
612
- collapsed={collapsed}
613
- />
614
- </Menu.Item>
615
- );
616
- }
617
- })}
618
- </SubMenu>
619
- );
620
- } else {
621
- // let randomIndex = parseInt(Math.random() * 10000000000);
622
-
623
- return (
624
- <Menu.Item
625
- // onClick={() => {
626
- // onMenuClick(menu, index);
627
- // }}
628
-
629
- onClick={() => {
630
- onMenuClick({ ...menu, parentKey: menu.path || menu.caption, isRoot: true }, index);
631
- }}
632
- // key={`${menu.id}-${randomIndex}`}
633
- key={menu.path || menu.caption}
634
- >
635
- <CollapsedIconMenu menu={menu} caption={menu.caption} icon={menu.icon_name || 'fa-solid fas fa-user'} collapsed={collapsed} />
636
- </Menu.Item>
637
- );
638
- }
639
- })}
558
+ {renderMenuTree(modules, { isRoot: true })}
640
559
 
641
560
  {loading ? (
642
561
  <div class="skeleton-wrapper"></div>
@@ -116,6 +116,13 @@ const MenuAdd = ({ model, callback, edit, history, formContent, match, additiona
116
116
  values.step = formContent.step || step;
117
117
  }
118
118
 
119
+ // Cleared selects come back as undefined; send null so the backend clears them
120
+ ['model_id', 'page_id', 'header_id'].forEach((field) => {
121
+ if (values[field] === undefined) {
122
+ values[field] = null;
123
+ }
124
+ });
125
+
119
126
  if (values.attributes && typeof values === 'object') {
120
127
  values = {
121
128
  ...values,
@@ -183,7 +190,7 @@ const MenuAdd = ({ model, callback, edit, history, formContent, match, additiona
183
190
  {/* Model */}
184
191
  <Form.Item label="Models" name={'model_id'}>
185
192
  {/* <ReferenceSelect value={model.id} label={model.name} model={Models} /> */}
186
- <Select showSearch style={{ width: '100%' }} placeholder="Select a Page" optionFilterProp="label">
193
+ <Select showSearch style={{ width: '100%' }} placeholder="Select a Page" optionFilterProp="label" allowClear>
187
194
  {models.map((model, key) => (
188
195
  <Option key={key} label={model.name} value={model.id}>
189
196
  {model.name}
@@ -198,7 +205,7 @@ const MenuAdd = ({ model, callback, edit, history, formContent, match, additiona
198
205
  <Form.Item label="Pages" name={'page_id'}>
199
206
  {/* <ReferenceSelect value={pages.id} label={pages.name} model={Pages} /> */}
200
207
 
201
- <Select showSearch optionFilterProp="children" style={{ width: '100%' }}>
208
+ <Select showSearch optionFilterProp="children" style={{ width: '100%' }} allowClear>
202
209
  {pages.map((model, key) => (
203
210
  <Option key={key} value={model.id}>
204
211
  {model.name}
@@ -229,13 +236,33 @@ const MenuAdd = ({ model, callback, edit, history, formContent, match, additiona
229
236
  {/* Pages Ends */}
230
237
 
231
238
  {/* Path */}
232
- <Form.Item name="path" label="Path" rules={[{ required: true, message: 'Path is required' }]}>
239
+ <Form.Item
240
+ name="path"
241
+ label="Path"
242
+ rules={[
243
+ { required: true, message: 'Path is required' },
244
+ {
245
+ pattern: /^\//,
246
+ message: 'Path must start with /',
247
+ },
248
+ ]}
249
+ >
233
250
  <Input placeholder="Enter path" />
234
251
  </Form.Item>
235
252
  {/* Path Ends */}
236
253
 
237
254
  {/* Route */}
238
- <Form.Item name="route" label="Route" rules={[{ required: true, message: 'Route is required' }]}>
255
+ <Form.Item
256
+ name="route"
257
+ label="Route"
258
+ rules={[
259
+ { required: false, message: 'Route is required' },
260
+ {
261
+ pattern: /^\//,
262
+ message: 'Route must start with /',
263
+ },
264
+ ]}
265
+ >
239
266
  <Input placeholder="Enter route" />
240
267
  </Form.Item>
241
268
  {/* Route Ends */}
@@ -30,8 +30,9 @@ const MenuLists = ({ model, match, relativeAdd = false, additional_queries = [],
30
30
  const [query, setQuery] = useState('');
31
31
  const [dragMode, setDragMode] = useState(false);
32
32
  const [orderChanged, setOrderChanged] = useState(false);
33
- // keys of manually-expanded top-level panels (used when NOT searching)
34
- const [manualOpenKeys, setManualOpenKeys] = useState([]);
33
+ // keys of currently-expanded top-level panels. Seeded from the search results
34
+ // when the query changes, but the user can freely toggle panels afterwards.
35
+ const [openKeys, setOpenKeys] = useState([]);
35
36
 
36
37
  const [nextId, setNextId] = useState(10000);
37
38
 
@@ -183,6 +184,16 @@ const MenuLists = ({ model, match, relativeAdd = false, additional_queries = [],
183
184
  return;
184
185
  }
185
186
 
187
+ // Prevent moving a menu into one of its own descendants
188
+ // (e.g. A > B, C — A cannot be dropped under B or C).
189
+ if (targetParentId) {
190
+ const draggedNode = findMenuById(records, draggedItem.id);
191
+ if (draggedNode && findMenuById(draggedNode.sub_menus || [], targetParentId)) {
192
+ message.warning('Cannot move a menu into its own sub menu');
193
+ return;
194
+ }
195
+ }
196
+
186
197
  // Remove item from original location
187
198
  const { newItems: itemsAfterRemove, foundItem } = findAndRemoveItem(records, draggedItem.id);
188
199
 
@@ -196,7 +207,7 @@ const MenuLists = ({ model, match, relativeAdd = false, additional_queries = [],
196
207
 
197
208
  setRecords(finalItems);
198
209
  setOrderChanged(true);
199
- message.success(`Moved "${foundItem.name}" to level ${targetLevel}`);
210
+ message.success(`Moved "${foundItem.caption}" to level ${targetLevel}`);
200
211
  },
201
212
  [records]
202
213
  );
@@ -234,10 +245,13 @@ const MenuLists = ({ model, match, relativeAdd = false, additional_queries = [],
234
245
  const searchActive = !!query;
235
246
  const visibleItems = filterMenus(records, query);
236
247
 
237
- // While searching, derive the open panels synchronously from the visible tree
238
- // (every branch on the path to a match), so the matched sub-menu is revealed.
239
- // When not searching, use the user's manually-expanded panels.
240
- const activeKeys = searchActive ? expandableKeys(visibleItems) : manualOpenKeys;
248
+ // When the query changes, seed the open panels from the search results (every
249
+ // branch on the path to a match) so the matched sub-menu is revealed. After
250
+ // that the user can collapse/expand freely via the Collapse onChange below.
251
+ useEffect(() => {
252
+ setOpenKeys(query ? expandableKeys(filterMenus(records, query)) : []);
253
+ // eslint-disable-next-line react-hooks/exhaustive-deps
254
+ }, [query]);
241
255
 
242
256
  const onSearch = (event) => {
243
257
  setQuery(event.target.value);
@@ -395,8 +409,8 @@ const MenuLists = ({ model, match, relativeAdd = false, additional_queries = [],
395
409
  <DndProvider backend={HTML5Backend}>
396
410
  <Collapse
397
411
  accordion={!searchActive}
398
- activeKey={activeKeys}
399
- onChange={(keys) => setManualOpenKeys(Array.isArray(keys) ? keys : keys ? [keys] : [])}
412
+ activeKey={openKeys}
413
+ onChange={(keys) => setOpenKeys(Array.isArray(keys) ? keys : keys ? [keys] : [])}
400
414
  >
401
415
  {visibleItems && visibleItems.length > 0 ? (
402
416
  visibleItems.map((item, index) => (
@@ -549,13 +563,21 @@ function NestedMenu({
549
563
  onChange,
550
564
  }) {
551
565
  const [localItems, setLocalItems] = useState(items);
552
- // keys of manually-expanded sub-panels (used when NOT searching)
553
- const [manualOpenKeys, setManualOpenKeys] = useState([]);
566
+ // keys of currently-expanded sub-panels. Seeded from the search results when
567
+ // the query changes, but the user can freely toggle panels afterwards.
568
+ const [openKeys, setOpenKeys] = useState([]);
554
569
 
555
570
  useEffect(() => {
556
571
  setLocalItems(items);
557
572
  }, [items]);
558
573
 
574
+ // Seed the open sub-panels from the search path when the query changes; keep
575
+ // the user's toggles otherwise so a search-opened panel can be closed.
576
+ useEffect(() => {
577
+ setOpenKeys(searchActive ? (items || []).filter((c) => c.sub_menus && c.sub_menus.length > 0).map((c) => String(c.id)) : []);
578
+ // eslint-disable-next-line react-hooks/exhaustive-deps
579
+ }, [query, searchActive]);
580
+
559
581
  const moveSubMenu = useCallback(
560
582
  (from, to) => {
561
583
  if (!dragMode || from === to) return;
@@ -574,17 +596,11 @@ function NestedMenu({
574
596
  return null;
575
597
  }
576
598
 
577
- // While searching, derive the open sub-panels from the items so the path to a
578
- // match stays expanded; otherwise use the user's manually-expanded panels.
579
- const activeKeys = searchActive
580
- ? localItems.filter((c) => c.sub_menus && c.sub_menus.length > 0).map((c) => String(c.id))
581
- : manualOpenKeys;
582
-
583
599
  return (
584
600
  <Collapse
585
601
  accordion={!searchActive}
586
- activeKey={activeKeys}
587
- onChange={(keys) => setManualOpenKeys(Array.isArray(keys) ? keys : keys ? [keys] : [])}
602
+ activeKey={openKeys}
603
+ onChange={(keys) => setOpenKeys(Array.isArray(keys) ? keys : keys ? [keys] : [])}
588
604
  >
589
605
  {localItems.map((child, index) => (
590
606
  <Panel
@@ -145,11 +145,19 @@ const RoleAdd = ({ model, callback, edit, formContent = {}, match, additional_qu
145
145
  });
146
146
 
147
147
  try {
148
- 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
+
149
156
  message.success(formContent?.id ? 'Role Updated' : 'Role Added');
150
157
  callback();
151
158
  } catch (err) {
152
- // message.error('Something went wrong');
159
+ // Surface any unexpected backend error message as a warning.
160
+ message.warning(err?.message || err?.result?.message || 'Failed to save role');
153
161
  } finally {
154
162
  onSubmittingChange?.(false);
155
163
  }
@@ -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
 
@@ -687,6 +687,29 @@ export default function ReportingDashboard({
687
687
  );
688
688
  }
689
689
 
690
+ /**
691
+ * Filters report records using the same search text that drives the table.
692
+ *
693
+ * @param {Array<Object>} records - Report rows.
694
+ * @param {string} searchText - Current table search text.
695
+ * @returns {Array<Object>} Rows matching the search text.
696
+ */
697
+ function filterReportRecords(records, searchText) {
698
+ if (!Array.isArray(records)) return [];
699
+
700
+ const query = String(searchText || '').trim().toLowerCase();
701
+
702
+ if (!query) return records;
703
+
704
+ return records.filter((record) =>
705
+ Object.values(record).some((value) => {
706
+ if (value === undefined || value === null || typeof value === 'object') return false;
707
+
708
+ return String(value).toLowerCase().indexOf(query) !== -1;
709
+ })
710
+ );
711
+ }
712
+
690
713
  /**
691
714
  *
692
715
  * @param root0
@@ -804,13 +827,13 @@ function GuestList({
804
827
  return col;
805
828
  });
806
829
  const summaryCols = columns.filter((col) => col.enable_summary);
807
- let dataToExport = [...patients];
830
+ let dataToExport = [...filterReportRecords(patients, query)];
808
831
 
809
832
  if (summaryCols.length > 0) {
810
833
  // Build one synthetic row for CSV export that mirrors the table layout:
811
834
  // numeric summary cells are populated from `calculateSummaryValues`, while
812
835
  // non-summary columns stay blank unless a configured caption should be shown.
813
- const summaryValues = calculateSummaryValues(summaryCols, patients);
836
+ const summaryValues = calculateSummaryValues(summaryCols, dataToExport);
814
837
  const summaryRow = { isSummaryRow: true };
815
838
 
816
839
  cols.forEach((col, index) => {
@@ -837,37 +860,15 @@ function GuestList({
837
860
  }
838
861
  let exportDatas = getExportData(dataToExport, exportCols);
839
862
 
840
- if (exportDatas.exportDataColumns.length && exportDatas.exportDataHeaders.length) {
863
+ if (exportDatas.exportDataHeaders.length) {
841
864
  setExportData({ exportDatas });
865
+ } else {
866
+ setExportData({});
842
867
  }
843
868
  }
844
- }, [patients, columns]);
869
+ }, [patients, columns, query]);
845
870
 
846
- let filtered;
847
-
848
- if (patients) {
849
- filtered = patients.filter((record) => {
850
- if (query) {
851
- // Keys
852
- let keys = Object.keys(record);
853
-
854
- let flag = false;
855
-
856
- keys.forEach((key) => {
857
- let ele = record[key];
858
-
859
- if (ele && typeof ele === 'string' && ele.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
860
- flag = true;
861
- }
862
- });
863
-
864
- /**Will return flag */
865
- return flag;
866
- } else {
867
- return true;
868
- }
869
- });
870
- }
871
+ let filtered = filterReportRecords(patients, query);
871
872
 
872
873
  /**
873
874
  * Checks for a match in the filtered patient list based on a scanned code,
@@ -1056,7 +1057,7 @@ function GuestList({
1056
1057
  dataSource={filtered ? filtered : patients} // In case if there is no filtered values we can use patient data
1057
1058
  columns={cols}
1058
1059
  sticky
1059
- pagination={false}
1060
+ pagination={true}
1060
1061
  summary={(pageData) => {
1061
1062
  const summaryCols = columns.filter((col) => col.enable_summary);
1062
1063
  if (!summaryCols.length) return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui-soxo-bootstrap-core",
3
- "version": "2.6.40-dev.12",
3
+ "version": "2.6.40-dev.16",
4
4
  "description": "All the Core Components for you to start",
5
5
  "keywords": [
6
6
  "all in one"