@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 +1 -1
- package/src/Components/Grid/Grid.js +24 -2
- package/src/Components/Hoc/withEditor.js +2 -1
- package/src/Components/Hoc/withPresetButtons.js +4 -3
- package/src/Components/Hoc/withSideEditor.js +4 -0
- package/src/Components/Hoc/withWindowedEditor.js +4 -0
- package/src/Components/Icons/Dot.js +18 -0
- package/src/Components/Panel/InlineGridEditorPanel.js +39 -0
- package/src/Components/Panel/SideGridEditorPanel.js +39 -0
- package/src/Components/Panel/SideTreeEditorPanel.js +39 -0
- package/src/Components/Panel/WindowedGridEditorPanel.js +39 -0
- package/src/Components/Panel/WindowedTreeEditorPanel.js +39 -0
- package/src/Components/Tree/Tree.js +227 -192
- package/src/Components/Tree/TreeNode.js +7 -3
package/package.json
CHANGED
|
@@ -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
|
|
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 =
|
|
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 =
|
|
38
|
-
disableEdit =
|
|
39
|
-
disableDelete =
|
|
38
|
+
disableAdd = !isEditor,
|
|
39
|
+
disableEdit = !isEditor,
|
|
40
|
+
disableDelete = !isEditor,
|
|
40
41
|
disableView = !isGrid,
|
|
41
42
|
disableCopy = !isGrid,
|
|
42
43
|
disableDuplicate = !isGrid,
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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="
|
|
257
|
+
key="searchNodes"
|
|
272
258
|
flex={1}
|
|
273
|
-
placeholder="
|
|
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
|
-
|
|
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
|
-
|
|
450
|
+
let nodes = [];
|
|
454
451
|
_.each(data, (datum) => {
|
|
455
|
-
|
|
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
|
-
|
|
460
|
-
let
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (
|
|
464
|
-
|
|
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
|
-
|
|
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
|
-
|
|
470
|
-
|
|
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
|
-
|
|
474
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
537
|
+
if (treeNode.isRoot) {
|
|
538
|
+
treeNodes.push(treeNode);
|
|
539
|
+
}
|
|
483
540
|
});
|
|
484
|
-
|
|
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?.
|
|
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
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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.
|
|
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
|
-
|
|
546
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
934
|
-
|
|
935
|
-
let rootNodes;
|
|
981
|
+
if (!isReady) {
|
|
936
982
|
if (Repository) {
|
|
937
|
-
|
|
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
|
-
|
|
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',
|
|
980
|
-
Repository.on('changeSorters',
|
|
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',
|
|
989
|
-
Repository.off('changeSorters',
|
|
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 =
|
|
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
|
-
|
|
1071
|
+
<Modal
|
|
1048
1072
|
isOpen={isSearchModalShown}
|
|
1049
1073
|
onClose={() => setIsSearchModalShown(false)}
|
|
1050
1074
|
>
|
|
1051
|
-
<Column bg="#fff" w={
|
|
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
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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
|
|
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
|
-
{
|
|
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
|
|