@onehat/ui 0.2.57 → 0.2.59
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/cypress/plugins/index.js +2 -0
- package/cypress/support/commands.js +2 -0
- package/cypress/support/e2e.js +2 -0
- package/cypress.config.js +41 -0
- package/package.json +34 -25
- package/src/Components/Form/Field/CKEditor/CKEditor.js +1 -1
- package/src/Components/Form/Form.js +1 -1
- package/src/Components/Grid/Grid.js +12 -5
- package/src/Components/Grid/GridHeaderRow.js +5 -2
- package/src/Components/Grid/GridRow.js +2 -0
- package/src/Components/Hoc/withContextMenu.js +1 -1
- package/src/Components/Icons/BarsStaggered.js +14 -0
- package/src/Components/Icons/Circle.js +14 -0
- package/src/Components/Icons/Folder.js +14 -0
- package/src/Components/Icons/FolderClosed.js +14 -0
- package/src/Components/Icons/FolderOpen.js +14 -0
- package/src/Components/Icons/FolderTree.js +14 -0
- package/src/Components/Icons/Leaf.js +14 -0
- package/src/Components/Icons/MagnifyingGlass.js +14 -0
- package/src/Components/Icons/collapse.js +17 -0
- package/src/Components/Tree/Tree.js +1143 -0
- package/src/Components/Tree/TreeNode.js +94 -0
- package/src/Constants/Styles.js +14 -0
- package/src/Functions/BankersRound.js +1 -1
|
@@ -0,0 +1,1143 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef, useMemo, } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Column,
|
|
4
|
+
FlatList,
|
|
5
|
+
Modal,
|
|
6
|
+
Pressable,
|
|
7
|
+
Icon,
|
|
8
|
+
Row,
|
|
9
|
+
Text,
|
|
10
|
+
} from 'native-base';
|
|
11
|
+
import {
|
|
12
|
+
SELECTION_MODE_SINGLE,
|
|
13
|
+
SELECTION_MODE_MULTI,
|
|
14
|
+
} from '../../Constants/Selection.js';
|
|
15
|
+
import {
|
|
16
|
+
VERTICAL,
|
|
17
|
+
} from '../../Constants/Directions.js';
|
|
18
|
+
import {
|
|
19
|
+
DROP_POSITION_BEFORE,
|
|
20
|
+
DROP_POSITION_AFTER,
|
|
21
|
+
} from '../../Constants/Tree.js';
|
|
22
|
+
import * as colourMixer from '@k-renwick/colour-mixer'
|
|
23
|
+
import UiGlobals from '../../UiGlobals.js';
|
|
24
|
+
import useForceUpdate from '../../Hooks/useForceUpdate.js';
|
|
25
|
+
import withContextMenu from '../Hoc/withContextMenu.js';
|
|
26
|
+
import withAlert from '../Hoc/withAlert.js';
|
|
27
|
+
import withData from '../Hoc/withData.js';
|
|
28
|
+
import withEvents from '../Hoc/withEvents.js';
|
|
29
|
+
import withSideEditor from '../Hoc/withSideEditor.js';
|
|
30
|
+
import withFilters from '../Hoc/withFilters.js';
|
|
31
|
+
import withPresetButtons from '../Hoc/withPresetButtons.js';
|
|
32
|
+
import withMultiSelection from '../Hoc/withMultiSelection.js';
|
|
33
|
+
import withSelection from '../Hoc/withSelection.js';
|
|
34
|
+
import withWindowedEditor from '../Hoc/withWindowedEditor.js';
|
|
35
|
+
import testProps from '../../Functions/testProps.js';
|
|
36
|
+
import nbToRgb from '../../Functions/nbToRgb.js';
|
|
37
|
+
import TreeNode, { ReorderableTreeNode } from './TreeNode.js';
|
|
38
|
+
import FormPanel from '../Panel/FormPanel.js';
|
|
39
|
+
import Input from '../Form/Field/Input.js';
|
|
40
|
+
import IconButton from '../Buttons/IconButton.js';
|
|
41
|
+
import Circle from '../Icons/Circle.js';
|
|
42
|
+
import Collapse from '../Icons/Collapse.js';
|
|
43
|
+
import FolderClosed from '../Icons/FolderClosed.js';
|
|
44
|
+
import FolderOpen from '../Icons/FolderOpen.js';
|
|
45
|
+
import MagnifyingGlass from '../Icons/MagnifyingGlass.js';
|
|
46
|
+
import NoReorderRows from '../Icons/NoReorderRows.js';
|
|
47
|
+
import ReorderRows from '../Icons/ReorderRows.js';
|
|
48
|
+
import PaginationToolbar from '../Toolbar/PaginationToolbar.js';
|
|
49
|
+
import NoRecordsFound from './NoRecordsFound.js';
|
|
50
|
+
import Toolbar from '../Toolbar/Toolbar.js';
|
|
51
|
+
import _ from 'lodash';
|
|
52
|
+
|
|
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
|
+
//////////////////////
|
|
60
|
+
//////////////////////
|
|
61
|
+
|
|
62
|
+
// I'm thinking if a repository senses that it's a tree, then at initial load
|
|
63
|
+
// it should get the root node +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 root+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
|
+
// Need to take into account whether using Repository or data.
|
|
77
|
+
// If using data, everything exists at once. What format will data be in?
|
|
78
|
+
// How does this interface with Repository?
|
|
79
|
+
// Maybe if Repository is not AjaxRepository, everything needs to be present at once!
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
// isRootVisible
|
|
83
|
+
|
|
84
|
+
//////////////////////
|
|
85
|
+
//////////////////////
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
export function Tree(props) {
|
|
93
|
+
const {
|
|
94
|
+
isRootVisible = true,
|
|
95
|
+
getAdditionalParams = () => { // URL params needed to get nodes from server (e.g, { venue_id: 1, getEquipment: true, getRentalEquipment: false, }), in addition to filters.
|
|
96
|
+
return {};
|
|
97
|
+
},
|
|
98
|
+
getNodeText = (item) => { // extracts model/data and decides what the row text should be
|
|
99
|
+
return item.displayValue;
|
|
100
|
+
},
|
|
101
|
+
getNodeIcon = (item, isExpanded) => { // decides what icon to show for this node
|
|
102
|
+
let icon;
|
|
103
|
+
if (item.hasChildren) {
|
|
104
|
+
if (isExpanded) {
|
|
105
|
+
icon = FolderOpen;
|
|
106
|
+
} else {
|
|
107
|
+
icon = FolderClosed;
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
icon = Circle;
|
|
111
|
+
}
|
|
112
|
+
return icon;
|
|
113
|
+
},
|
|
114
|
+
nodeProps = (item) => {
|
|
115
|
+
return {};
|
|
116
|
+
},
|
|
117
|
+
noneFoundText,
|
|
118
|
+
disableLoadingIndicator = false,
|
|
119
|
+
disableSelectorSelected = false,
|
|
120
|
+
showHovers = true,
|
|
121
|
+
canNodesReorder = false,
|
|
122
|
+
allowToggleSelection = true, // i.e. single click with no shift key toggles the selection of the node clicked on
|
|
123
|
+
disableBottomToolbar = false,
|
|
124
|
+
bottomToolbar = null,
|
|
125
|
+
topToolbar = null,
|
|
126
|
+
additionalToolbarButtons = [],
|
|
127
|
+
|
|
128
|
+
// withEditor
|
|
129
|
+
onAdd,
|
|
130
|
+
onEdit,
|
|
131
|
+
onDelete,
|
|
132
|
+
onView,
|
|
133
|
+
onDuplicate,
|
|
134
|
+
onReset,
|
|
135
|
+
onContextMenu,
|
|
136
|
+
|
|
137
|
+
// withData
|
|
138
|
+
Repository,
|
|
139
|
+
data,
|
|
140
|
+
fields,
|
|
141
|
+
idField,
|
|
142
|
+
displayField,
|
|
143
|
+
idIx,
|
|
144
|
+
displayIx,
|
|
145
|
+
|
|
146
|
+
// withSelection
|
|
147
|
+
selection,
|
|
148
|
+
setSelection,
|
|
149
|
+
selectionMode,
|
|
150
|
+
removeFromSelection,
|
|
151
|
+
addToSelection,
|
|
152
|
+
deselectAll,
|
|
153
|
+
selectRangeTo,
|
|
154
|
+
isInSelection,
|
|
155
|
+
noSelectorMeansNoResults = false,
|
|
156
|
+
|
|
157
|
+
// DataMgt
|
|
158
|
+
selectorId,
|
|
159
|
+
selectorSelected,
|
|
160
|
+
|
|
161
|
+
} = props,
|
|
162
|
+
styles = UiGlobals.styles,
|
|
163
|
+
forceUpdate = useForceUpdate(),
|
|
164
|
+
treeRef = useRef(),
|
|
165
|
+
[isReady, setIsReady] = useState(false),
|
|
166
|
+
[isLoading, setIsLoading] = useState(false),
|
|
167
|
+
[isReorderMode, setIsReorderMode] = useState(false),
|
|
168
|
+
[isSearchModalShown, setIsSearchModalShown] = useState(false),
|
|
169
|
+
[treeNodeData, setTreeNodeData] = useState({}),
|
|
170
|
+
[searchFormData, setSearchFormData] = useState([]),
|
|
171
|
+
[dragNodeSlot, setDragNodeSlot] = useState(null),
|
|
172
|
+
[dragNodeIx, setDragNodeIx] = useState(),
|
|
173
|
+
onNodeClick = (item, e) => {
|
|
174
|
+
const
|
|
175
|
+
{
|
|
176
|
+
shiftKey,
|
|
177
|
+
metaKey,
|
|
178
|
+
} = e;
|
|
179
|
+
|
|
180
|
+
if (selectionMode === SELECTION_MODE_MULTI) {
|
|
181
|
+
if (shiftKey) {
|
|
182
|
+
if (isInSelection(item)) {
|
|
183
|
+
removeFromSelection(item);
|
|
184
|
+
} else {
|
|
185
|
+
selectRangeTo(item);
|
|
186
|
+
}
|
|
187
|
+
} else if (metaKey) {
|
|
188
|
+
if (isInSelection(item)) {
|
|
189
|
+
// Already selected
|
|
190
|
+
if (allowToggleSelection) {
|
|
191
|
+
removeFromSelection(item);
|
|
192
|
+
} else {
|
|
193
|
+
// Do nothing.
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
addToSelection(item);
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
if (isInSelection(item)) {
|
|
200
|
+
// Already selected
|
|
201
|
+
if (allowToggleSelection) {
|
|
202
|
+
removeFromSelection(item);
|
|
203
|
+
} else {
|
|
204
|
+
// Do nothing.
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
// select just this one
|
|
208
|
+
setSelection([item]);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
// selectionMode is SELECTION_MODE_SINGLE
|
|
213
|
+
let newSelection = selection;
|
|
214
|
+
if (isInSelection(item)) {
|
|
215
|
+
// Already selected
|
|
216
|
+
if (allowToggleSelection) {
|
|
217
|
+
// Create empty selection
|
|
218
|
+
newSelection = [];
|
|
219
|
+
} else {
|
|
220
|
+
// Do nothing.
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
// Select it alone
|
|
224
|
+
newSelection = [item];
|
|
225
|
+
}
|
|
226
|
+
if (newSelection) {
|
|
227
|
+
setSelection(newSelection);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
onRefresh = () => {
|
|
232
|
+
if (!Repository) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const promise = Repository.reload();
|
|
236
|
+
if (promise) { // Some repository types don't use promises
|
|
237
|
+
promise.then(() => {
|
|
238
|
+
setIsLoading(false);
|
|
239
|
+
forceUpdate();
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
getHeaderToolbarItems = () => {
|
|
244
|
+
const
|
|
245
|
+
buttons = [
|
|
246
|
+
|
|
247
|
+
{
|
|
248
|
+
key: 'searchBtn',
|
|
249
|
+
text: 'Search tree',
|
|
250
|
+
handler: onSearchTree,
|
|
251
|
+
icon: MagnifyingGlass,
|
|
252
|
+
isDisabled: false,
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
key: 'collapseBtn',
|
|
256
|
+
text: 'Collapse whole tree',
|
|
257
|
+
handler: onCollapseAll,
|
|
258
|
+
icon: Collapse,
|
|
259
|
+
isDisabled: false,
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
if (canNodesReorder) {
|
|
263
|
+
buttons.push({
|
|
264
|
+
key: 'reorderBtn',
|
|
265
|
+
text: 'Reorder tree',
|
|
266
|
+
handler: () => setIsReorderMode(!isReorderMode),
|
|
267
|
+
icon: isReorderMode ? NoReorderRows : ReorderRows,
|
|
268
|
+
isDisabled: false,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
const items = _.map(buttons, getIconFromConfig);
|
|
272
|
+
|
|
273
|
+
items.unshift(<Input // Add text input to beginning of header items
|
|
274
|
+
key="searchTree"
|
|
275
|
+
flex={1}
|
|
276
|
+
placeholder="Search all tree nodes"
|
|
277
|
+
onChangeValue={onSearchTree}
|
|
278
|
+
autoSubmit={false}
|
|
279
|
+
/>);
|
|
280
|
+
|
|
281
|
+
return items;
|
|
282
|
+
},
|
|
283
|
+
getFooterToolbarItems = () => {
|
|
284
|
+
return _.map(additionalToolbarButtons, getIconFromConfig);
|
|
285
|
+
},
|
|
286
|
+
getIconFromConfig = (config, ix) => {
|
|
287
|
+
const
|
|
288
|
+
iconButtonProps = {
|
|
289
|
+
_hover: {
|
|
290
|
+
bg: 'trueGray.400',
|
|
291
|
+
},
|
|
292
|
+
mx: 1,
|
|
293
|
+
px: 3,
|
|
294
|
+
},
|
|
295
|
+
iconProps = {
|
|
296
|
+
alignSelf: 'center',
|
|
297
|
+
size: styles.TREE_TOOLBAR_ITEMS_ICON_SIZE,
|
|
298
|
+
h: 20,
|
|
299
|
+
w: 20,
|
|
300
|
+
};
|
|
301
|
+
let {
|
|
302
|
+
key,
|
|
303
|
+
text,
|
|
304
|
+
handler,
|
|
305
|
+
icon = null,
|
|
306
|
+
isDisabled = false,
|
|
307
|
+
} = config;
|
|
308
|
+
if (icon) {
|
|
309
|
+
const thisIconProps = {
|
|
310
|
+
color: isDisabled ? styles.TREE_TOOLBAR_ITEMS_DISABLED_COLOR : styles.TREE_TOOLBAR_ITEMS_COLOR,
|
|
311
|
+
};
|
|
312
|
+
icon = React.cloneElement(icon, {...iconProps, ...thisIconProps});
|
|
313
|
+
}
|
|
314
|
+
return <IconButton
|
|
315
|
+
key={key || ix}
|
|
316
|
+
onPress={handler}
|
|
317
|
+
icon={icon}
|
|
318
|
+
isDisabled={isDisabled}
|
|
319
|
+
tooltip={text}
|
|
320
|
+
{...iconButtonProps}
|
|
321
|
+
/>;
|
|
322
|
+
},
|
|
323
|
+
buildTreeNodeDatum = (treeNode) => {
|
|
324
|
+
// Build the data-representation of one node and its children,
|
|
325
|
+
// caching text & icon, keeping track of the state for whole tree
|
|
326
|
+
// renderTreeNode uses this to render the nodes.
|
|
327
|
+
const
|
|
328
|
+
isRoot = treeNode.isRoot,
|
|
329
|
+
isLeaf = !treeNode.hasChildren,
|
|
330
|
+
datum = {
|
|
331
|
+
item: treeNode,
|
|
332
|
+
text: getNodeText(treeNode),
|
|
333
|
+
iconCollapsed: isLeaf ? null : getNodeIcon(treeNode, false),
|
|
334
|
+
iconExpanded: isLeaf ? null : getNodeIcon(treeNode, true),
|
|
335
|
+
iconLeaf: isLeaf ? getNodeIcon(treeNode) : null,
|
|
336
|
+
isExpanded: isRoot, // all non-root treeNodes are not expanded by default
|
|
337
|
+
isVisible: isRoot ? isRootVisible : true,
|
|
338
|
+
children: buildTreeNodeData(treeNode.children), // recursively get data for children
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
return datum;
|
|
342
|
+
},
|
|
343
|
+
buildTreeNodeData = (treeNodes) => {
|
|
344
|
+
const data = [];
|
|
345
|
+
_.each(treeNodes, (item) => {
|
|
346
|
+
data.push(buildTreeNodeDatum(item));
|
|
347
|
+
});
|
|
348
|
+
return data;
|
|
349
|
+
},
|
|
350
|
+
renderTreeNode = (datum) => {
|
|
351
|
+
const item = datum.item;
|
|
352
|
+
if (item.isDestroyed) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
if (!datum.isVisible) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
let nodeProps = getNodeProps ? getNodeProps(item) : {},
|
|
360
|
+
isSelected = isInSelection(item);
|
|
361
|
+
|
|
362
|
+
return <Pressable
|
|
363
|
+
// {...testProps(Repository ? Repository.schema.name + '-' + item.id : item.id)}
|
|
364
|
+
key={item.hash}
|
|
365
|
+
onPress={(e) => {
|
|
366
|
+
if (e.preventDefault && e.cancelable) {
|
|
367
|
+
e.preventDefault();
|
|
368
|
+
}
|
|
369
|
+
if (isReorderMode) {
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
switch (e.detail) {
|
|
373
|
+
case 1: // single click
|
|
374
|
+
onNodeClick(item, e); // sets selection
|
|
375
|
+
break;
|
|
376
|
+
case 2: // double click
|
|
377
|
+
if (!isSelected) { // If a row was already selected when double-clicked, the first click will deselect it,
|
|
378
|
+
onNodeClick(item, e); // so reselect it
|
|
379
|
+
}
|
|
380
|
+
if (onEdit) {
|
|
381
|
+
onEdit();
|
|
382
|
+
}
|
|
383
|
+
break;
|
|
384
|
+
case 3: // triple click
|
|
385
|
+
break;
|
|
386
|
+
default:
|
|
387
|
+
}
|
|
388
|
+
}}
|
|
389
|
+
onLongPress={(e) => {
|
|
390
|
+
if (e.preventDefault && e.cancelable) {
|
|
391
|
+
e.preventDefault();
|
|
392
|
+
}
|
|
393
|
+
if (isReorderMode) {
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// context menu
|
|
398
|
+
const selection = [item];
|
|
399
|
+
setSelection(selection);
|
|
400
|
+
if (onContextMenu) {
|
|
401
|
+
onContextMenu(item, e, selection, setSelection);
|
|
402
|
+
}
|
|
403
|
+
}}
|
|
404
|
+
flexDirection="row"
|
|
405
|
+
flexGrow={1}
|
|
406
|
+
>
|
|
407
|
+
{({
|
|
408
|
+
isHovered,
|
|
409
|
+
isFocused,
|
|
410
|
+
isPressed,
|
|
411
|
+
}) => {
|
|
412
|
+
let bg = nodeProps.bg || styles.TREE_NODE_BG,
|
|
413
|
+
mixWith;
|
|
414
|
+
if (isSelected) {
|
|
415
|
+
if (showHovers && isHovered) {
|
|
416
|
+
mixWith = styles.TREE_NODE_SELECTED_HOVER_BG;
|
|
417
|
+
} else {
|
|
418
|
+
mixWith = styles.TREE_NODE_SELECTED_BG;
|
|
419
|
+
}
|
|
420
|
+
} else if (showHovers && isHovered) {
|
|
421
|
+
mixWith = styles.TREE_NODE_HOVER_BG;
|
|
422
|
+
}
|
|
423
|
+
if (mixWith) {
|
|
424
|
+
const
|
|
425
|
+
mixWithObj = nbToRgb(mixWith),
|
|
426
|
+
ratio = mixWithObj.alpha ? 1 - mixWithObj.alpha : 0.5;
|
|
427
|
+
bg = colourMixer.blend(bg, ratio, mixWithObj.color);
|
|
428
|
+
}
|
|
429
|
+
let WhichTreeNode = TreeNode,
|
|
430
|
+
rowReorderProps = {};
|
|
431
|
+
if (canNodesReorder && isReorderMode) {
|
|
432
|
+
WhichTreeNode = ReorderableTreeNode;
|
|
433
|
+
rowReorderProps = {
|
|
434
|
+
mode: VERTICAL,
|
|
435
|
+
onDragStart: onNodeReorderDragStart,
|
|
436
|
+
onDrag: onNodeReorderDrag,
|
|
437
|
+
onDragStop: onNodeReorderDragStop,
|
|
438
|
+
proxyParent: treeRef.current?.getScrollableNode().children[0],
|
|
439
|
+
proxyPositionRelativeToParent: true,
|
|
440
|
+
getParentNode: (node) => node.parentElement.parentElement.parentElement,
|
|
441
|
+
getProxy: getReorderProxy,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return <WhichTreeNode
|
|
446
|
+
nodeProps={nodeProps}
|
|
447
|
+
bg={bg}
|
|
448
|
+
datum={datum}
|
|
449
|
+
onToggle={onToggle}
|
|
450
|
+
|
|
451
|
+
// fields={fields}
|
|
452
|
+
{...rowReorderProps}
|
|
453
|
+
/>;
|
|
454
|
+
}}
|
|
455
|
+
</Pressable>;
|
|
456
|
+
},
|
|
457
|
+
renderTreeNodes = (data) => {
|
|
458
|
+
const nodes = [];
|
|
459
|
+
_.each(data, (datum) => {
|
|
460
|
+
nodes.push(renderTreeNode(datum));
|
|
461
|
+
});
|
|
462
|
+
return nodes;
|
|
463
|
+
},
|
|
464
|
+
renderAllTreeNodes = () => {
|
|
465
|
+
const nodes = [];
|
|
466
|
+
_.each(treeNodeData, (datum) => {
|
|
467
|
+
const node = renderTreeNode(datum);
|
|
468
|
+
if (_.isEmpty(node)) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
nodes.push(node);
|
|
473
|
+
|
|
474
|
+
if (_.isEmpty(datum.children)) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const children = renderTreeNodes(datum.children);
|
|
479
|
+
if (_.isEmpty(children)) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
nodes.concat(children);
|
|
484
|
+
});
|
|
485
|
+
return nodes;
|
|
486
|
+
},
|
|
487
|
+
|
|
488
|
+
// Button handlers
|
|
489
|
+
onToggle = (datum) => {
|
|
490
|
+
datum.isExpanded = !datum.isExpanded;
|
|
491
|
+
forceUpdate();
|
|
492
|
+
|
|
493
|
+
if (datum.item?.repository.isRemote && datum.item.hasChildren && !datum.item.isChildrenLoaded) {
|
|
494
|
+
loadChildren(datum, 1);
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
loadChildren = async (datum, depth) => {
|
|
498
|
+
// Helper for onToggle
|
|
499
|
+
|
|
500
|
+
// TODO: Flesh this out
|
|
501
|
+
// Show loading indicator (red bar at top? Spinner underneath current node?)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
// Calls getAdditionalParams(), then submits to server
|
|
505
|
+
// Server returns this for each node:
|
|
506
|
+
// Build up treeNodeData for just these new nodes
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
// Hide loading indicator
|
|
510
|
+
|
|
511
|
+
},
|
|
512
|
+
onCollapseAll = (setNewTreeNodeData = true) => {
|
|
513
|
+
// Go through whole tree and collapse all nodes
|
|
514
|
+
const newTreeNodeData = _.clone(treeNodeData);
|
|
515
|
+
|
|
516
|
+
// Recursive method to collapse all children
|
|
517
|
+
function collapseChildren(children) {
|
|
518
|
+
_.each(children, (child) => {
|
|
519
|
+
child.isExpanded = true;
|
|
520
|
+
if (!_.isEmpty(child.children)) {
|
|
521
|
+
collapseChildren(child.children);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
collapseChildren(newTreeNodeData);
|
|
527
|
+
|
|
528
|
+
if (setNewTreeNodeData) {
|
|
529
|
+
setTreeNodeData(newTreeNodeData);
|
|
530
|
+
}
|
|
531
|
+
return newTreeNodeData;
|
|
532
|
+
},
|
|
533
|
+
onSearchTree = async (value) => {
|
|
534
|
+
|
|
535
|
+
let found = [];
|
|
536
|
+
if (Repository?.isRemote) {
|
|
537
|
+
// Search tree on server
|
|
538
|
+
found = await Repository.searchTree(value);
|
|
539
|
+
} else {
|
|
540
|
+
// Search local tree data
|
|
541
|
+
found = findTreeNodesByText(value);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
const isMultipleHits = found.length > 1;
|
|
546
|
+
let path = '';
|
|
547
|
+
let searchFormData = [];
|
|
548
|
+
|
|
549
|
+
if (Repository?.isRemote) {
|
|
550
|
+
if (isMultipleHits) {
|
|
551
|
+
// 'found' is the results from the server. Use these to show the modal and choose which node you want to select
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
} else {
|
|
557
|
+
// Search local tree data
|
|
558
|
+
found = findTreeNodesByText(value);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// TODO: create searchFormData based on 'found' array
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
setSearchFormData(searchFormData);
|
|
568
|
+
setIsSearchModalShown(true);
|
|
569
|
+
|
|
570
|
+
} else {
|
|
571
|
+
// Expand that one path immediately
|
|
572
|
+
expandPath(path);
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
findTreeNodesByText = (text) => {
|
|
576
|
+
// Helper for onSearchTree
|
|
577
|
+
// Searches whole treeNodeData for any matching items
|
|
578
|
+
// Returns multiple nodes
|
|
579
|
+
|
|
580
|
+
const regex = new RegExp(text, 'i'); // instead of matching based on full text match, search for a partial match
|
|
581
|
+
|
|
582
|
+
function searchChildren(children, found = []) {
|
|
583
|
+
_.each(children, (child) => {
|
|
584
|
+
if (child.text.match(regex)) {
|
|
585
|
+
found.push(child);
|
|
586
|
+
}
|
|
587
|
+
if (child.children) {
|
|
588
|
+
searchChildren(child.children, found);
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
return found;
|
|
592
|
+
}
|
|
593
|
+
return searchChildren(treeNodeData);
|
|
594
|
+
},
|
|
595
|
+
getTreeNodeByNodeId = (node_id) => {
|
|
596
|
+
if (Repository) {
|
|
597
|
+
return Repository.getById(node_id);
|
|
598
|
+
}
|
|
599
|
+
return data[node_id]; // TODO: This is probably not right!
|
|
600
|
+
},
|
|
601
|
+
getPathByTreeNode = (treeNode) => {
|
|
602
|
+
|
|
603
|
+
///////// THIS DOESN'T WORK YET /////////
|
|
604
|
+
|
|
605
|
+
function searchChildren(children, currentPath = []) {
|
|
606
|
+
let found = [];
|
|
607
|
+
_.each(children, (child) => {
|
|
608
|
+
const
|
|
609
|
+
item = child.item,
|
|
610
|
+
id = idField ? item[idField] : item.id;
|
|
611
|
+
if (child.text.match(regex)) {
|
|
612
|
+
found.push(child);
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
if (child.children) {
|
|
616
|
+
const childrenFound = searchChildren(child.children, [...currentPath, id]);
|
|
617
|
+
if (!_.isEmpty(childrenFound)) {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
return found;
|
|
623
|
+
}
|
|
624
|
+
const nodes = searchChildren(treeNodeData);
|
|
625
|
+
return nodes.join('/');
|
|
626
|
+
|
|
627
|
+
},
|
|
628
|
+
expandPath = (path) => {
|
|
629
|
+
// Helper for onSearchTree
|
|
630
|
+
|
|
631
|
+
// Drills down the tree based on path (usually given by server).
|
|
632
|
+
// Path would be a list of sequential IDs (3/35/263/1024)
|
|
633
|
+
// Initially, it closes thw whole tree.
|
|
634
|
+
|
|
635
|
+
let newTreeNodeData = collapseAll(false); // false = don't set new treeNodeData
|
|
636
|
+
|
|
637
|
+
// As it navigates down, it will expand the appropriate branches,
|
|
638
|
+
// and then finally highlight & select the node in question
|
|
639
|
+
let pathParts,
|
|
640
|
+
id,
|
|
641
|
+
currentLevelData = newTreeNodeData,
|
|
642
|
+
currentDatum,
|
|
643
|
+
currentNode;
|
|
644
|
+
|
|
645
|
+
while(path.length) {
|
|
646
|
+
pathParts = path.split('/');
|
|
647
|
+
id = parseInt(pathParts[0], 10); // grab the first part of the path
|
|
648
|
+
|
|
649
|
+
// find match in current level
|
|
650
|
+
currentDatum = _.find(currentLevelData, (treeNodeDatum) => {
|
|
651
|
+
return treeNodeDatum.item.id === id;
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
currentNode = currentDatum.item;
|
|
655
|
+
|
|
656
|
+
// THE MAGIC!
|
|
657
|
+
currentDatum.isExpanded = true;
|
|
658
|
+
|
|
659
|
+
path = pathParts.slice(1).join('/'); // put the rest of it back together
|
|
660
|
+
currentLevelData = currentDatum.children;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
setSelection([currentNode]);
|
|
664
|
+
scrollToNode(currentNode);
|
|
665
|
+
highlightNode(currentNode);
|
|
666
|
+
|
|
667
|
+
setTreeNodeData(newTreeNodeData);
|
|
668
|
+
},
|
|
669
|
+
scrollToNode = (node) => {
|
|
670
|
+
// Helper for expandPath
|
|
671
|
+
// Scroll the tree so the given node is in view
|
|
672
|
+
|
|
673
|
+
// TODO: This will probably need different methods in web and mobile
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
},
|
|
677
|
+
highlightNode = (node) => {
|
|
678
|
+
// Helper for expandPath
|
|
679
|
+
// Show a brief highlight animation to draw attention to the node
|
|
680
|
+
|
|
681
|
+
// TODO: This will probably need different methods in web and mobile
|
|
682
|
+
// react-highlight for web?
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
},
|
|
686
|
+
|
|
687
|
+
// Drag/Drop
|
|
688
|
+
getReorderProxy = (node) => {
|
|
689
|
+
const
|
|
690
|
+
row = node.parentElement.parentElement,
|
|
691
|
+
rowRect = row.getBoundingClientRect(),
|
|
692
|
+
parent = row.parentElement,
|
|
693
|
+
parentRect = parent.getBoundingClientRect(),
|
|
694
|
+
proxy = row.cloneNode(true),
|
|
695
|
+
top = rowRect.top - parentRect.top,
|
|
696
|
+
dragNodeIx = Array.from(parent.children).indexOf(row)
|
|
697
|
+
|
|
698
|
+
setDragNodeIx(dragNodeIx); // the ix of which record is being dragged
|
|
699
|
+
|
|
700
|
+
proxy.style.top = top + 'px';
|
|
701
|
+
proxy.style.left = '20px';
|
|
702
|
+
proxy.style.height = rowRect.height + 'px';
|
|
703
|
+
proxy.style.width = rowRect.width + 'px';
|
|
704
|
+
proxy.style.display = 'flex';
|
|
705
|
+
// proxy.style.backgroundColor = '#ccc';
|
|
706
|
+
proxy.style.position = 'absolute';
|
|
707
|
+
proxy.style.border = '1px solid #000';
|
|
708
|
+
return proxy;
|
|
709
|
+
},
|
|
710
|
+
onNodeReorderDragStart = (info, e, proxy, node) => {
|
|
711
|
+
// console.log('onNodeReorderDragStart', info, e, proxy, node);
|
|
712
|
+
const
|
|
713
|
+
proxyRect = proxy.getBoundingClientRect(),
|
|
714
|
+
row = node.parentElement.parentElement,
|
|
715
|
+
parent = row.parentElement,
|
|
716
|
+
parentRect = parent.getBoundingClientRect(),
|
|
717
|
+
rows = _.filter(row.parentElement.children, (childNode) => {
|
|
718
|
+
return childNode.getBoundingClientRect().height !== 0; // Skip zero-height children
|
|
719
|
+
}),
|
|
720
|
+
currentY = proxyRect.top - parentRect.top, // top position of pointer, relative to page
|
|
721
|
+
headerNodeIx = showHeaders ? 0 : null,
|
|
722
|
+
firstActualNodeIx = showHeaders ? 1 : 0;
|
|
723
|
+
|
|
724
|
+
// Figure out which index the user wants
|
|
725
|
+
let newIx = 0;
|
|
726
|
+
_.each(rows, (child, ix, all) => {
|
|
727
|
+
const
|
|
728
|
+
rect = child.getBoundingClientRect(), // rect of the row of this iteration
|
|
729
|
+
{
|
|
730
|
+
top,
|
|
731
|
+
bottom,
|
|
732
|
+
height,
|
|
733
|
+
} = rect,
|
|
734
|
+
compensatedTop = top - parentRect.top,
|
|
735
|
+
compensatedBottom = bottom - parentRect.top,
|
|
736
|
+
halfHeight = height / 2;
|
|
737
|
+
|
|
738
|
+
if (ix === headerNodeIx || child === proxy) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (ix === firstActualNodeIx) {
|
|
742
|
+
// first row
|
|
743
|
+
if (currentY < compensatedTop + halfHeight) {
|
|
744
|
+
newIx = firstActualNodeIx;
|
|
745
|
+
return false;
|
|
746
|
+
} else if (currentY < compensatedBottom) {
|
|
747
|
+
newIx = firstActualNodeIx + 1;
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
return;
|
|
751
|
+
} else if (ix === all.length -1) {
|
|
752
|
+
// last row
|
|
753
|
+
if (currentY < compensatedTop + halfHeight) {
|
|
754
|
+
newIx = ix;
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
newIx = ix +1;
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// all other rows
|
|
762
|
+
if (compensatedTop <= currentY && currentY < compensatedTop + halfHeight) {
|
|
763
|
+
newIx = ix;
|
|
764
|
+
return false;
|
|
765
|
+
} else if (currentY < compensatedBottom) {
|
|
766
|
+
newIx = ix +1;
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
let useBottom = false;
|
|
772
|
+
if (!rows[newIx] || rows[newIx] === proxy) {
|
|
773
|
+
newIx--;
|
|
774
|
+
useBottom = true;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Render marker showing destination location
|
|
778
|
+
const
|
|
779
|
+
rowContainerRect = rows[newIx].getBoundingClientRect(),
|
|
780
|
+
top = (useBottom ? rowContainerRect.bottom : rowContainerRect.top) - parentRect.top - parseInt(parent.style.borderWidth), // get relative Y position
|
|
781
|
+
treeNodesContainer = treeRef.current._listRef._scrollRef.childNodes[0],
|
|
782
|
+
treeNodesContainerRect = treeNodesContainer.getBoundingClientRect(),
|
|
783
|
+
marker = document.createElement('div');
|
|
784
|
+
|
|
785
|
+
marker.style.position = 'absolute';
|
|
786
|
+
marker.style.top = top -4 + 'px'; // -4 so it's always visible
|
|
787
|
+
marker.style.height = '4px';
|
|
788
|
+
marker.style.width = treeNodesContainerRect.width + 'px';
|
|
789
|
+
marker.style.backgroundColor = '#f00';
|
|
790
|
+
|
|
791
|
+
treeNodesContainer.appendChild(marker);
|
|
792
|
+
|
|
793
|
+
setDragNodeSlot({ ix: newIx, marker, useBottom, });
|
|
794
|
+
},
|
|
795
|
+
onNodeReorderDrag = (info, e, proxy, node) => {
|
|
796
|
+
// console.log('onNodeReorderDrag', info, e, proxy, node);
|
|
797
|
+
const
|
|
798
|
+
proxyRect = proxy.getBoundingClientRect(),
|
|
799
|
+
row = node.parentElement.parentElement,
|
|
800
|
+
parent = row.parentElement,
|
|
801
|
+
parentRect = parent.getBoundingClientRect(),
|
|
802
|
+
rows = _.filter(row.parentElement.children, (childNode) => {
|
|
803
|
+
return childNode.getBoundingClientRect().height !== 0; // Skip zero-height children
|
|
804
|
+
}),
|
|
805
|
+
currentY = proxyRect.top - parentRect.top, // top position of pointer, relative to page
|
|
806
|
+
headerNodeIx = showHeaders ? 0 : null,
|
|
807
|
+
firstActualNodeIx = showHeaders ? 1 : 0;
|
|
808
|
+
|
|
809
|
+
// Figure out which index the user wants
|
|
810
|
+
let newIx = 0;
|
|
811
|
+
_.each(rows, (child, ix, all) => {
|
|
812
|
+
const
|
|
813
|
+
rect = child.getBoundingClientRect(), // rect of the row of this iteration
|
|
814
|
+
{
|
|
815
|
+
top,
|
|
816
|
+
bottom,
|
|
817
|
+
height,
|
|
818
|
+
} = rect,
|
|
819
|
+
compensatedTop = top - parentRect.top,
|
|
820
|
+
compensatedBottom = bottom - parentRect.top,
|
|
821
|
+
halfHeight = height / 2;
|
|
822
|
+
|
|
823
|
+
if (ix === headerNodeIx || child === proxy) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (ix === firstActualNodeIx) {
|
|
827
|
+
// first row
|
|
828
|
+
if (currentY < compensatedTop + halfHeight) {
|
|
829
|
+
newIx = firstActualNodeIx;
|
|
830
|
+
return false;
|
|
831
|
+
} else if (currentY < compensatedBottom) {
|
|
832
|
+
newIx = firstActualNodeIx + 1;
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
return;
|
|
836
|
+
} else if (ix === all.length -1) {
|
|
837
|
+
// last row
|
|
838
|
+
if (currentY < compensatedTop + halfHeight) {
|
|
839
|
+
newIx = ix;
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
newIx = ix +1;
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// all other rows
|
|
847
|
+
if (compensatedTop <= currentY && currentY < compensatedTop + halfHeight) {
|
|
848
|
+
newIx = ix;
|
|
849
|
+
return false;
|
|
850
|
+
} else if (currentY < compensatedBottom) {
|
|
851
|
+
newIx = ix +1;
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
let useBottom = false;
|
|
857
|
+
if (!rows[newIx] || rows[newIx] === proxy) {
|
|
858
|
+
newIx--;
|
|
859
|
+
useBottom = true;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Render marker showing destination location (can't use regular render cycle because this div is absolutely positioned on page)
|
|
863
|
+
const
|
|
864
|
+
rowContainerRect = rows[newIx].getBoundingClientRect(),
|
|
865
|
+
top = (useBottom ? rowContainerRect.bottom : rowContainerRect.top) - parentRect.top - parseInt(parent.style.borderWidth); // get relative Y position
|
|
866
|
+
let marker = dragNodeSlot && dragNodeSlot.marker;
|
|
867
|
+
if (marker) {
|
|
868
|
+
marker.style.top = top -4 + 'px'; // -4 so it's always visible
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
setDragNodeSlot({ ix: newIx, marker, useBottom, });
|
|
872
|
+
// console.log('onNodeReorderDrag', newIx);
|
|
873
|
+
|
|
874
|
+
},
|
|
875
|
+
onNodeReorderDragStop = (delta, e, config) => {
|
|
876
|
+
// console.log('onNodeReorderDragStop', delta, e, config);
|
|
877
|
+
const
|
|
878
|
+
dropIx = dragNodeSlot.ix,
|
|
879
|
+
compensatedDragIx = showHeaders ? dragNodeIx -1 : dragNodeIx, // ix, without taking header row into account
|
|
880
|
+
compensatedDropIx = showHeaders ? dropIx -1 : dropIx, // // ix, without taking header row into account
|
|
881
|
+
dropPosition = dragNodeSlot.useBottom ? DROP_POSITION_AFTER : DROP_POSITION_BEFORE;
|
|
882
|
+
|
|
883
|
+
let shouldMove = true,
|
|
884
|
+
finalDropIx = compensatedDropIx;
|
|
885
|
+
|
|
886
|
+
if (dropPosition === DROP_POSITION_BEFORE) {
|
|
887
|
+
if (dragNodeIx === dropIx || dragNodeIx === dropIx -1) { // basically before or after the drag row's origin
|
|
888
|
+
// Same as origin; don't do anything
|
|
889
|
+
shouldMove = false;
|
|
890
|
+
} else {
|
|
891
|
+
// Actually move it
|
|
892
|
+
if (!Repository) { // If we're just going to be switching rows, rather than telling server to reorder rows, so maybe adjust finalDropIx...
|
|
893
|
+
if (finalDropIx > compensatedDragIx) { // if we're dropping *before* the origin ix
|
|
894
|
+
finalDropIx = finalDropIx -1; // Because we're using BEFORE, we want to switch with the row *prior to* the ix we're dropping before
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
} else if (dropPosition === DROP_POSITION_AFTER) {
|
|
899
|
+
// Only happens on the very last row. Everything else is BEFORE...
|
|
900
|
+
if (dragNodeIx === dropIx) {
|
|
901
|
+
// Same as origin; don't do anything
|
|
902
|
+
shouldMove = false;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (shouldMove) {
|
|
907
|
+
// Update the row with the new ix
|
|
908
|
+
let dragRecord,
|
|
909
|
+
dropRecord;
|
|
910
|
+
if (Repository) {
|
|
911
|
+
dragRecord = Repository.getByIx(compensatedDragIx);
|
|
912
|
+
dropRecord = Repository.getByIx(finalDropIx);
|
|
913
|
+
|
|
914
|
+
Repository.reorder(dragRecord, dropRecord, dropPosition);
|
|
915
|
+
|
|
916
|
+
} else {
|
|
917
|
+
function arrayMove(arr, fromIndex, toIndex) {
|
|
918
|
+
var element = arr[fromIndex];
|
|
919
|
+
arr.splice(fromIndex, 1);
|
|
920
|
+
arr.splice(toIndex, 0, element);
|
|
921
|
+
}
|
|
922
|
+
arrayMove(data, compensatedDragIx, finalDropIx);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (dragNodeSlot) {
|
|
927
|
+
dragNodeSlot.marker.remove();
|
|
928
|
+
}
|
|
929
|
+
setDragNodeSlot(null);
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
useEffect(() => {
|
|
933
|
+
|
|
934
|
+
async function buildAndSetTreeNodeData() {
|
|
935
|
+
|
|
936
|
+
let rootNodes;
|
|
937
|
+
if (Repository) {
|
|
938
|
+
rootNodes = await Repository.getRootNodes(true, 1, getAdditionalParams);
|
|
939
|
+
} else {
|
|
940
|
+
// TODO: Make this work for data array
|
|
941
|
+
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const treeNodeData = buildTreeNodeData(rootNodes);
|
|
945
|
+
setTreeNodeData(treeNodeData);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (!isReady) {
|
|
949
|
+
(async () => {
|
|
950
|
+
await buildAndSetTreeNodeData();
|
|
951
|
+
setIsReady(true);
|
|
952
|
+
})();
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (!Repository) {
|
|
956
|
+
return () => {};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// set up @onehat/data repository
|
|
960
|
+
const
|
|
961
|
+
setTrue = () => setIsLoading(true),
|
|
962
|
+
setFalse = () => setIsLoading(false),
|
|
963
|
+
onChangeFilters = () => {
|
|
964
|
+
if (!Repository.isAutoLoad) {
|
|
965
|
+
Repository.reload();
|
|
966
|
+
}
|
|
967
|
+
},
|
|
968
|
+
onChangeSorters = () => {
|
|
969
|
+
if (!Repository.isAutoLoad) {
|
|
970
|
+
Repository.reload();
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
Repository.on('beforeLoad', setTrue);
|
|
975
|
+
Repository.on('load', setFalse);
|
|
976
|
+
Repository.ons(['changePage', 'changePageSize',], deselectAll);
|
|
977
|
+
Repository.ons(['changeData', 'change'], buildAndSetTreeNodeData);
|
|
978
|
+
Repository.on('changeFilters', onChangeFilters);
|
|
979
|
+
Repository.on('changeSorters', onChangeSorters);
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
return () => {
|
|
983
|
+
Repository.off('beforeLoad', setTrue);
|
|
984
|
+
Repository.off('load', setFalse);
|
|
985
|
+
Repository.offs(['changePage', 'changePageSize',], deselectAll);
|
|
986
|
+
Repository.offs(['changeData', 'change'], buildAndSetTreeNodeData);
|
|
987
|
+
Repository.off('changeFilters', onChangeFilters);
|
|
988
|
+
Repository.off('changeSorters', onChangeSorters);
|
|
989
|
+
};
|
|
990
|
+
}, []);
|
|
991
|
+
|
|
992
|
+
useEffect(() => {
|
|
993
|
+
if (!Repository) {
|
|
994
|
+
return () => {};
|
|
995
|
+
}
|
|
996
|
+
if (!disableSelectorSelected && selectorId) {
|
|
997
|
+
let id = selectorSelected?.id;
|
|
998
|
+
if (_.isEmpty(selectorSelected)) {
|
|
999
|
+
id = noSelectorMeansNoResults ? 'NO_MATCHES' : null;
|
|
1000
|
+
}
|
|
1001
|
+
Repository.filter(selectorId, id, false); // so it doesn't clear existing filters
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
}, [selectorId, selectorSelected]);
|
|
1005
|
+
|
|
1006
|
+
const
|
|
1007
|
+
headerToolbarItemComponents = useMemo(() => getHeaderToolbarItems(), []),
|
|
1008
|
+
footerToolbarItemComponents = useMemo(() => getFooterToolbarItems(), [additionalToolbarButtons, isReorderMode]);
|
|
1009
|
+
|
|
1010
|
+
if (!isReady) {
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Actual TreeNodes
|
|
1015
|
+
const treeNodes = renderAllTreeNodes();
|
|
1016
|
+
|
|
1017
|
+
// headers & footers
|
|
1018
|
+
let treeFooterComponent = null;
|
|
1019
|
+
if (!disableBottomToolbar) {
|
|
1020
|
+
if (Repository && bottomToolbar === 'pagination' && !disablePagination && Repository.isPaginated) {
|
|
1021
|
+
treeFooterComponent = <PaginationToolbar Repository={Repository} toolbarItems={footerToolbarItemComponents} />;
|
|
1022
|
+
} else if (footerToolbarItemComponents.length) {
|
|
1023
|
+
treeFooterComponent = <Toolbar>{footerToolbarItemComponents}</Toolbar>;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
return <>
|
|
1028
|
+
<Column
|
|
1029
|
+
{...testProps('Tree')}
|
|
1030
|
+
flex={1}
|
|
1031
|
+
w="100%"
|
|
1032
|
+
>
|
|
1033
|
+
{topToolbar}
|
|
1034
|
+
{headerToolbarItemComponents}
|
|
1035
|
+
|
|
1036
|
+
<Column w="100%" flex={1} borderTopWidth={isLoading ? 2 : 1} borderTopColor={isLoading ? '#f00' : 'trueGray.300'} onClick={() => {
|
|
1037
|
+
if (!isReorderMode) {
|
|
1038
|
+
deselectAll();
|
|
1039
|
+
}
|
|
1040
|
+
}}>
|
|
1041
|
+
{!treeNodes.length ? <NoRecordsFound text={noneFoundText} onRefresh={onRefresh} /> :
|
|
1042
|
+
treeNodes}
|
|
1043
|
+
</Column>
|
|
1044
|
+
|
|
1045
|
+
{treeFooterComponent}
|
|
1046
|
+
</Column>
|
|
1047
|
+
|
|
1048
|
+
<Modal
|
|
1049
|
+
isOpen={isSearchModalShown}
|
|
1050
|
+
onClose={() => setIsSearchModalShown(false)}
|
|
1051
|
+
>
|
|
1052
|
+
<Column bg="#fff" w={500}>
|
|
1053
|
+
<FormPanel
|
|
1054
|
+
title="Choose Tree Node"
|
|
1055
|
+
instructions="Multiple tree nodes matched your search. Please select which one to show."
|
|
1056
|
+
flex={1}
|
|
1057
|
+
items={[
|
|
1058
|
+
{
|
|
1059
|
+
type: 'Column',
|
|
1060
|
+
flex: 1,
|
|
1061
|
+
items: [
|
|
1062
|
+
{
|
|
1063
|
+
key: 'node_id',
|
|
1064
|
+
name: 'node_id',
|
|
1065
|
+
type: 'Combo',
|
|
1066
|
+
label: 'Tree Node',
|
|
1067
|
+
data: searchFormData,
|
|
1068
|
+
}
|
|
1069
|
+
],
|
|
1070
|
+
},
|
|
1071
|
+
]}
|
|
1072
|
+
onCancel={(e) => {
|
|
1073
|
+
// Just close the modal
|
|
1074
|
+
setIsSearchModalShown(false);
|
|
1075
|
+
}}
|
|
1076
|
+
onSave={(data, e) => {
|
|
1077
|
+
|
|
1078
|
+
const node_id = data.node_id; // NOT SURE THIS IS CORRECT!
|
|
1079
|
+
|
|
1080
|
+
if (isMultipleHits) {
|
|
1081
|
+
// Tell the server which one you want and get it, loading all children necessary to get there
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
} else {
|
|
1086
|
+
// Show the path based on local data
|
|
1087
|
+
const
|
|
1088
|
+
treeNode = getTreeNodeByNodeId(node_id),
|
|
1089
|
+
path = getPathByTreeNode(treeNode);
|
|
1090
|
+
expandPath(path);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Close the modal
|
|
1094
|
+
setIsSearchModalShown(false);
|
|
1095
|
+
}}
|
|
1096
|
+
/>
|
|
1097
|
+
</Column>
|
|
1098
|
+
</Modal>
|
|
1099
|
+
</>;
|
|
1100
|
+
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
export const SideTreeEditor = withAlert(
|
|
1104
|
+
withEvents(
|
|
1105
|
+
withData(
|
|
1106
|
+
// withMultiSelection(
|
|
1107
|
+
withSelection(
|
|
1108
|
+
withSideEditor(
|
|
1109
|
+
withFilters(
|
|
1110
|
+
withPresetButtons(
|
|
1111
|
+
withContextMenu(
|
|
1112
|
+
Tree
|
|
1113
|
+
)
|
|
1114
|
+
)
|
|
1115
|
+
)
|
|
1116
|
+
)
|
|
1117
|
+
)
|
|
1118
|
+
// )
|
|
1119
|
+
)
|
|
1120
|
+
)
|
|
1121
|
+
);
|
|
1122
|
+
|
|
1123
|
+
export const WindowedTreeEditor = withAlert(
|
|
1124
|
+
withEvents(
|
|
1125
|
+
withData(
|
|
1126
|
+
// withMultiSelection(
|
|
1127
|
+
withSelection(
|
|
1128
|
+
withWindowedEditor(
|
|
1129
|
+
withFilters(
|
|
1130
|
+
withPresetButtons(
|
|
1131
|
+
withContextMenu(
|
|
1132
|
+
Tree
|
|
1133
|
+
)
|
|
1134
|
+
)
|
|
1135
|
+
)
|
|
1136
|
+
)
|
|
1137
|
+
)
|
|
1138
|
+
// )
|
|
1139
|
+
)
|
|
1140
|
+
)
|
|
1141
|
+
);
|
|
1142
|
+
|
|
1143
|
+
export default WindowedTreeEditor;
|