@onehat/ui 0.2.69 → 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.69",
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,18 @@
1
+ import * as React from "react"
2
+ import Svg, { G, Path } from "react-native-svg"
3
+ import { Icon } from 'native-base';
4
+
5
+ function SvgComponent(props) {
6
+ return (
7
+ <Icon
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 0 508.3 508.87"
10
+ {...props}
11
+ >
12
+ <Path d="M253.87 387.44c73.46 0 133-59.55 133-133s-59.55-133-133-133-133 59.55-133 133 59.55 133 133 133z" />
13
+ <Path d="M0 0H508.3V508.87H0z" fill="none" />
14
+ </Icon>
15
+ )
16
+ }
17
+
18
+ export default SvgComponent
@@ -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,
@@ -38,7 +38,7 @@ import TreeNode, { ReorderableTreeNode } from './TreeNode.js';
38
38
  import FormPanel from '../Panel/FormPanel.js';
39
39
  import Input from '../Form/Field/Input.js';
40
40
  import IconButton from '../Buttons/IconButton.js';
41
- import Circle from '../Icons/Circle.js';
41
+ import Dot from '../Icons/Dot.js';
42
42
  import Collapse from '../Icons/Collapse.js';
43
43
  import FolderClosed from '../Icons/FolderClosed.js';
44
44
  import FolderOpen from '../Icons/FolderOpen.js';
@@ -51,28 +51,9 @@ import Toolbar from '../Toolbar/Toolbar.js';
51
51
  import _ from 'lodash';
52
52
 
53
53
 
54
- // Tree requires the use of HOC withSelection() whenever it's used.
55
- // The default export is *with* the HOC. A separate *raw* component is
56
- // exported which can be combined with many HOCs for various functionality.
57
-
58
-
59
54
  //////////////////////
60
55
  //////////////////////
61
56
 
62
- // I'm thinking if a repository senses that it's a tree, then at initial load
63
- // it should get the root nodes +1 level of children.
64
- //
65
- // How would it then subsequently get the proper children?
66
- // i.e. When a node gets its children, how will it do this
67
- // while maintaining the nodes that already exist there?
68
- // We don't want it to *replace* all exisitng nodes!
69
- //
70
- // And if the repository does a reload, should it just get roots+1 again?
71
- // Changing filters would potentially change the tree structure.
72
- // Changing sorting would only change the ordering, not what is expanded/collapsed or visible/invisible.
73
-
74
-
75
-
76
57
  // Need to take into account whether using Repository or data.
77
58
  // If using data, everything exists at once. What format will data be in?
78
59
  // How does this interface with Repository?
@@ -82,20 +63,18 @@ import _ from 'lodash';
82
63
  //////////////////////
83
64
 
84
65
 
