@onehat/ui 0.2.70 → 0.2.71

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onehat/ui",
3
- "version": "0.2.70",
3
+ "version": "0.2.71",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -851,6 +851,27 @@ export function Grid(props) {
851
851
 
852
852
  }
853
853
 
854
+ export const Grid = withAlert(
855
+ withEvents(
856
+ withData(
857
+ withMultiSelection(
858
+ withSelection(
859
+ // withSideEditor(
860
+ withFilters(
861
+ withPresetButtons(
862
+ withContextMenu(
863
+ Grid
864
+ ),
865
+ true // isGrid
866
+ )
867
+ )
868
+ // )
869
+ )
870
+ )
871
+ )
872
+ )
873
+ );
874
+
854
875
  export const SideGridEditor = withAlert(
855
876
  withEvents(
856
877
  withData(
@@ -861,7 +882,8 @@ export const SideGridEditor = withAlert(
861
882
  withPresetButtons(
862
883
  withContextMenu(
863
884
  Grid
864
- )
885
+ ),
886
+ true // isGrid
865
887
  )
866
888
  )
867
889
  )
@@ -913,4 +935,4 @@ export const InlineGridEditor = withAlert(
913
935
  )
914
936
  );
915
937
 
916
- export default WindowedGridEditor;
938
+ export default Grid;
@@ -11,7 +11,7 @@ export default function withEditor(WrappedComponent) {
11
11
 
12
12
  let [editorMode, setEditorMode] = useState(EDITOR_MODE__VIEW); // Can change below, so use 'let'
13
13
  const {
14
- useEditor = false,
14
+ useEditor = true,
15
15
  userCanEdit = true,
16
16
  userCanView = true,
17
17
  disableAdd = false,
@@ -213,6 +213,7 @@ export default function withEditor(WrappedComponent) {
213
213
  onEditorCancel={onEditorCancel}
214
214
  onEditorDelete={(!userCanEdit || disableDelete || (editorMode === EDITOR_MODE__ADD && (selection[0]?.isPhantom || currentRecord?.isPhantom))) ? null : onEditorDelete}
215
215
  onEditorClose={onEditorClose}
216
+ isEditor={true}
216
217
  useEditor={useEditor}
217
218
  userCanEdit={userCanEdit}
218
219
  userCanView={userCanView}
@@ -33,10 +33,11 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
33
33
  } = props,
34
34
  {
35
35
  // for local use
36
+ isEditor = false,
36
37
  useEditor = true,
37
- disableAdd = false,
38
- disableEdit = false,
39
- disableDelete = false,
38
+ disableAdd = !isEditor,
39
+ disableEdit = !isEditor,
40
+ disableDelete = !isEditor,
40
41
  disableView = !isGrid,
41
42
  disableCopy = !isGrid,
42
43
  disableDuplicate = !isGrid,
@@ -14,6 +14,10 @@ export default function withSideEditor(WrappedComponent) {
14
14
  sideFlex = 100,
15
15
  } = props;
16
16
 
17
+ if (!Editor) {
18
+ throw Error('Editor is not defined');
19
+ }
20
+
17
21
  return <Container
18
22
  center={<WrappedComponent {...props} />}
19
23
  east={<Editor
@@ -35,6 +35,10 @@ export default function withWindowedEditor(WrappedComponent) {
35
35
  editorProps = {},
36
36
  } = props;
37
37
 
38
+ if (!Editor) {
39
+ throw Error('Editor is not defined');
40
+ }
41
+
38
42
  return <>
39
43
  <WrappedComponent {...props} />
40
44
  {useEditor && isEditorShown &&
@@ -0,0 +1,39 @@
1
+ import { useEffect, useState, } from 'react';
2
+ import Panel from './Panel.js';
3
+ import { InlineGridEditor, } from '../Grid/Grid.js';
4
+ import _ from 'lodash';
5
+
6
+ export function GridPanel(props) {
7
+ const {
8
+ disableTitleChange = false,
9
+ selectorSelected,
10
+ } = props,
11
+ originalTitle = props.title,
12
+ [isReady, setIsReady] = useState(disableTitleChange),
13
+ [title, setTitle] = useState(originalTitle);
14
+
15
+ useEffect(() => {
16
+ if (!disableTitleChange && originalTitle) {
17
+ let newTitle = originalTitle;
18
+ if (selectorSelected?.[0]?.displayValue) {
19
+ newTitle = originalTitle + ' for ' + selectorSelected[0].displayValue;
20
+ }
21
+ if (newTitle !== title) {
22
+ setTitle(newTitle);
23
+ }
24
+ }
25
+ if (!isReady) {
26
+ setIsReady(true);
27
+ }
28
+ }, [selectorSelected, disableTitleChange, originalTitle]);
29
+
30
+ if (!isReady) {
31
+ return null;
32
+ }
33
+
34
+ return <Panel {...props} title={title}>
35
+ <InlineGridEditor {...props} />
36
+ </Panel>;
37
+ }
38
+
39
+ export default GridPanel;
@@ -0,0 +1,39 @@
1
+ import { useEffect, useState, } from 'react';
2
+ import Panel from './Panel.js';
3
+ import { SideGridEditor, } from '../Grid/Grid.js';
4
+ import _ from 'lodash';
5
+
6
+ export function GridPanel(props) {
7
+ const {
8
+ disableTitleChange = false,
9
+ selectorSelected,
10
+ } = props,
11
+ originalTitle = props.title,
12
+ [isReady, setIsReady] = useState(disableTitleChange),
13
+ [title, setTitle] = useState(originalTitle);
14
+
15
+ useEffect(() => {
16
+ if (!disableTitleChange && originalTitle) {
17
+ let newTitle = originalTitle;
18
+ if (selectorSelected?.[0]?.displayValue) {
19
+ newTitle = originalTitle + ' for ' + selectorSelected[0].displayValue;
20
+ }
21
+ if (newTitle !== title) {
22
+ setTitle(newTitle);
23
+ }
24
+ }
25
+ if (!isReady) {
26
+ setIsReady(true);
27
+ }
28
+ }, [selectorSelected, disableTitleChange, originalTitle]);
29
+
30
+ if (!isReady) {
31
+ return null;
32
+ }
33
+
34
+ return <Panel {...props} title={title}>
35
+ <SideGridEditor {...props} />
36
+ </Panel>;
37
+ }
38
+
39
+ export default GridPanel;
@@ -0,0 +1,39 @@
1
+ import { useEffect, useState, } from 'react';
2
+ import Panel from './Panel.js';
3
+ import { SideTreeEditor, } from '../Tree/Tree.js';
4
+ import _ from 'lodash';
5
+
6
+ export function TreePanel(props) {
7
+ const {
8
+ disableTitleChange = false,
9
+ selectorSelected,
10
+ } = props,
11
+ originalTitle = props.title,
12
+ [isReady, setIsReady] = useState(disableTitleChange),
13
+ [title, setTitle] = useState(originalTitle);
14
+
15
+ useEffect(() => {
16
+ if (!disableTitleChange && originalTitle) {
17
+ let newTitle = originalTitle;
18
+ if (selectorSelected?.[0]?.displayValue) {
19
+ newTitle = originalTitle + ' for ' + selectorSelected[0].displayValue;
20
+ }
21
+ if (newTitle !== title) {
22
+ setTitle(newTitle);
23
+ }
24
+ }
25
+ if (!isReady) {
26
+ setIsReady(true);
27
+ }
28
+ }, [selectorSelected, disableTitleChange, originalTitle]);
29
+
30
+ if (!isReady) {
31
+ return null;
32
+ }
33
+
34
+ return <Panel {...props} title={title}>
35
+ <SideTreeEditor {...props} />
36
+ </Panel>;
37
+ }
38
+
39
+ export default TreePanel;
@@ -0,0 +1,39 @@
1
+ import { useEffect, useState, } from 'react';
2
+ import Panel from './Panel.js';
3
+ import { WindowedGridEditor, } from '../Grid/Grid.js';
4
+ import _ from 'lodash';
5
+
6
+ export function GridPanel(props) {
7
+ const {
8
+ disableTitleChange = false,
9
+ selectorSelected,
10
+ } = props,
11
+ originalTitle = props.title,
12
+ [isReady, setIsReady] = useState(disableTitleChange),
13
+ [title, setTitle] = useState(originalTitle);
14
+
15
+ useEffect(() => {
16
+ if (!disableTitleChange && originalTitle) {
17
+ let newTitle = originalTitle;
18
+ if (selectorSelected?.[0]?.displayValue) {
19
+ newTitle = originalTitle + ' for ' + selectorSelected[0].displayValue;
20
+ }
21
+ if (newTitle !== title) {
22
+ setTitle(newTitle);
23
+ }
24
+ }
25
+ if (!isReady) {
26
+ setIsReady(true);
27
+ }
28
+ }, [selectorSelected, disableTitleChange, originalTitle]);
29
+
30
+ if (!isReady) {
31
+ return null;
32
+ }
33
+
34
+ return <Panel {...props} title={title}>
35
+ <WindowedGridEditor {...props} />
36
+ </Panel>;
37
+ }
38
+
39
+ export default GridPanel;
@@ -0,0 +1,39 @@
1
+ import { useEffect, useState, } from 'react';
2
+ import Panel from './Panel.js';
3
+ import { WindowedTreeEditor, } from '../Tree/Tree.js';
4
+ import _ from 'lodash';
5
+
6
+ export function TreePanel(props) {
7
+ const {
8
+ disableTitleChange = false,
9
+ selectorSelected,
10
+ } = props,
11
+ originalTitle = props.title,
12
+ [isReady, setIsReady] = useState(disableTitleChange),
13
+ [title, setTitle] = useState(originalTitle);
14
+
15
+ useEffect(() => {
16
+ if (!disableTitleChange && originalTitle) {
17
+ let newTitle = originalTitle;
18
+ if (selectorSelected?.[0]?.displayValue) {
19
+ newTitle = originalTitle + ' for ' + selectorSelected[0].displayValue;
20
+ }
21
+ if (newTitle !== title) {
22
+ setTitle(newTitle);
23
+ }
24
+ }
25
+ if (!isReady) {
26
+ setIsReady(true);
27
+ }
28
+ }, [selectorSelected, disableTitleChange, originalTitle]);
29
+
30
+ if (!isReady) {
31
+ return null;
32
+ }
33
+
34
+ return <Panel {...props} title={title}>
35
+ <WindowedTreeEditor {...props} />
36
+ </Panel>;
37
+ }
38
+
39
+ export default TreePanel;
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useRef, useMemo, } from 'react';
1
+ import { useState, useEffect, useRef, useMemo, } from 'react';
2
2
  import {
3
3
  Column,
4
4
  FlatList,
@@ -19,7 +19,6 @@ import {
19
19
  DROP_POSITION_BEFORE,
20
20
  DROP_POSITION_AFTER,
21
21
  } from '../../Constants/Tree.js';
22
- import sleep from '@onehat/ui/src/Functions/sleep.js';
23
22
  import * as colourMixer from '@k-renwick/colour-mixer'
24
23
  import UiGlobals from '../../UiGlobals.js';
25
24
  import useForceUpdate from '../../Hooks/useForceUpdate.js';
@@ -69,7 +68,10 @@ export function Tree(props) {
69
68
  areRootsVisible = true,
70
69
  extraParams = {}, // Additional params to send with each request ( e.g. { order: 'Categories.name ASC' })
71
70
  getNodeText = (item) => { // extracts model/data and decides what the row text should be
72
- return item.displayValue;
71
+ if (Repository) {
72
+ return item.displayValue;
73
+ }
74
+ return item[displayIx];
73
75
  },
74
76
  getNodeIcon = (item, isExpanded) => { // decides what icon to show for this node
75
77
  // TODO: Allow for dynamic props on the icon (e.g. special color for some icons)
@@ -98,6 +100,8 @@ export function Tree(props) {
98
100
  bottomToolbar = null,
99
101
  topToolbar = null,
100
102
  additionalToolbarButtons = [],
103
+ reload = null, // Whenever this value changes after initial render, the tree will reload from scratch
104
+ parentIdIx,
101
105
 
102
106
  // withEditor
103
107
  onAdd,
@@ -141,6 +145,7 @@ export function Tree(props) {
141
145
  [isReorderMode, setIsReorderMode] = useState(false),
142
146
  [isSearchModalShown, setIsSearchModalShown] = useState(false),
143
147
  [treeNodeData, setTreeNodeData] = useState({}),
148
+ [searchResults, setSearchResults] = useState([]),
144
149
  [searchFormData, setSearchFormData] = useState([]),
145
150
  [dragNodeSlot, setDragNodeSlot] = useState(null),
146
151
  [dragNodeIx, setDragNodeIx] = useState(),
@@ -477,6 +482,69 @@ export function Tree(props) {
477
482
 
478
483
  return !_.isEmpty(intersection);
479
484
  },
485
+ buildAndSetTreeNodeData = async () => {
486
+ let rootNodes;
487
+ if (Repository) {
488
+ if (!Repository.areRootNodesLoaded) {
489
+ rootNodes = await Repository.getRootNodes(1);
490
+ }
491
+ } else {
492
+ rootNodes = assembleDataTreeNodes();
493
+ }
494
+
495
+ const treeNodeData = buildTreeNodeData(rootNodes);
496
+ setTreeNodeData(treeNodeData);
497
+ },
498
+ assembleDataTreeNodes = () => {
499
+ // Populates the TreeNodes with .parent and .children references
500
+ // NOTE: This is only for 'data', not for Repositories!
501
+ // 'data' is essentially an Adjacency List, not a ClosureTable.
502
+
503
+ const clonedData = _.clone(data);
504
+
505
+ // Reset all parent/child relationships
506
+ _.each(clonedData, (treeNode) => {
507
+ treeNode.isRoot = !treeNode[parentIdIx];
508
+ treeNode.parent = null;
509
+ treeNode.children = [];
510
+ });
511
+
512
+ // Rebuild all parent/child relationships
513
+ _.each(clonedData, (treeNode) => {
514
+ const parent = _.find(clonedData, (tn) => {
515
+ return tn[idIx] === treeNode[parentIdIx];
516
+ });
517
+ if (parent) {
518
+ treeNode.parent = parent;
519
+ parent.children.push(treeNode);
520
+ }
521
+ });
522
+
523
+ // populate calculated fields
524
+ const treeNodes = [];
525
+ _.each(clonedData, (treeNode) => {
526
+ treeNode.hasChildren = !_.isEmpty(treeNode.children);
527
+
528
+ let parent = treeNode.parent,
529
+ i = 0;
530
+ while(parent) {
531
+ i++;
532
+ parent = parent.parent;
533
+ }
534
+ treeNode.depth = i;
535
+ treeNode.hash = treeNode[idIx];
536
+
537
+ if (treeNode.isRoot) {
538
+ treeNodes.push(treeNode);
539
+ }
540
+ });
541
+
542
+ return treeNodes;
543
+ },
544
+ reloadTree = () => {
545
+ Repository.areRootNodesLoaded = false;
546
+ return buildAndSetTreeNodeData();
547
+ };
480
548
 
481
549
  // Button handlers
482
550
  onToggle = (datum) => {
@@ -539,7 +607,6 @@ export function Tree(props) {
539
607
  });
540
608
  },
541
609
  onSearchTree = async (value) => {
542
-
543
610
  let found = [];
544
611
  if (Repository?.isRemote) {
545
612
  // Search tree on server
@@ -550,16 +617,17 @@ export function Tree(props) {
550
617
  }
551
618
 
552
619
  const isMultipleHits = found.length > 1;
553
- let path = '';
554
- let searchFormData = [];
555
-
556
620
  if (!isMultipleHits) {
557
- path = found[0].path;
558
- expandPath(path);
621
+ expandPath(found[0].path);
559
622
  return;
560
623
  }
561
624
 
625
+ const searchFormData = [];
626
+ _.each(found, (item) => {
627
+ searchFormData.push([item.id, getNodeText(item)]);
628
+ });
562
629
  setSearchFormData(searchFormData);
630
+ setSearchResults(found);
563
631
  setIsSearchModalShown(true);
564
632
  },
565
633
  findTreeNodesByText = (text) => {
@@ -588,33 +656,6 @@ export function Tree(props) {
588
656
  }
589
657
  return data[node_id]; // TODO: This is probably not right!
590
658
  },
591
- getPathByTreeNode = (treeNode) => {
592
-
593
- ///////// THIS DOESN'T WORK YET /////////
594
-
595
- function searchChildren(children, currentPath = []) {
596
- let found = [];
597
- _.each(children, (child) => {
598
- const
599
- item = child.item,
600
- id = idField ? item[idField] : item.id;
601
- if (child.text.match(regex)) {
602
- found.push(child);
603
- return false;
604
- }
605
- if (child.children) {
606
- const childrenFound = searchChildren(child.children, [...currentPath, id]);
607
- if (!_.isEmpty(childrenFound)) {
608
- return false;
609
- }
610
- }
611
- });
612
- return found;
613
- }
614
- const nodes = searchChildren(treeNodeData);
615
- return nodes.join('/');
616
-
617
- },
618
659
  expandPath = async (path) => {
619
660
  // Helper for onSearchTree
620
661
 
@@ -628,6 +669,7 @@ export function Tree(props) {
628
669
  id,
629
670
  currentLevelData = newTreeNodeData,
630
671
  currentDatum,
672
+ parentDatum,
631
673
  currentNode;
632
674
 
633
675
  while(path.length) {
@@ -641,9 +683,11 @@ export function Tree(props) {
641
683
 
642
684
  if (!currentDatum) {
643
685
  // datum is not currently loaded, so load it
644
-
645
- // LEFT OFF HERE
646
- debugger;
686
+ await loadChildren(parentDatum, 1);
687
+ currentLevelData = parentDatum.children;
688
+ currentDatum = _.find(currentLevelData, (treeNodeDatum) => {
689
+ return treeNodeDatum.item.id === id;
690
+ });
647
691
  }
648
692
 
649
693
  currentNode = currentDatum.item;
@@ -653,6 +697,7 @@ export function Tree(props) {
653
697
 
654
698
  path = pathParts.slice(1).join('/'); // put the rest of it back together
655
699
  currentLevelData = currentDatum.children;
700
+ parentDatum = currentDatum;
656
701
  }
657
702
 
658
703
  setSelection([currentNode]);
@@ -923,29 +968,15 @@ export function Tree(props) {
923
968
  }
924
969
  setDragNodeSlot(null);
925
970
  };
926
-
927
- useEffect(() => {
928
-
929
- async function buildAndSetTreeNodeData() {
930
-
931
- let rootNodes;
932
- if (Repository) {
933
- if (!Repository.areRootNodesLoaded) {
934
- rootNodes = await Repository.getRootNodes(1);
935
- }
936
- } else {
937
- // TODO: Make this work for data array
938
971
 
939
- }
940
-
941
- const treeNodeData = buildTreeNodeData(rootNodes);
942
- setTreeNodeData(treeNodeData);
943
- }
944
-
945
- function reloadTreeData() {
946
- Repository.areRootNodesLoaded = false;
947
- return buildAndSetTreeNodeData();
972
+ useEffect(() => {
973
+ if (!isReady) {
974
+ return () => {};
948
975
  }
976
+ reloadTree();
977
+ }, [reload]);
978
+
979
+ useEffect(() => {
949
980
 
950
981
  if (!isReady) {
951
982
  if (Repository) {
@@ -970,16 +1001,16 @@ export function Tree(props) {
970
1001
  Repository.on('load', setFalse);
971
1002
  Repository.ons(['changePage', 'changePageSize',], deselectAll);
972
1003
  Repository.ons(['changeData', 'change'], buildAndSetTreeNodeData);
973
- Repository.on('changeFilters', reloadTreeData);
974
- Repository.on('changeSorters', reloadTreeData);
1004
+ Repository.on('changeFilters', reloadTree);
1005
+ Repository.on('changeSorters', reloadTree);
975
1006
 
976
1007
  return () => {
977
1008
  Repository.off('beforeLoad', setTrue);
978
1009
  Repository.off('load', setFalse);
979
1010
  Repository.offs(['changePage', 'changePageSize',], deselectAll);
980
1011
  Repository.offs(['changeData', 'change'], buildAndSetTreeNodeData);
981
- Repository.off('changeFilters', onChangeFilters);
982
- Repository.off('changeSorters', onChangeSorters);
1012
+ Repository.off('changeFilters', reloadTree);
1013
+ Repository.off('changeSorters', reloadTree);
983
1014
  };
984
1015
  }, []);
985
1016
 
@@ -1037,11 +1068,11 @@ export function Tree(props) {
1037
1068
  {treeFooterComponent}
1038
1069
  </Column>
1039
1070
 
1040
- {/* <Modal
1071
+ <Modal
1041
1072
  isOpen={isSearchModalShown}
1042
1073
  onClose={() => setIsSearchModalShown(false)}
1043
1074
  >
1044
- <Column bg="#fff" w={500}>
1075
+ <Column bg="#fff" w={300}>
1045
1076
  <FormPanel
1046
1077
  title="Choose Tree Node"
1047
1078
  instructions="Multiple tree nodes matched your search. Please select which one to show."
@@ -1066,32 +1097,43 @@ export function Tree(props) {
1066
1097
  setIsSearchModalShown(false);
1067
1098
  }}
1068
1099
  onSave={(data, e) => {
1069
-
1070
- const node_id = data.node_id; // NOT SURE THIS IS CORRECT!
1071
-
1072
- if (isMultipleHits) {
1073
- // Tell the server which one you want and get it, loading all children necessary to get there
1074
-
1075
-
1076
-
1077
- } else {
1078
- // Show the path based on local data
1079
- const
1080
- treeNode = getTreeNodeByNodeId(node_id),
1081
- path = getPathByTreeNode(treeNode);
1082
- expandPath(path);
1083
- }
1100
+ const
1101
+ treeNode = _.find(searchResults, (item) => {
1102
+ return item.id === data.node_id;
1103
+ }),
1104
+ path = treeNode.path;
1105
+ expandPath(path);
1084
1106
 
1085
1107
  // Close the modal
1086
1108
  setIsSearchModalShown(false);
1087
1109
  }}
1088
1110
  />
1089
1111
  </Column>
1090
- </Modal> */}
1112
+ </Modal>
1091
1113
  </>;
1092
1114
 
1093
1115
  }
1094
1116
 
1117
+ export const Tree = withAlert(
1118
+ withEvents(
1119
+ withData(
1120
+ // withMultiSelection(
1121
+ withSelection(
1122
+ // withSideEditor(
1123
+ withFilters(
1124
+ // withPresetButtons(
1125
+ withContextMenu(
1126
+ Tree
1127
+ )
1128
+ // )
1129
+ )
1130
+ // )
1131
+ )
1132
+ // )
1133
+ )
1134
+ )
1135
+ );
1136
+
1095
1137
  export const SideTreeEditor = withAlert(
1096
1138
  withEvents(
1097
1139
  withData(
@@ -1132,4 +1174,4 @@ export const WindowedTreeEditor = withAlert(
1132
1174
  )
1133
1175
  );
1134
1176
 
1135
- export default WindowedTreeEditor;
1177
+ export default Tree;