@onehat/ui 0.2.69 → 0.2.70
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
|
@@ -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
|
|
@@ -19,6 +19,7 @@ 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';
|
|
22
23
|
import * as colourMixer from '@k-renwick/colour-mixer'
|
|
23
24
|
import UiGlobals from '../../UiGlobals.js';
|
|
24
25
|
import useForceUpdate from '../../Hooks/useForceUpdate.js';
|
|
@@ -38,7 +39,7 @@ import TreeNode, { ReorderableTreeNode } from './TreeNode.js';
|
|
|
38
39
|
import FormPanel from '../Panel/FormPanel.js';
|
|
39
40
|
import Input from '../Form/Field/Input.js';
|
|
40
41
|
import IconButton from '../Buttons/IconButton.js';
|
|
41
|
-
import
|
|
42
|
+
import Dot from '../Icons/Dot.js';
|
|
42
43
|
import Collapse from '../Icons/Collapse.js';
|
|
43
44
|
import FolderClosed from '../Icons/FolderClosed.js';
|
|
44
45
|
import FolderOpen from '../Icons/FolderOpen.js';
|
|
@@ -51,28 +52,9 @@ import Toolbar from '../Toolbar/Toolbar.js';
|
|
|
51
52
|
import _ from 'lodash';
|
|
52
53
|
|
|
53
54
|
|
|
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
55
|
//////////////////////
|
|
60
56
|
//////////////////////
|
|
61
57
|
|
|
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
58
|
// Need to take into account whether using Repository or data.
|
|
77
59
|
// If using data, everything exists at once. What format will data be in?
|
|
78
60
|
// How does this interface with Repository?
|
|
@@ -82,20 +64,15 @@ import _ from 'lodash';
|
|
|
82
64
|
//////////////////////
|
|
83
65
|
|
|
84
66
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
67
|
export function Tree(props) {
|
|
90
68
|
const {
|
|
91
69
|
areRootsVisible = true,
|
|
92
|
-
|
|
93
|
-
return {};
|
|
94
|
-
},
|
|
70
|
+
extraParams = {}, // Additional params to send with each request ( e.g. { order: 'Categories.name ASC' })
|
|
95
71
|
getNodeText = (item) => { // extracts model/data and decides what the row text should be
|
|
96
72
|
return item.displayValue;
|
|
97
73
|
},
|
|
98
74
|
getNodeIcon = (item, isExpanded) => { // decides what icon to show for this node
|
|
75
|
+
// TODO: Allow for dynamic props on the icon (e.g. special color for some icons)
|
|
99
76
|
let icon;
|
|
100
77
|
if (item.hasChildren) {
|
|
101
78
|
if (isExpanded) {
|
|
@@ -104,7 +81,7 @@ export function Tree(props) {
|
|
|
104
81
|
icon = FolderClosed;
|
|
105
82
|
}
|
|
106
83
|
} else {
|
|
107
|
-
icon =
|
|
84
|
+
icon = Dot;
|
|
108
85
|
}
|
|
109
86
|
return icon;
|
|
110
87
|
},
|
|
@@ -169,6 +146,10 @@ export function Tree(props) {
|
|
|
169
146
|
[dragNodeIx, setDragNodeIx] = useState(),
|
|
170
147
|
[treeSearchValue, setTreeSearchValue] = useState(''),
|
|
171
148
|
onNodeClick = (item, e) => {
|
|
149
|
+
if (!setSelection) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
172
153
|
const
|
|
173
154
|
{
|
|
174
155
|
shiftKey,
|
|
@@ -268,10 +249,15 @@ export function Tree(props) {
|
|
|
268
249
|
const items = _.map(buttons, getIconFromConfig);
|
|
269
250
|
|
|
270
251
|
items.unshift(<Input // Add text input to beginning of header items
|
|
271
|
-
key="
|
|
252
|
+
key="searchNodes"
|
|
272
253
|
flex={1}
|
|
273
|
-
placeholder="
|
|
254
|
+
placeholder="Find tree node"
|
|
274
255
|
onChangeText={(val) => setTreeSearchValue(val)}
|
|
256
|
+
onKeyPress={(e, value) => {
|
|
257
|
+
if (e.key === 'Enter') {
|
|
258
|
+
onSearchTree(value);
|
|
259
|
+
}
|
|
260
|
+
}}
|
|
275
261
|
value={treeSearchValue}
|
|
276
262
|
autoSubmit={false}
|
|
277
263
|
/>);
|
|
@@ -320,6 +306,7 @@ export function Tree(props) {
|
|
|
320
306
|
// renderTreeNode uses this to render the nodes.
|
|
321
307
|
const
|
|
322
308
|
isRoot = treeNode.isRoot,
|
|
309
|
+
children = buildTreeNodeData(treeNode.children), // recursively get data for children
|
|
323
310
|
datum = {
|
|
324
311
|
item: treeNode,
|
|
325
312
|
text: getNodeText(treeNode),
|
|
@@ -328,7 +315,8 @@ export function Tree(props) {
|
|
|
328
315
|
iconLeaf: getNodeIcon(treeNode),
|
|
329
316
|
isExpanded: isRoot, // all non-root treeNodes are not expanded by default
|
|
330
317
|
isVisible: isRoot ? areRootsVisible : true,
|
|
331
|
-
|
|
318
|
+
isLoading: false,
|
|
319
|
+
children,
|
|
332
320
|
};
|
|
333
321
|
|
|
334
322
|
return datum;
|
|
@@ -386,7 +374,11 @@ export function Tree(props) {
|
|
|
386
374
|
e.preventDefault();
|
|
387
375
|
}
|
|
388
376
|
if (isReorderMode) {
|
|
389
|
-
return
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (!setSelection) {
|
|
381
|
+
return;
|
|
390
382
|
}
|
|
391
383
|
|
|
392
384
|
// context menu
|
|
@@ -450,78 +442,87 @@ export function Tree(props) {
|
|
|
450
442
|
</Pressable>;
|
|
451
443
|
},
|
|
452
444
|
renderTreeNodes = (data) => {
|
|
453
|
-
const nodes = [];
|
|
454
|
-
_.each(data, (datum) => {
|
|
455
|
-
nodes.push(renderTreeNode(datum));
|
|
456
|
-
});
|
|
457
|
-
return nodes;
|
|
458
|
-
},
|
|
459
|
-
renderAllTreeNodes = () => {
|
|
460
445
|
let nodes = [];
|
|
461
|
-
_.each(
|
|
446
|
+
_.each(data, (datum) => {
|
|
462
447
|
const node = renderTreeNode(datum);
|
|
463
|
-
if (_.isEmpty(node)) {
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
448
|
nodes.push(node);
|
|
468
449
|
|
|
469
|
-
if (
|
|
470
|
-
|
|
450
|
+
if (datum.children.length && datum.isExpanded) {
|
|
451
|
+
const childTreeNodes = renderTreeNodes(datum.children); // recursion
|
|
452
|
+
nodes = nodes.concat(childTreeNodes);
|
|
471
453
|
}
|
|
472
|
-
|
|
473
|
-
if (_.isEmpty(datum.children)) {
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
const children = renderTreeNodes(datum.children);
|
|
478
|
-
if (_.isEmpty(children)) {
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
nodes = nodes.concat(children);
|
|
483
454
|
});
|
|
484
455
|
return nodes;
|
|
485
456
|
},
|
|
457
|
+
getDatumChildIds = (datum) => {
|
|
458
|
+
let ids = [];
|
|
459
|
+
_.each(datum.children, (childDatum) => {
|
|
460
|
+
ids.push(childDatum.item.id);
|
|
461
|
+
if (childDatum.children.length) {
|
|
462
|
+
const childIds = getDatumChildIds(childDatum);
|
|
463
|
+
ids = ids.concat(childIds);
|
|
464
|
+
const t = true;
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
return ids;
|
|
468
|
+
},
|
|
469
|
+
datumContainsSelection = (datum) => {
|
|
470
|
+
if (_.isEmpty(selection)) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
const
|
|
474
|
+
selectionIds = _.map(selection, (item) => item.id),
|
|
475
|
+
datumIds = getDatumChildIds(datum),
|
|
476
|
+
intersection = selectionIds.filter(x => datumIds.includes(x));
|
|
477
|
+
|
|
478
|
+
return !_.isEmpty(intersection);
|
|
479
|
+
},
|
|
486
480
|
|
|
487
481
|
// Button handlers
|
|
488
482
|
onToggle = (datum) => {
|
|
483
|
+
if (datum.isLoading) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
489
487
|
datum.isExpanded = !datum.isExpanded;
|
|
490
|
-
forceUpdate();
|
|
491
488
|
|
|
492
|
-
if (datum.isExpanded && datum.item?.
|
|
489
|
+
if (datum.isExpanded && datum.item.repository?.isRemote && datum.item.hasChildren && !datum.item.areChildrenLoaded) {
|
|
493
490
|
loadChildren(datum, 1);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (!datum.isExpanded && datumContainsSelection(datum)) {
|
|
495
|
+
deselectAll();
|
|
494
496
|
}
|
|
497
|
+
|
|
498
|
+
forceUpdate();
|
|
495
499
|
},
|
|
496
500
|
loadChildren = async (datum, depth) => {
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
// Show loading indicator (red bar at top? Spinner underneath current node?)
|
|
501
|
-
|
|
501
|
+
// Show loading indicator (spinner underneath current node?)
|
|
502
|
+
datum.isLoading = true;
|
|
503
|
+
forceUpdate();
|
|
502
504
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
505
|
+
try {
|
|
506
|
+
|
|
507
|
+
const children = await datum.item.loadChildren(1);
|
|
508
|
+
const tnd = buildTreeNodeData(children);
|
|
509
|
+
datum.children = tnd;
|
|
510
|
+
|
|
511
|
+
} catch (err) {
|
|
512
|
+
// TODO: how do I handle errors?
|
|
513
|
+
// Color parent node red
|
|
514
|
+
// Modal alert box?
|
|
515
|
+
// Inline error msg? I'm concerned about modals not stacking correctly, but if we put it inline, it'll work.
|
|
516
|
+
datum.isExpanded = false;
|
|
517
|
+
}
|
|
507
518
|
|
|
508
519
|
// Hide loading indicator
|
|
509
|
-
|
|
520
|
+
datum.isLoading = false;
|
|
521
|
+
forceUpdate();
|
|
510
522
|
},
|
|
511
523
|
onCollapseAll = (setNewTreeNodeData = true) => {
|
|
512
524
|
// Go through whole tree and collapse all nodes
|
|
513
525
|
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
526
|
collapseNodes(newTreeNodeData);
|
|
526
527
|
|
|
527
528
|
if (setNewTreeNodeData) {
|
|
@@ -529,47 +530,37 @@ export function Tree(props) {
|
|
|
529
530
|
}
|
|
530
531
|
return newTreeNodeData;
|
|
531
532
|
},
|
|
533
|
+
collapseNodes = (nodes) => {
|
|
534
|
+
_.each(nodes, (node) => {
|
|
535
|
+
node.isExpanded = false;
|
|
536
|
+
if (!_.isEmpty(node.children)) {
|
|
537
|
+
collapseNodes(node.children);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
},
|
|
532
541
|
onSearchTree = async (value) => {
|
|
533
542
|
|
|
534
543
|
let found = [];
|
|
535
544
|
if (Repository?.isRemote) {
|
|
536
545
|
// Search tree on server
|
|
537
|
-
found = await Repository.
|
|
546
|
+
found = await Repository.searchNodes(value);
|
|
538
547
|
} else {
|
|
539
548
|
// Search local tree data
|
|
540
549
|
found = findTreeNodesByText(value);
|
|
541
550
|
}
|
|
542
551
|
|
|
543
|
-
|
|
544
552
|
const isMultipleHits = found.length > 1;
|
|
545
553
|
let path = '';
|
|
546
554
|
let searchFormData = [];
|
|
547
555
|
|
|
548
|
-
if (
|
|
549
|
-
|
|
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
|
|
556
|
+
if (!isMultipleHits) {
|
|
557
|
+
path = found[0].path;
|
|
571
558
|
expandPath(path);
|
|
559
|
+
return;
|
|
572
560
|
}
|
|
561
|
+
|
|
562
|
+
setSearchFormData(searchFormData);
|
|
563
|
+
setIsSearchModalShown(true);
|
|
573
564
|
},
|
|
574
565
|
findTreeNodesByText = (text) => {
|
|
575
566
|
// Helper for onSearchTree
|
|
@@ -624,14 +615,12 @@ export function Tree(props) {
|
|
|
624
615
|
return nodes.join('/');
|
|
625
616
|
|
|
626
617
|
},
|
|
627
|
-
expandPath = (path) => {
|
|
618
|
+
expandPath = async (path) => {
|
|
628
619
|
// Helper for onSearchTree
|
|
629
620
|
|
|
630
|
-
//
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
let newTreeNodeData = collapseAll(false); // false = don't set new treeNodeData
|
|
621
|
+
// First, close thw whole tree.
|
|
622
|
+
let newTreeNodeData = _.clone(treeNodeData);
|
|
623
|
+
collapseNodes(newTreeNodeData);
|
|
635
624
|
|
|
636
625
|
// As it navigates down, it will expand the appropriate branches,
|
|
637
626
|
// and then finally highlight & select the node in question
|
|
@@ -649,6 +638,13 @@ export function Tree(props) {
|
|
|
649
638
|
currentDatum = _.find(currentLevelData, (treeNodeDatum) => {
|
|
650
639
|
return treeNodeDatum.item.id === id;
|
|
651
640
|
});
|
|
641
|
+
|
|
642
|
+
if (!currentDatum) {
|
|
643
|
+
// datum is not currently loaded, so load it
|
|
644
|
+
|
|
645
|
+
// LEFT OFF HERE
|
|
646
|
+
debugger;
|
|
647
|
+
}
|
|
652
648
|
|
|
653
649
|
currentNode = currentDatum.item;
|
|
654
650
|
|
|
@@ -935,7 +931,7 @@ export function Tree(props) {
|
|
|
935
931
|
let rootNodes;
|
|
936
932
|
if (Repository) {
|
|
937
933
|
if (!Repository.areRootNodesLoaded) {
|
|
938
|
-
rootNodes = await Repository.getRootNodes(
|
|
934
|
+
rootNodes = await Repository.getRootNodes(1);
|
|
939
935
|
}
|
|
940
936
|
} else {
|
|
941
937
|
// TODO: Make this work for data array
|
|
@@ -946,7 +942,15 @@ export function Tree(props) {
|
|
|
946
942
|
setTreeNodeData(treeNodeData);
|
|
947
943
|
}
|
|
948
944
|
|
|
945
|
+
function reloadTreeData() {
|
|
946
|
+
Repository.areRootNodesLoaded = false;
|
|
947
|
+
return buildAndSetTreeNodeData();
|
|
948
|
+
}
|
|
949
|
+
|
|
949
950
|
if (!isReady) {
|
|
951
|
+
if (Repository) {
|
|
952
|
+
Repository.setBaseParams(extraParams);
|
|
953
|
+
}
|
|
950
954
|
(async () => {
|
|
951
955
|
await buildAndSetTreeNodeData();
|
|
952
956
|
setIsReady(true);
|
|
@@ -956,29 +960,18 @@ export function Tree(props) {
|
|
|
956
960
|
if (!Repository) {
|
|
957
961
|
return () => {};
|
|
958
962
|
}
|
|
959
|
-
|
|
963
|
+
|
|
960
964
|
// set up @onehat/data repository
|
|
961
965
|
const
|
|
962
966
|
setTrue = () => setIsLoading(true),
|
|
963
|
-
setFalse = () => setIsLoading(false)
|
|
964
|
-
|
|
965
|
-
if (!Repository.isAutoLoad) {
|
|
966
|
-
Repository.reload();
|
|
967
|
-
}
|
|
968
|
-
},
|
|
969
|
-
onChangeSorters = () => {
|
|
970
|
-
if (!Repository.isAutoLoad) {
|
|
971
|
-
Repository.reload();
|
|
972
|
-
}
|
|
973
|
-
};
|
|
974
|
-
|
|
967
|
+
setFalse = () => setIsLoading(false);
|
|
968
|
+
|
|
975
969
|
Repository.on('beforeLoad', setTrue);
|
|
976
970
|
Repository.on('load', setFalse);
|
|
977
971
|
Repository.ons(['changePage', 'changePageSize',], deselectAll);
|
|
978
972
|
Repository.ons(['changeData', 'change'], buildAndSetTreeNodeData);
|
|
979
|
-
Repository.on('changeFilters',
|
|
980
|
-
Repository.on('changeSorters',
|
|
981
|
-
|
|
973
|
+
Repository.on('changeFilters', reloadTreeData);
|
|
974
|
+
Repository.on('changeSorters', reloadTreeData);
|
|
982
975
|
|
|
983
976
|
return () => {
|
|
984
977
|
Repository.off('beforeLoad', setTrue);
|
|
@@ -1011,7 +1004,7 @@ export function Tree(props) {
|
|
|
1011
1004
|
if (!isReady) {
|
|
1012
1005
|
return null;
|
|
1013
1006
|
}
|
|
1014
|
-
const treeNodes =
|
|
1007
|
+
const treeNodes = renderTreeNodes(treeNodeData);
|
|
1015
1008
|
|
|
1016
1009
|
// headers & footers
|
|
1017
1010
|
let treeFooterComponent = null;
|
|
@@ -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
|
-
{
|
|
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
|
|