85
-
86
-
87
-
88
-
89
66
  export function Tree(props) {
90
67
  const {
91
68
  areRootsVisible = true,
92
- getAdditionalParams = () => { // URL params needed to get nodes from server (e.g, { venue_id: 1, getEquipment: true, getRentalEquipment: false, }), in addition to filters.
93
- return {};
94
- },
69
+ extraParams = {}, // Additional params to send with each request ( e.g. { order: 'Categories.name ASC' })
95
70
  getNodeText = (item) => { // extracts model/data and decides what the row text should be
96
- return item.displayValue;
71
+ if (Repository) {
72
+ return item.displayValue;
73
+ }
74
+ return item[displayIx];
97
75
  },
98
76
  getNodeIcon = (item, isExpanded) => { // decides what icon to show for this node
77
+ // TODO: Allow for dynamic props on the icon (e.g. special color for some icons)
99
78
  let icon;
100
79
  if (item.hasChildren) {
101
80
  if (isExpanded) {
@@ -104,7 +83,7 @@ export function Tree(props) {
104
83
  icon = FolderClosed;
105
84
  }
106
85
  } else {
107
- icon = Circle;
86
+ icon = Dot;
108
87
  }
109
88
  return icon;
110
89
  },
@@ -121,6 +100,8 @@ export function Tree(props) {
121
100
  bottomToolbar = null,
122
101
  topToolbar = null,
123
102
  additionalToolbarButtons = [],
103
+ reload = null, // Whenever this value changes after initial render, the tree will reload from scratch
104
+ parentIdIx,
124
105
 
125
106
  // withEditor
126
107
  onAdd,
@@ -164,11 +145,16 @@ export function Tree(props) {
164
145
  [isReorderMode, setIsReorderMode] = useState(false),
165
146
  [isSearchModalShown, setIsSearchModalShown] = useState(false),
166
147
  [treeNodeData, setTreeNodeData] = useState({}),
148
+ [searchResults, setSearchResults] = useState([]),
167
149
  [searchFormData, setSearchFormData] = useState([]),
168
150
  [dragNodeSlot, setDragNodeSlot] = useState(null),
169
151
  [dragNodeIx, setDragNodeIx] = useState(),
170
152
  [treeSearchValue, setTreeSearchValue] = useState(''),
171
153
  onNodeClick = (item, e) => {
154
+ if (!setSelection) {
155
+ return;
156
+ }
157
+
172
158
  const
173
159
  {
174
160
  shiftKey,
@@ -268,10 +254,15 @@ export function Tree(props) {
268
254
  const items = _.map(buttons, getIconFromConfig);
269
255
 
270
256
  items.unshift(<Input // Add text input to beginning of header items
271
- key="searchTree"
257
+ key="searchNodes"
272
258
  flex={1}
273
- placeholder="Search all tree nodes"
259
+ placeholder="Find tree node"
274
260
  onChangeText={(val) => setTreeSearchValue(val)}
261
+ onKeyPress={(e, value) => {
262
+ if (e.key === 'Enter') {
263
+ onSearchTree(value);
264
+ }
265
+ }}
275
266
  value={treeSearchValue}
276
267
  autoSubmit={false}
277
268
  />);
@@ -320,6 +311,7 @@ export function Tree(props) {
320
311
  // renderTreeNode uses this to render the nodes.
321
312
  const
322
313
  isRoot = treeNode.isRoot,
314
+ children = buildTreeNodeData(treeNode.children), // recursively get data for children
323
315
  datum = {
324
316
  item: treeNode,
325
317
  text: getNodeText(treeNode),
@@ -328,7 +320,8 @@ export function Tree(props) {
328
320
  iconLeaf: getNodeIcon(treeNode),
329
321
  isExpanded: isRoot, // all non-root treeNodes are not expanded by default
330
322
  isVisible: isRoot ? areRootsVisible : true,
331
- children: buildTreeNodeData(treeNode.children), // recursively get data for children
323
+ isLoading: false,
324
+ children,
332
325
  };
333
326
 
334
327
  return datum;
@@ -386,7 +379,11 @@ export function Tree(props) {
386
379
  e.preventDefault();
387
380
  }
388
381
  if (isReorderMode) {
389
- return
382
+ return;
383
+ }
384
+
385
+ if (!setSelection) {
386
+ return;
390
387
  }
391
388
 
392
389
  // context menu
@@ -450,78 +447,150 @@ export function Tree(props) {
450
447
  </Pressable>;
451
448
  },
452
449
  renderTreeNodes = (data) => {
453
- const nodes = [];
450
+ let nodes = [];
454
451
  _.each(data, (datum) => {
455
- nodes.push(renderTreeNode(datum));
452
+ const node = renderTreeNode(datum);
453
+ nodes.push(node);
454
+
455
+ if (datum.children.length && datum.isExpanded) {
456
+ const childTreeNodes = renderTreeNodes(datum.children); // recursion
457
+ nodes = nodes.concat(childTreeNodes);
458
+ }
456
459
  });
457
460
  return nodes;
458
461
  },
459
- renderAllTreeNodes = () => {
460
- let nodes = [];
461
- _.each(treeNodeData, (datum) => {
462
- const node = renderTreeNode(datum);
463
- if (_.isEmpty(node)) {
464
- return;
462
+ getDatumChildIds = (datum) => {
463
+ let ids = [];
464
+ _.each(datum.children, (childDatum) => {
465
+ ids.push(childDatum.item.id);
466
+ if (childDatum.children.length) {
467
+ const childIds = getDatumChildIds(childDatum);
468
+ ids = ids.concat(childIds);
469
+ const t = true;
465
470
  }
466
-
467
- nodes.push(node);
471
+ });
472
+ return ids;
473
+ },
474
+ datumContainsSelection = (datum) => {
475
+ if (_.isEmpty(selection)) {
476
+ return false;
477
+ }
478
+ const
479
+ selectionIds = _.map(selection, (item) => item.id),
480
+ datumIds = getDatumChildIds(datum),
481
+ intersection = selectionIds.filter(x => datumIds.includes(x));
468
482
 
469
- if (!datum.isExpanded) {
470
- return;
483
+ return !_.isEmpty(intersection);
484
+ },
485
+ buildAndSetTreeNodeData = async () => {
486
+ let rootNodes;
487
+ if (Repository) {
488
+ if (!Repository.areRootNodesLoaded) {
489
+ rootNodes = await Repository.getRootNodes(1);
471
490
  }
491
+ } else {
492
+ rootNodes = assembleDataTreeNodes();
493
+ }
472
494
 
473
- if (_.isEmpty(datum.children)) {
474
- return;
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);
475
520
  }
521
+ });
476
522
 
477
- const children = renderTreeNodes(datum.children);
478
- if (_.isEmpty(children)) {
479
- return;
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;
480
533
  }
534
+ treeNode.depth = i;
535
+ treeNode.hash = treeNode[idIx];
481
536
 
482
- nodes = nodes.concat(children);
537
+ if (treeNode.isRoot) {
538
+ treeNodes.push(treeNode);
539
+ }
483
540
  });
484
- return nodes;
541
+
542
+ return treeNodes;
485
543
  },
544
+ reloadTree = () => {
545
+ Repository.areRootNodesLoaded = false;
546
+ return buildAndSetTreeNodeData();
547
+ };
486
548
 
487
549
  // Button handlers
488
550
  onToggle = (datum) => {
551
+ if (datum.isLoading) {
552
+ return;
553
+ }
554
+
489
555
  datum.isExpanded = !datum.isExpanded;
490
- forceUpdate();
491
556
 
492
- if (datum.isExpanded && datum.item?.repository.isRemote && datum.item.hasChildren && !datum.item.isChildrenLoaded) {
557
+ if (datum.isExpanded && datum.item.repository?.isRemote && datum.item.hasChildren && !datum.item.areChildrenLoaded) {
493
558
  loadChildren(datum, 1);
559
+ return;
494
560
  }
561
+
562
+ if (!datum.isExpanded && datumContainsSelection(datum)) {
563
+ deselectAll();
564
+ }
565
+
566
+ forceUpdate();
495
567
  },
496
568
  loadChildren = async (datum, depth) => {
497
- // Helper for onToggle
498
-
499
- // TODO: Flesh this out
500
- // Show loading indicator (red bar at top? Spinner underneath current node?)
501
-
569
+ // Show loading indicator (spinner underneath current node?)
570
+ datum.isLoading = true;
571
+ forceUpdate();
502
572
 
503
- // Calls getAdditionalParams(), then submits to server
504
- // Server returns this for each node:
505
- // Build up treeNodeData for just these new nodes
506
-
573
+ try {
574
+
575
+ const children = await datum.item.loadChildren(1);
576
+ const tnd = buildTreeNodeData(children);
577
+ datum.children = tnd;
578
+
579
+ } catch (err) {
580
+ // TODO: how do I handle errors?
581
+ // Color parent node red
582
+ // Modal alert box?
583
+ // Inline error msg? I'm concerned about modals not stacking correctly, but if we put it inline, it'll work.
584
+ datum.isExpanded = false;
585
+ }
507
586
 
508
587
  // Hide loading indicator
509
-
588
+ datum.isLoading = false;
589
+ forceUpdate();
510
590
  },
511
591
  onCollapseAll = (setNewTreeNodeData = true) => {
512
592
  // Go through whole tree and collapse all nodes
513
593
  const newTreeNodeData = _.clone(treeNodeData);
514
-
515
- // Recursive method to collapse all children
516
- function collapseNodes(nodes) {
517
- _.each(nodes, (node) => {
518
- node.isExpanded = false;
519
- if (!_.isEmpty(node.children)) {
520
- collapseNodes(node.children);
521
- }
522
- });
523
- }
524
-
525
594
  collapseNodes(newTreeNodeData);
526
595
 
527
596
  if (setNewTreeNodeData) {
@@ -529,47 +598,37 @@ export function Tree(props) {
529
598
  }
530
599
  return newTreeNodeData;
531
600
  },
601
+ collapseNodes = (nodes) => {
602
+ _.each(nodes, (node) => {
603
+ node.isExpanded = false;
604
+ if (!_.isEmpty(node.children)) {
605
+ collapseNodes(node.children);
606
+ }
607
+ });
608
+ },
532
609
  onSearchTree = async (value) => {
533
-
534
610
  let found = [];
535
611
  if (Repository?.isRemote) {
536
612
  // Search tree on server
537
- found = await Repository.searchTree(value);
613
+ found = await Repository.searchNodes(value);
538
614
  } else {
539
615
  // Search local tree data
540
616
  found = findTreeNodesByText(value);
541
617
  }
542
618
 
543
-
544
619
  const isMultipleHits = found.length > 1;
545
- let path = '';
546
- let searchFormData = [];
547
-
548
- if (Repository?.isRemote) {
549
- if (isMultipleHits) {
550
- // 'found' is the results from the server. Use these to show the modal and choose which node you want to select
551
-
552
-
553
-
554
-
555
- } else {
556
- // Search local tree data
557
- found = findTreeNodesByText(value);
558
- }
559
-
560
- // TODO: create searchFormData based on 'found' array
561
-
562
-
563
-
564
-
565
-
566
- setSearchFormData(searchFormData);
567
- setIsSearchModalShown(true);
568
-
569
- } else {
570
- // Expand that one path immediately
571
- expandPath(path);
620
+ if (!isMultipleHits) {
621
+ expandPath(found[0].path);
622
+ return;
572
623
  }
624
+
625
+ const searchFormData = [];
626
+ _.each(found, (item) => {
627
+ searchFormData.push([item.id, getNodeText(item)]);
628
+ });
629
+ setSearchFormData(searchFormData);
630
+ setSearchResults(found);
631
+ setIsSearchModalShown(true);
573
632
  },
574
633
  findTreeNodesByText = (text) => {
575
634
  // Helper for onSearchTree
@@ -597,41 +656,12 @@ export function Tree(props) {
597
656
  }
598
657
  return data[node_id]; // TODO: This is probably not right!
599
658
  },
600
- getPathByTreeNode = (treeNode) => {
601
-
602
- ///////// THIS DOESN'T WORK YET /////////
603
-
604
- function searchChildren(children, currentPath = []) {
605
- let found = [];
606
- _.each(children, (child) => {
607
- const
608
- item = child.item,
609
- id = idField ? item[idField] : item.id;
610
- if (child.text.match(regex)) {
611
- found.push(child);
612
- return false;
613
- }
614
- if (child.children) {
615
- const childrenFound = searchChildren(child.children, [...currentPath, id]);
616
- if (!_.isEmpty(childrenFound)) {
617
- return false;
618
- }
619
- }
620
- });
621
- return found;
622
- }
623
- const nodes = searchChildren(treeNodeData);
624
- return nodes.join('/');
625
-
626
- },
627
- expandPath = (path) => {
659
+ expandPath = async (path) => {
628
660
  // Helper for onSearchTree
629
661
 
630
- // Drills down the tree based on path (usually given by server).
631
- // Path would be a list of sequential IDs (3/35/263/1024)
632
- // Initially, it closes thw whole tree.
633
-
634
- let newTreeNodeData = collapseAll(false); // false = don't set new treeNodeData
662
+ // First, close thw whole tree.
663
+ let newTreeNodeData = _.clone(treeNodeData);
664
+ collapseNodes(newTreeNodeData);
635
665
 
636
666
  // As it navigates down, it will expand the appropriate branches,
637
667
  // and then finally highlight & select the node in question
@@ -639,6 +669,7 @@ export function Tree(props) {
639
669
  id,
640
670
  currentLevelData = newTreeNodeData,
641
671
  currentDatum,
672
+ parentDatum,
642
673
  currentNode;
643
674
 
644
675
  while(path.length) {
@@ -649,6 +680,15 @@ export function Tree(props) {
649
680
  currentDatum = _.find(currentLevelData, (treeNodeDatum) => {
650
681
  return treeNodeDatum.item.id === id;
651
682
  });
683
+
684
+ if (!currentDatum) {
685
+ // datum is not currently loaded, so load it
686
+ await loadChildren(parentDatum, 1);
687
+ currentLevelData = parentDatum.children;
688
+ currentDatum = _.find(currentLevelData, (treeNodeDatum) => {
689
+ return treeNodeDatum.item.id === id;
690
+ });
691
+ }
652
692
 
653
693
  currentNode = currentDatum.item;
654
694
 
@@ -657,6 +697,7 @@ export function Tree(props) {
657
697
 
658
698
  path = pathParts.slice(1).join('/'); // put the rest of it back together
659
699
  currentLevelData = currentDatum.children;
700
+ parentDatum = currentDatum;
660
701
  }
661
702
 
662
703
  setSelection([currentNode]);
@@ -927,26 +968,20 @@ export function Tree(props) {
927
968
  }
928
969
  setDragNodeSlot(null);
929
970
  };
971
+
972
+ useEffect(() => {
973
+ if (!isReady) {
974
+ return () => {};
975
+ }
976
+ reloadTree();
977
+ }, [reload]);
930
978
 
931
979
  useEffect(() => {
932
980
 
933
- async function buildAndSetTreeNodeData() {
934
-
935
- let rootNodes;
981
+ if (!isReady) {
936
982
  if (Repository) {
937
- if (!Repository.areRootNodesLoaded) {
938
- rootNodes = await Repository.getRootNodes(true, 1, getAdditionalParams);
939
- }
940
- } else {
941
- // TODO: Make this work for data array
942
-
983
+ Repository.setBaseParams(extraParams);
943
984
  }
944
-
945
- const treeNodeData = buildTreeNodeData(rootNodes);
946
- setTreeNodeData(treeNodeData);
947
- }
948
-
949
- if (!isReady) {
950
985
  (async () => {
951
986
  await buildAndSetTreeNodeData();
952
987
  setIsReady(true);
@@ -956,37 +991,26 @@ export function Tree(props) {
956
991
  if (!Repository) {
957
992
  return () => {};
958
993
  }
959
-
994
+
960
995
  // set up @onehat/data repository
961
996
  const
962
997
  setTrue = () => setIsLoading(true),
963
- setFalse = () => setIsLoading(false),
964
- onChangeFilters = () => {
965
- if (!Repository.isAutoLoad) {
966
- Repository.reload();
967
- }
968
- },
969
- onChangeSorters = () => {
970
- if (!Repository.isAutoLoad) {
971
- Repository.reload();
972
- }
973
- };
974
-
998
+ setFalse = () => setIsLoading(false);
999
+
975
1000
  Repository.on('beforeLoad', setTrue);
976
1001
  Repository.on('load', setFalse);
977
1002
  Repository.ons(['changePage', 'changePageSize',], deselectAll);
978
1003
  Repository.ons(['changeData', 'change'], buildAndSetTreeNodeData);
979
- Repository.on('changeFilters', onChangeFilters);
980
- Repository.on('changeSorters', onChangeSorters);
981
-
1004
+ Repository.on('changeFilters', reloadTree);
1005
+ Repository.on('changeSorters', reloadTree);
982
1006
 
983
1007
  return () => {
984
1008
  Repository.off('beforeLoad', setTrue);
985
1009
  Repository.off('load', setFalse);
986
1010
  Repository.offs(['changePage', 'changePageSize',], deselectAll);
987
1011
  Repository.offs(['changeData', 'change'], buildAndSetTreeNodeData);
988
- Repository.off('changeFilters', onChangeFilters);
989
- Repository.off('changeSorters', onChangeSorters);
1012
+ Repository.off('changeFilters', reloadTree);
1013
+ Repository.off('changeSorters', reloadTree);
990
1014
  };
991
1015
  }, []);
992
1016
 
@@ -1011,7 +1035,7 @@ export function Tree(props) {
1011
1035
  if (!isReady) {
1012
1036
  return null;
1013
1037
  }
1014
- const treeNodes = renderAllTreeNodes();
1038
+ const treeNodes = renderTreeNodes(treeNodeData);
1015
1039
 
1016
1040
  // headers & footers
1017
1041
  let treeFooterComponent = null;
@@ -1044,11 +1068,11 @@ export function Tree(props) {
1044
1068
  {treeFooterComponent}
1045
1069
  </Column>
1046
1070
 
1047
- {/* <Modal
1071
+ <Modal
1048
1072
  isOpen={isSearchModalShown}
1049
1073
  onClose={() => setIsSearchModalShown(false)}
1050
1074
  >
1051
- <Column bg="#fff" w={500}>
1075
+ <Column bg="#fff" w={300}>
1052
1076
  <FormPanel
1053
1077
  title="Choose Tree Node"
1054
1078
  instructions="Multiple tree nodes matched your search. Please select which one to show."
@@ -1073,32 +1097,43 @@ export function Tree(props) {
1073
1097
  setIsSearchModalShown(false);
1074
1098
  }}
1075
1099
  onSave={(data, e) => {
1076
-
1077
- const node_id = data.node_id; // NOT SURE THIS IS CORRECT!
1078
-
1079
- if (isMultipleHits) {
1080
- // Tell the server which one you want and get it, loading all children necessary to get there
1081
-
1082
-
1083
-
1084
- } else {
1085
- // Show the path based on local data
1086
- const
1087
- treeNode = getTreeNodeByNodeId(node_id),
1088
- path = getPathByTreeNode(treeNode);
1089
- expandPath(path);
1090
- }
1100
+ const
1101
+ treeNode = _.find(searchResults, (item) => {
1102
+ return item.id === data.node_id;
1103
+ }),
1104
+ path = treeNode.path;
1105
+ expandPath(path);
1091
1106
 
1092
1107
  // Close the modal
1093
1108
  setIsSearchModalShown(false);
1094
1109
  }}
1095
1110
  />
1096
1111
  </Column>
1097
- </Modal> */}
1112
+ </Modal>
1098
1113
  </>;
1099
1114
 
1100
1115
  }
1101
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
+
1102
1137
  export const SideTreeEditor = withAlert(
1103
1138
  withEvents(
1104
1139
  withData(
@@ -1139,4 +1174,4 @@ export const WindowedTreeEditor = withAlert(
1139
1174
  )
1140
1175
  );
1141
1176
 
1142
- export default WindowedTreeEditor;
1177
+ export default Tree;
@@ -3,6 +3,7 @@ import {
3
3
  Box,
4
4
  Icon,
5
5
  Row,
6
+ Spinner,
6
7
  Text,
7
8
  } from 'native-base';
8
9
  import {
@@ -17,7 +18,7 @@ import _ from 'lodash';
17
18
 
18
19
  export default function TreeNode(props) {
19
20
  const {
20
- nodeProps,
21
+ nodeProps = {},
21
22
  bg,
22
23
  datum,
23
24
  onToggle,
@@ -27,6 +28,7 @@ export default function TreeNode(props) {
27
28
  item = datum.item,
28
29
  isPhantom = item.isPhantom,
29
30
  isExpanded = datum.isExpanded,
31
+ isLoading = datum.isLoading,
30
32
  hasChildren = item.hasChildren,
31
33
  depth = item.depth,
32
34
  text = datum.text,
@@ -45,11 +47,12 @@ export default function TreeNode(props) {
45
47
  {...nodeProps}
46
48
  bg={bg}
47
49
  key={hash}
48
- pl={(depth * 10) + 'px'}
49
50
  >
50
51
  {isPhantom && <Box position="absolute" bg="#f00" h={2} w={2} t={0} l={0} />}
51
52
 
52
- {hasChildren ? <IconButton icon={icon} onPress={() => onToggle(datum)} /> : <Icon as={icon} />}
53
+ {isLoading ?
54
+ <Spinner px={2} /> :
55
+ (hasChildren ? <IconButton icon={icon} onPress={() => onToggle(datum)} /> : <Icon as={icon} px={2} />)}
53
56
 
54
57
  <Text
55
58
  overflow="hidden"
@@ -77,6 +80,7 @@ export default function TreeNode(props) {
77
80
  text,
78
81
  icon,
79
82
  onToggle,
83
+ isLoading,
80
84
  ]);
81
85
  }
82
86