@onehat/ui 0.2.74 → 0.2.75
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/Buttons/IconButton.js +7 -2
- package/src/Components/Hoc/withAlert.js +42 -39
- package/src/Components/Hoc/withEditor.js +136 -59
- package/src/Components/Tree/Tree.js +389 -315
- package/src/Constants/Styles.js +1 -1
- package/src/Functions/getIconButtonFromConfig.js +8 -9
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Tooltip,
|
|
7
7
|
} from 'native-base';
|
|
8
8
|
import styles from '../../Constants/Styles.js';
|
|
9
|
+
import _ from 'lodash';
|
|
9
10
|
|
|
10
11
|
const IconButton = React.forwardRef((props, ref) => {
|
|
11
12
|
const {
|
|
@@ -17,12 +18,16 @@ const IconButton = React.forwardRef((props, ref) => {
|
|
|
17
18
|
tooltipPlacement = 'bottom',
|
|
18
19
|
} = props;
|
|
19
20
|
const propsIcon = props._icon || {};
|
|
20
|
-
let icon = props.icon
|
|
21
|
+
let icon = props.icon,
|
|
21
22
|
ret;
|
|
22
23
|
if (isLoading) {
|
|
23
24
|
icon = <Spinner {..._spinner} />;
|
|
24
25
|
}
|
|
25
|
-
if (
|
|
26
|
+
if (React.isValidElement(icon)) {
|
|
27
|
+
if (!_.isEmpty(propsIcon)) {
|
|
28
|
+
icon = React.cloneElement(icon, {...propsIcon});
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
26
31
|
icon = <Icon as={icon} {...propsIcon} />;
|
|
27
32
|
}
|
|
28
33
|
const pressable = <Pressable
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState, useRef, useEffect, } from 'react';
|
|
2
2
|
import {
|
|
3
|
+
AlertDialog,
|
|
3
4
|
Button,
|
|
4
5
|
Column,
|
|
5
6
|
Icon,
|
|
@@ -30,26 +31,29 @@ export default function withAlert(WrappedComponent) {
|
|
|
30
31
|
[customButtons, setCustomButtons] = useState(),
|
|
31
32
|
[mode, setMode] = useState(ALERT_MODE_OK),
|
|
32
33
|
autoFocusRef = useRef(null),
|
|
33
|
-
|
|
34
|
+
cancelRef = useRef(null),
|
|
35
|
+
onAlert = (arg1, okCallback, includeCancel = false) => {
|
|
34
36
|
clearAll();
|
|
35
37
|
if (_.isString(arg1)) {
|
|
36
38
|
setMode(ALERT_MODE_OK);
|
|
37
39
|
setTitle('Alert');
|
|
38
40
|
setMessage(arg1);
|
|
39
|
-
setOkCallback(() =>
|
|
41
|
+
setOkCallback(() => okCallback);
|
|
42
|
+
setIncludeCancel(includeCancel);
|
|
40
43
|
} else if (_.isPlainObject(arg1)) {
|
|
41
44
|
// custom
|
|
42
45
|
const {
|
|
43
46
|
title = 'Alert',
|
|
44
47
|
message,
|
|
45
48
|
buttons,
|
|
49
|
+
includeCancel,
|
|
46
50
|
} = arg1;
|
|
47
51
|
setMode(ALERT_MODE_CUSTOM);
|
|
48
52
|
setTitle(title);
|
|
49
53
|
setMessage(message);
|
|
50
54
|
setCustomButtons(buttons);
|
|
55
|
+
setIncludeCancel(includeCancel);
|
|
51
56
|
}
|
|
52
|
-
setIncludeCancel(includeCancel);
|
|
53
57
|
showAlert();
|
|
54
58
|
},
|
|
55
59
|
onConfirm = (message, callback, includeCancel = false) => {
|
|
@@ -65,31 +69,29 @@ export default function withAlert(WrappedComponent) {
|
|
|
65
69
|
setIsAlertShown(false);
|
|
66
70
|
},
|
|
67
71
|
onOk = () => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (callback) {
|
|
71
|
-
callback();
|
|
72
|
+
if (okCallback) {
|
|
73
|
+
okCallback();
|
|
72
74
|
}
|
|
75
|
+
hideAlert();
|
|
73
76
|
},
|
|
74
77
|
onYes = () => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (callback) {
|
|
78
|
-
callback();
|
|
78
|
+
if (yesCallback) {
|
|
79
|
+
yesCallback();
|
|
79
80
|
}
|
|
81
|
+
hideAlert();
|
|
80
82
|
},
|
|
81
83
|
onNo = () => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (callback) {
|
|
85
|
-
callback();
|
|
84
|
+
if (noCallback) {
|
|
85
|
+
noCallback();
|
|
86
86
|
}
|
|
87
|
+
hideAlert();
|
|
87
88
|
},
|
|
88
89
|
showAlert = () => {
|
|
89
90
|
setIsAlertShown(true);
|
|
90
91
|
},
|
|
91
92
|
hideAlert = () => {
|
|
92
93
|
setIsAlertShown(false);
|
|
94
|
+
clearAll();
|
|
93
95
|
},
|
|
94
96
|
clearAll = () => {
|
|
95
97
|
setOkCallback();
|
|
@@ -104,7 +106,9 @@ export default function withAlert(WrappedComponent) {
|
|
|
104
106
|
key="cancelBtn"
|
|
105
107
|
onPress={onCancel}
|
|
106
108
|
color="#fff"
|
|
107
|
-
|
|
109
|
+
colorScheme="coolGray"
|
|
110
|
+
variant="ghost" // or unstyled
|
|
111
|
+
ref={cancelRef}
|
|
108
112
|
>Cancel</Button>);
|
|
109
113
|
}
|
|
110
114
|
switch(mode) {
|
|
@@ -128,10 +132,13 @@ export default function withAlert(WrappedComponent) {
|
|
|
128
132
|
ref={autoFocusRef}
|
|
129
133
|
onPress={onYes}
|
|
130
134
|
color="#fff"
|
|
135
|
+
colorScheme="danger"
|
|
131
136
|
>Yes</Button>);
|
|
132
137
|
break;
|
|
133
138
|
case ALERT_MODE_CUSTOM:
|
|
134
|
-
|
|
139
|
+
_.each(customButtons, (button) => {
|
|
140
|
+
buttons.push(button);
|
|
141
|
+
});
|
|
135
142
|
break;
|
|
136
143
|
default:
|
|
137
144
|
}
|
|
@@ -141,36 +148,32 @@ export default function withAlert(WrappedComponent) {
|
|
|
141
148
|
{...props}
|
|
142
149
|
alert={onAlert}
|
|
143
150
|
confirm={onConfirm}
|
|
151
|
+
hideAlert={hideAlert}
|
|
144
152
|
/>
|
|
145
|
-
|
|
153
|
+
|
|
154
|
+
<AlertDialog
|
|
155
|
+
leastDestructiveRef={cancelRef}
|
|
146
156
|
isOpen={isAlertShown}
|
|
147
|
-
onOpen={() => {debugger;}}
|
|
148
157
|
onClose={() => setIsAlertShown(false)}
|
|
149
158
|
>
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
<Panel
|
|
156
|
-
title={title}
|
|
157
|
-
isCollapsible={false}
|
|
158
|
-
p={0}
|
|
159
|
-
footer={<Footer justifyContent="flex-end" >
|
|
160
|
-
<Button.Group space={2}>
|
|
161
|
-
{buttons}
|
|
162
|
-
</Button.Group>
|
|
163
|
-
</Footer>}
|
|
164
|
-
>
|
|
165
|
-
<Row flex={1} p={5}>
|
|
159
|
+
<AlertDialog.Content>
|
|
160
|
+
<AlertDialog.CloseButton />
|
|
161
|
+
<AlertDialog.Header>{title}</AlertDialog.Header>
|
|
162
|
+
<AlertDialog.Body>
|
|
163
|
+
<Row>
|
|
166
164
|
<Column w="40px" p={0} mr={5}>
|
|
167
165
|
<Icon as={TriangleExclamation} size={10} color="#f00" />
|
|
168
166
|
</Column>
|
|
169
|
-
<Text>{message}</Text>
|
|
167
|
+
<Text flex={1}>{message}</Text>
|
|
170
168
|
</Row>
|
|
171
|
-
</
|
|
172
|
-
|
|
173
|
-
|
|
169
|
+
</AlertDialog.Body>
|
|
170
|
+
<AlertDialog.Footer>
|
|
171
|
+
<Button.Group space={2}>
|
|
172
|
+
{buttons}
|
|
173
|
+
</Button.Group>
|
|
174
|
+
</AlertDialog.Footer>
|
|
175
|
+
</AlertDialog.Content>
|
|
176
|
+
</AlertDialog>
|
|
174
177
|
</>;
|
|
175
178
|
};
|
|
176
179
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { useEffect, useState, } from 'react';
|
|
1
|
+
import { useEffect, useState, useRef, } from 'react';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
Modal,
|
|
5
|
-
Text,
|
|
3
|
+
Button,
|
|
6
4
|
} from 'native-base';
|
|
7
5
|
import {
|
|
8
6
|
EDITOR_MODE__VIEW,
|
|
@@ -11,7 +9,7 @@ import {
|
|
|
11
9
|
} from '../../Constants/Editor.js';
|
|
12
10
|
import _ from 'lodash';
|
|
13
11
|
|
|
14
|
-
export default function withEditor(WrappedComponent) {
|
|
12
|
+
export default function withEditor(WrappedComponent, isTree = false) {
|
|
15
13
|
return (props) => {
|
|
16
14
|
|
|
17
15
|
let [editorMode, setEditorMode] = useState(EDITOR_MODE__VIEW); // Can change below, so use 'let'
|
|
@@ -24,7 +22,6 @@ export default function withEditor(WrappedComponent) {
|
|
|
24
22
|
disableDelete = false,
|
|
25
23
|
disableDuplicate = false,
|
|
26
24
|
disableView = false,
|
|
27
|
-
isTree = false,
|
|
28
25
|
getRecordIdentifier = (selection) => {
|
|
29
26
|
if (selection.length > 1) {
|
|
30
27
|
return 'records?';
|
|
@@ -45,13 +42,23 @@ export default function withEditor(WrappedComponent) {
|
|
|
45
42
|
setSelection,
|
|
46
43
|
|
|
47
44
|
// withAlert
|
|
45
|
+
alert,
|
|
48
46
|
confirm,
|
|
47
|
+
hideAlert,
|
|
49
48
|
} = props,
|
|
49
|
+
listeners = useRef({}),
|
|
50
50
|
[currentRecord, setCurrentRecord] = useState(null),
|
|
51
51
|
[isEditorShown, setIsEditorShown] = useState(false),
|
|
52
52
|
[isEditorViewOnly, setIsEditorViewOnly] = useState(false),
|
|
53
53
|
[isModalShown, setIsModalShown] = useState(false),
|
|
54
54
|
[lastSelection, setLastSelection] = useState(),
|
|
55
|
+
getListeners = () => {
|
|
56
|
+
return listeners.current;
|
|
57
|
+
},
|
|
58
|
+
setListeners = (obj) => {
|
|
59
|
+
listeners.current = obj;
|
|
60
|
+
// forceUpdate(); // we don't want to get into an infinite loop of renders. Simply directly assign the listeners in every child render
|
|
61
|
+
},
|
|
55
62
|
onAdd = async () => {
|
|
56
63
|
const defaultValues = Repository.getSchema().model.defaultValues;
|
|
57
64
|
let addValues = _.clone(defaultValues);
|
|
@@ -60,42 +67,84 @@ export default function withEditor(WrappedComponent) {
|
|
|
60
67
|
addValues[selectorId] = selectorSelected.id;
|
|
61
68
|
}
|
|
62
69
|
|
|
70
|
+
if (getListeners().onBeforeAdd) {
|
|
71
|
+
const listenerResult = await getListeners().onBeforeAdd();
|
|
72
|
+
if (listenerResult === false) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
if (isTree) {
|
|
64
78
|
if (!selection[0]) {
|
|
65
79
|
throw Error('Must select a parent node.');
|
|
66
80
|
}
|
|
67
81
|
addValues.parentId = selection[0].id;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
} else {
|
|
83
|
+
// Set repository to sort by id DESC and switch to page 1, so this new entity is guaranteed to show up on the current page, even after saving
|
|
84
|
+
const currentSorter = Repository.sorters[0];
|
|
85
|
+
if (currentSorter.name !== Repository.schema.model.idProperty || currentSorter.direction !== 'DESC') {
|
|
86
|
+
Repository.pauseEvents();
|
|
87
|
+
Repository.sort(Repository.schema.model.idProperty, 'DESC');
|
|
88
|
+
Repository.setPage(1);
|
|
89
|
+
Repository.resumeEvents();
|
|
90
|
+
await Repository.reload();
|
|
91
|
+
}
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
// Unmap the values, so we can input true originalData
|
|
81
95
|
addValues = Repository.unmapData(addValues);
|
|
82
96
|
|
|
83
|
-
const entity = await Repository.add(addValues, false, true
|
|
97
|
+
const entity = await Repository.add(addValues, false, true);
|
|
84
98
|
setSelection([entity]);
|
|
85
99
|
setIsEditorViewOnly(false);
|
|
86
100
|
setEditorMode(EDITOR_MODE__ADD);
|
|
87
101
|
setIsEditorShown(true);
|
|
102
|
+
|
|
103
|
+
if (getListeners().onAfterAdd) {
|
|
104
|
+
await getListeners().onAfterAdd(entity);
|
|
105
|
+
}
|
|
88
106
|
},
|
|
89
|
-
onEdit = () => {
|
|
107
|
+
onEdit = async () => {
|
|
108
|
+
if (getListeners().onBeforeEdit) {
|
|
109
|
+
const listenerResult = await getListeners().onBeforeEdit();
|
|
110
|
+
if (listenerResult === false) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
90
114
|
setIsEditorViewOnly(false);
|
|
91
115
|
setEditorMode(EDITOR_MODE__EDIT);
|
|
92
116
|
setIsEditorShown(true);
|
|
93
117
|
},
|
|
94
|
-
onDelete = () => {
|
|
118
|
+
onDelete = async () => {
|
|
119
|
+
if (getListeners().onBeforeDelete) {
|
|
120
|
+
const listenerResult = await getListeners().onBeforeDelete();
|
|
121
|
+
if (listenerResult === false) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
95
125
|
const
|
|
96
126
|
isSingle = selection.length === 1,
|
|
97
|
-
|
|
127
|
+
firstSelection = selection[0],
|
|
128
|
+
isTree = firstSelection?.isTree,
|
|
129
|
+
hasChildren = firstSelection?.hasChildren,
|
|
130
|
+
isPhantom = firstSelection?.isPhantom;
|
|
98
131
|
|
|
132
|
+
if (isSingle && isTree && hasChildren) {
|
|
133
|
+
alert({
|
|
134
|
+
title: 'Move up children?',
|
|
135
|
+
message: 'The node you have selected for deletion has children. ' +
|
|
136
|
+
'Should these children be moved up to this node\'s parent, or be deleted?',
|
|
137
|
+
buttons: [
|
|
138
|
+
<Button colorScheme="danger" onPress={onMoveChildren} key="moveBtn">
|
|
139
|
+
Move Children
|
|
140
|
+
</Button>,
|
|
141
|
+
<Button colorScheme="danger" onPress={onDeleteChildren} key="deleteBtn">
|
|
142
|
+
Delete Children
|
|
143
|
+
</Button>
|
|
144
|
+
],
|
|
145
|
+
includeCancel: true,
|
|
146
|
+
});
|
|
147
|
+
} else
|
|
99
148
|
if (isSingle && isPhantom) {
|
|
100
149
|
deleteRecord();
|
|
101
150
|
} else {
|
|
@@ -103,13 +152,28 @@ export default function withEditor(WrappedComponent) {
|
|
|
103
152
|
confirm('Are you sure you want to delete the ' + identifier, deleteRecord);
|
|
104
153
|
}
|
|
105
154
|
},
|
|
106
|
-
|
|
155
|
+
onMoveChildren = () => {
|
|
156
|
+
hideAlert();
|
|
157
|
+
deleteRecord(true);
|
|
158
|
+
},
|
|
159
|
+
onDeleteChildren = () => {
|
|
160
|
+
hideAlert();
|
|
161
|
+
deleteRecord();
|
|
162
|
+
},
|
|
163
|
+
deleteRecord = async (moveSubtreeUp) => {
|
|
164
|
+
if (getListeners().onBeforeDeleteSave) {
|
|
165
|
+
await getListeners().onBeforeDeleteSave(selection);
|
|
166
|
+
}
|
|
167
|
+
|
|
107
168
|
await Repository.delete(selection);
|
|
108
169
|
if (!Repository.isAutoSave) {
|
|
109
170
|
await Repository.save();
|
|
110
171
|
}
|
|
172
|
+
if (getListeners().onAfterDelete) {
|
|
173
|
+
await getListeners().onAfterDelete(selection);
|
|
174
|
+
}
|
|
111
175
|
},
|
|
112
|
-
viewRecord = () => {
|
|
176
|
+
viewRecord = async () => {
|
|
113
177
|
if (!userCanView) {
|
|
114
178
|
return;
|
|
115
179
|
}
|
|
@@ -119,6 +183,10 @@ export default function withEditor(WrappedComponent) {
|
|
|
119
183
|
setIsEditorViewOnly(true);
|
|
120
184
|
setEditorMode(EDITOR_MODE__VIEW);
|
|
121
185
|
setIsEditorShown(true);
|
|
186
|
+
|
|
187
|
+
if (getListeners().onAfterDelete) {
|
|
188
|
+
await getListeners().onAfterDelete(entity);
|
|
189
|
+
}
|
|
122
190
|
},
|
|
123
191
|
duplicateRecord = async () => {
|
|
124
192
|
if (!userCanEdit || disableDuplicate) {
|
|
@@ -157,8 +225,17 @@ export default function withEditor(WrappedComponent) {
|
|
|
157
225
|
}
|
|
158
226
|
});
|
|
159
227
|
}
|
|
228
|
+
|
|
229
|
+
if (getListeners().onBeforeEditSave) {
|
|
230
|
+
await getListeners().onBeforeEditSave(what);
|
|
231
|
+
}
|
|
232
|
+
|
|
160
233
|
await Repository.save();
|
|
161
234
|
setIsEditorShown(false);
|
|
235
|
+
|
|
236
|
+
if (getListeners().onAfterEdit) {
|
|
237
|
+
await getListeners().onAfterEdit(what);
|
|
238
|
+
}
|
|
162
239
|
},
|
|
163
240
|
onEditorCancel = async () => {
|
|
164
241
|
const
|
|
@@ -174,9 +251,17 @@ export default function withEditor(WrappedComponent) {
|
|
|
174
251
|
setIsEditorShown(false);
|
|
175
252
|
},
|
|
176
253
|
onEditorDelete = async () => {
|
|
254
|
+
if (getListeners().onBeforeDeleteSave) {
|
|
255
|
+
await getListeners().onBeforeDeleteSave(selection);
|
|
256
|
+
}
|
|
257
|
+
|
|
177
258
|
await deleteRecord();
|
|
178
259
|
setEditorMode(EDITOR_MODE__VIEW);
|
|
179
260
|
setIsEditorShown(false);
|
|
261
|
+
|
|
262
|
+
if (getListeners().onAfterDelete) {
|
|
263
|
+
await getListeners().onAfterDelete(selection);
|
|
264
|
+
}
|
|
180
265
|
},
|
|
181
266
|
calculateEditorMode = () => {
|
|
182
267
|
let mode = EDITOR_MODE__VIEW;
|
|
@@ -209,42 +294,34 @@ export default function withEditor(WrappedComponent) {
|
|
|
209
294
|
editorMode = calculateEditorMode();
|
|
210
295
|
}
|
|
211
296
|
|
|
212
|
-
return
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
{isTree && isModalShown &&
|
|
242
|
-
<Modal
|
|
243
|
-
isOpen={true}
|
|
244
|
-
onClose={() => setIsModalShown(false)}
|
|
245
|
-
>
|
|
246
|
-
|
|
247
|
-
</Modal>}
|
|
248
|
-
</>;
|
|
297
|
+
return <WrappedComponent
|
|
298
|
+
{...props}
|
|
299
|
+
currentRecord={currentRecord}
|
|
300
|
+
setCurrentRecord={setCurrentRecord}
|
|
301
|
+
isEditorShown={isEditorShown}
|
|
302
|
+
isEditorViewOnly={isEditorViewOnly}
|
|
303
|
+
editorMode={editorMode}
|
|
304
|
+
setEditorMode={setEditorMode}
|
|
305
|
+
setIsEditorShown={setIsEditorShown}
|
|
306
|
+
onAdd={(!userCanEdit || disableAdd) ? null : onAdd}
|
|
307
|
+
onEdit={(!userCanEdit || disableEdit) ? null : onEdit}
|
|
308
|
+
onDelete={(!userCanEdit || disableDelete || (editorMode === EDITOR_MODE__ADD && (selection[0]?.isPhantom || currentRecord?.isPhantom))) ? null : onDelete}
|
|
309
|
+
onView={viewRecord}
|
|
310
|
+
onDuplicate={duplicateRecord}
|
|
311
|
+
onEditorSave={onEditorSave}
|
|
312
|
+
onEditorCancel={onEditorCancel}
|
|
313
|
+
onEditorDelete={(!userCanEdit || disableDelete || (editorMode === EDITOR_MODE__ADD && (selection[0]?.isPhantom || currentRecord?.isPhantom))) ? null : onEditorDelete}
|
|
314
|
+
onEditorClose={onEditorClose}
|
|
315
|
+
setWithEditListeners={setListeners}
|
|
316
|
+
isEditor={true}
|
|
317
|
+
useEditor={useEditor}
|
|
318
|
+
userCanEdit={userCanEdit}
|
|
319
|
+
userCanView={userCanView}
|
|
320
|
+
disableAdd={disableAdd}
|
|
321
|
+
disableEdit={disableEdit}
|
|
322
|
+
disableDelete={disableDelete}
|
|
323
|
+
disableDuplicate={disableDuplicate}
|
|
324
|
+
disableView ={disableView}
|
|
325
|
+
/>;
|
|
249
326
|
};
|
|
250
327
|
}
|
|
@@ -52,18 +52,6 @@ import Toolbar from '../Toolbar/Toolbar.js';
|
|
|
52
52
|
import _ from 'lodash';
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
//////////////////////
|
|
56
|
-
//////////////////////
|
|
57
|
-
|
|
58
|
-
// Need to take into account whether using Repository or data.
|
|
59
|
-
// If using data, everything exists at once. What format will data be in?
|
|
60
|
-
// How does this interface with Repository?
|
|
61
|
-
// Maybe if Repository is not AjaxRepository, everything needs to be present at once!
|
|
62
|
-
|
|
63
|
-
//////////////////////
|
|
64
|
-
//////////////////////
|
|
65
|
-
|
|
66
|
-
|
|
67
55
|
function TreeComponent(props) {
|
|
68
56
|
const {
|
|
69
57
|
areRootsVisible = true,
|
|
@@ -103,7 +91,7 @@ function TreeComponent(props) {
|
|
|
103
91
|
additionalToolbarButtons = [],
|
|
104
92
|
reload = null, // Whenever this value changes after initial render, the tree will reload from scratch
|
|
105
93
|
parentIdIx,
|
|
106
|
-
|
|
94
|
+
|
|
107
95
|
// withEditor
|
|
108
96
|
onAdd,
|
|
109
97
|
onEdit,
|
|
@@ -112,6 +100,7 @@ function TreeComponent(props) {
|
|
|
112
100
|
onDuplicate,
|
|
113
101
|
onReset,
|
|
114
102
|
onContextMenu,
|
|
103
|
+
setWithEditListeners,
|
|
115
104
|
|
|
116
105
|
// withData
|
|
117
106
|
Repository,
|
|
@@ -141,16 +130,27 @@ function TreeComponent(props) {
|
|
|
141
130
|
styles = UiGlobals.styles,
|
|
142
131
|
forceUpdate = useForceUpdate(),
|
|
143
132
|
treeRef = useRef(),
|
|
133
|
+
treeNodeData = useRef(),
|
|
144
134
|
[isReady, setIsReady] = useState(false),
|
|
145
135
|
[isLoading, setIsLoading] = useState(false),
|
|
146
136
|
[isReorderMode, setIsReorderMode] = useState(false),
|
|
147
137
|
[isSearchModalShown, setIsSearchModalShown] = useState(false),
|
|
148
|
-
[treeNodeData, setTreeNodeData] = useState({}),
|
|
149
138
|
[searchResults, setSearchResults] = useState([]),
|
|
150
139
|
[searchFormData, setSearchFormData] = useState([]),
|
|
151
140
|
[dragNodeSlot, setDragNodeSlot] = useState(null),
|
|
152
141
|
[dragNodeIx, setDragNodeIx] = useState(),
|
|
153
142
|
[treeSearchValue, setTreeSearchValue] = useState(''),
|
|
143
|
+
|
|
144
|
+
// state getters & setters
|
|
145
|
+
getTreeNodeData = () => {
|
|
146
|
+
return treeNodeData.current;
|
|
147
|
+
},
|
|
148
|
+
setTreeNodeData = (tnd) => {
|
|
149
|
+
treeNodeData.current = tnd;
|
|
150
|
+
forceUpdate();
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// event handers
|
|
154
154
|
onNodeClick = (item, e) => {
|
|
155
155
|
if (!setSelection) {
|
|
156
156
|
return;
|
|
@@ -213,65 +213,137 @@ function TreeComponent(props) {
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
},
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
216
|
+
onBeforeAdd = async () => {
|
|
217
|
+
// Load children before adding the new node
|
|
218
|
+
const
|
|
219
|
+
parent = selection[0],
|
|
220
|
+
parentDatum = getNodeData(parent.id);
|
|
221
|
+
|
|
222
|
+
if (parent.hasChildren && !parent.areChildrenLoaded) {
|
|
223
|
+
await loadChildren(parentDatum);
|
|
219
224
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
225
|
+
},
|
|
226
|
+
onAfterAdd = async (entity) => {
|
|
227
|
+
// Expand the parent before showing the new node
|
|
228
|
+
const
|
|
229
|
+
parent = entity.parent,
|
|
230
|
+
parentDatum = getNodeData(parent.id);
|
|
231
|
+
|
|
232
|
+
if (!parentDatum.isExpanded) {
|
|
233
|
+
parentDatum.isExpanded = true;
|
|
226
234
|
}
|
|
235
|
+
|
|
236
|
+
// Add the entity to the tree
|
|
237
|
+
const entityDatum = buildTreeNodeDatum(entity);
|
|
238
|
+
parentDatum.children.unshift(entityDatum);
|
|
239
|
+
forceUpdate();
|
|
227
240
|
},
|
|
228
|
-
|
|
241
|
+
onBeforeEditSave = (entities) => {
|
|
242
|
+
onBeforeSave(entities);
|
|
243
|
+
},
|
|
244
|
+
onAfterEdit = async (entities) => {
|
|
245
|
+
// Refresh the node's display
|
|
229
246
|
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
247
|
+
node = entities[0],
|
|
248
|
+
existingDatum = getNodeData(node.id), // TODO: Make this work for >1 entity
|
|
249
|
+
newDatum = buildTreeNodeDatum(node);
|
|
250
|
+
|
|
251
|
+
// copy the updated data to existingDatum
|
|
252
|
+
_.merge(existingDatum, newDatum);
|
|
253
|
+
existingDatum.isLoading = false;
|
|
254
|
+
forceUpdate();
|
|
255
|
+
},
|
|
256
|
+
onBeforeDeleteSave = (entities) => {
|
|
257
|
+
onBeforeSave(entities);
|
|
258
|
+
},
|
|
259
|
+
onBeforeSave = (entities) => {
|
|
260
|
+
const
|
|
261
|
+
node = entities[0],
|
|
262
|
+
datum = getNodeData(node.id); // TODO: Make this work for >1 entity
|
|
263
|
+
|
|
264
|
+
datum.isLoading = true;
|
|
265
|
+
forceUpdate();
|
|
266
|
+
},
|
|
267
|
+
onAfterDelete = async (entities) => {
|
|
268
|
+
// TODO: Refresh the parent node
|
|
269
|
+
|
|
270
|
+
debugger;
|
|
271
|
+
},
|
|
272
|
+
onToggle = (datum) => {
|
|
273
|
+
if (datum.isLoading) {
|
|
274
|
+
return;
|
|
254
275
|
}
|
|
255
|
-
const items = _.map(buttons, getIconButtonFromConfig);
|
|
256
276
|
|
|
257
|
-
|
|
258
|
-
key="searchNodes"
|
|
259
|
-
flex={1}
|
|
260
|
-
placeholder="Find tree node"
|
|
261
|
-
onChangeText={(val) => setTreeSearchValue(val)}
|
|
262
|
-
onKeyPress={(e, value) => {
|
|
263
|
-
if (e.key === 'Enter') {
|
|
264
|
-
onSearchTree(value);
|
|
265
|
-
}
|
|
266
|
-
}}
|
|
267
|
-
value={treeSearchValue}
|
|
268
|
-
autoSubmit={false}
|
|
269
|
-
/>);
|
|
277
|
+
datum.isExpanded = !datum.isExpanded;
|
|
270
278
|
|
|
271
|
-
|
|
279
|
+
if (datum.isExpanded && datum.item.repository?.isRemote && datum.item.hasChildren && !datum.item.areChildrenLoaded) {
|
|
280
|
+
loadChildren(datum, 1);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!datum.isExpanded && datumContainsSelection(datum)) {
|
|
285
|
+
deselectAll();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
forceUpdate();
|
|
272
289
|
},
|
|
273
|
-
|
|
274
|
-
|
|
290
|
+
onCollapseAll = (setNewTreeNodeData = true) => {
|
|
291
|
+
// Go through whole tree and collapse all nodes
|
|
292
|
+
const newTreeNodeData = _.clone(getTreeNodeData());
|
|
293
|
+
collapseNodes(newTreeNodeData);
|
|
294
|
+
|
|
295
|
+
if (setNewTreeNodeData) {
|
|
296
|
+
setTreeNodeData(newTreeNodeData);
|
|
297
|
+
}
|
|
298
|
+
return newTreeNodeData;
|
|
299
|
+
},
|
|
300
|
+
onSearchTree = async (value) => {
|
|
301
|
+
let found = [];
|
|
302
|
+
if (Repository?.isRemote) {
|
|
303
|
+
// Search tree on server
|
|
304
|
+
found = await Repository.searchNodes(value);
|
|
305
|
+
} else {
|
|
306
|
+
// Search local tree data
|
|
307
|
+
found = findTreeNodesByText(value);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const isMultipleHits = found.length > 1;
|
|
311
|
+
if (!isMultipleHits) {
|
|
312
|
+
expandPath(found[0].path);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const searchFormData = [];
|
|
317
|
+
_.each(found, (item) => {
|
|
318
|
+
searchFormData.push([item.id, getNodeText(item)]);
|
|
319
|
+
});
|
|
320
|
+
setSearchFormData(searchFormData);
|
|
321
|
+
setSearchResults(found);
|
|
322
|
+
setIsSearchModalShown(true);
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
// utilities
|
|
326
|
+
getNodeData = (itemId) => {
|
|
327
|
+
function findNodeById(node, id) {
|
|
328
|
+
if (node.item.id === id) {
|
|
329
|
+
return node;
|
|
330
|
+
}
|
|
331
|
+
if (!_.isEmpty(node.children)) {
|
|
332
|
+
return _.find(node.children, (node2) => {
|
|
333
|
+
return findNodeById(node2, id);
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
let found = null;
|
|
339
|
+
_.each(getTreeNodeData(), (node) => {
|
|
340
|
+
const foundNode = findNodeById(node, itemId);
|
|
341
|
+
if (foundNode) {
|
|
342
|
+
found = foundNode;
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
return found;
|
|
275
347
|
},
|
|
276
348
|
buildTreeNodeDatum = (treeNode) => {
|
|
277
349
|
// Build the data-representation of one node and its children,
|
|
@@ -286,7 +358,7 @@ function TreeComponent(props) {
|
|
|
286
358
|
iconCollapsed: getNodeIcon(treeNode, false),
|
|
287
359
|
iconExpanded: getNodeIcon(treeNode, true),
|
|
288
360
|
iconLeaf: getNodeIcon(treeNode),
|
|
289
|
-
isExpanded: isRoot, // all non-root treeNodes are
|
|
361
|
+
isExpanded: isRoot, // all non-root treeNodes are collapsed by default
|
|
290
362
|
isVisible: isRoot ? areRootsVisible : true,
|
|
291
363
|
isLoading: false,
|
|
292
364
|
children,
|
|
@@ -301,131 +373,56 @@ function TreeComponent(props) {
|
|
|
301
373
|
});
|
|
302
374
|
return data;
|
|
303
375
|
},
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
376
|
+
buildAndSetTreeNodeData = async () => {
|
|
377
|
+
let nodes = [];
|
|
378
|
+
if (Repository) {
|
|
379
|
+
if (!Repository.areRootNodesLoaded) {
|
|
380
|
+
nodes = await Repository.loadRootNodes(1);
|
|
381
|
+
} else {
|
|
382
|
+
nodes = Repository.getRootNodes();
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
nodes = assembleDataTreeNodes();
|
|
313
386
|
}
|
|
314
387
|
|
|
315
|
-
|
|
316
|
-
|
|
388
|
+
setTreeNodeData(buildTreeNodeData(nodes));
|
|
389
|
+
},
|
|
390
|
+
datumContainsSelection = (datum) => {
|
|
391
|
+
if (_.isEmpty(selection)) {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
const
|
|
395
|
+
selectionIds = _.map(selection, (item) => item.id),
|
|
396
|
+
datumIds = getDatumChildIds(datum),
|
|
397
|
+
intersection = selectionIds.filter(x => datumIds.includes(x));
|
|
317
398
|
|
|
318
|
-
return
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
if (isReorderMode) {
|
|
326
|
-
return
|
|
327
|
-
}
|
|
328
|
-
switch (e.detail) {
|
|
329
|
-
case 1: // single click
|
|
330
|
-
onNodeClick(item, e); // sets selection
|
|
331
|
-
break;
|
|
332
|
-
case 2: // double click
|
|
333
|
-
if (!isSelected) { // If a row was already selected when double-clicked, the first click will deselect it,
|
|
334
|
-
onNodeClick(item, e); // so reselect it
|
|
335
|
-
}
|
|
336
|
-
if (onEdit) {
|
|
337
|
-
onEdit();
|
|
338
|
-
}
|
|
339
|
-
break;
|
|
340
|
-
case 3: // triple click
|
|
341
|
-
break;
|
|
342
|
-
default:
|
|
343
|
-
}
|
|
344
|
-
}}
|
|
345
|
-
onLongPress={(e) => {
|
|
346
|
-
if (e.preventDefault && e.cancelable) {
|
|
347
|
-
e.preventDefault();
|
|
348
|
-
}
|
|
349
|
-
if (isReorderMode) {
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
399
|
+
return !_.isEmpty(intersection);
|
|
400
|
+
},
|
|
401
|
+
findTreeNodesByText = (text) => {
|
|
402
|
+
// Helper for onSearchTree
|
|
403
|
+
// Searches whole treeNodeData for any matching items
|
|
404
|
+
// Returns multiple nodes
|
|
352
405
|
|
|
353
|
-
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// context menu
|
|
358
|
-
const selection = [item];
|
|
359
|
-
setSelection(selection);
|
|
360
|
-
if (onContextMenu) {
|
|
361
|
-
onContextMenu(item, e, selection, setSelection);
|
|
362
|
-
}
|
|
363
|
-
}}
|
|
364
|
-
flexDirection="row"
|
|
365
|
-
ml={((areRootsVisible ? depth : depth -1) * 20) + 'px'}
|
|
366
|
-
>
|
|
367
|
-
{({
|
|
368
|
-
isHovered,
|
|
369
|
-
isFocused,
|
|
370
|
-
isPressed,
|
|
371
|
-
}) => {
|
|
372
|
-
let bg = nodeProps.bg || styles.TREE_NODE_BG,
|
|
373
|
-
mixWith;
|
|
374
|
-
if (isSelected) {
|
|
375
|
-
if (showHovers && isHovered) {
|
|
376
|
-
mixWith = styles.TREE_NODE_SELECTED_HOVER_BG;
|
|
377
|
-
} else {
|
|
378
|
-
mixWith = styles.TREE_NODE_SELECTED_BG;
|
|
379
|
-
}
|
|
380
|
-
} else if (showHovers && isHovered) {
|
|
381
|
-
mixWith = styles.TREE_NODE_HOVER_BG;
|
|
382
|
-
}
|
|
383
|
-
if (mixWith) {
|
|
384
|
-
const
|
|
385
|
-
mixWithObj = nbToRgb(mixWith),
|
|
386
|
-
ratio = mixWithObj.alpha ? 1 - mixWithObj.alpha : 0.5;
|
|
387
|
-
bg = colourMixer.blend(bg, ratio, mixWithObj.color);
|
|
388
|
-
}
|
|
389
|
-
let WhichTreeNode = TreeNode,
|
|
390
|
-
rowReorderProps = {};
|
|
391
|
-
if (canNodesReorder && isReorderMode) {
|
|
392
|
-
WhichTreeNode = ReorderableTreeNode;
|
|
393
|
-
rowReorderProps = {
|
|
394
|
-
mode: VERTICAL,
|
|
395
|
-
onDragStart: onNodeReorderDragStart,
|
|
396
|
-
onDrag: onNodeReorderDrag,
|
|
397
|
-
onDragStop: onNodeReorderDragStop,
|
|
398
|
-
proxyParent: treeRef.current?.getScrollableNode().children[0],
|
|
399
|
-
proxyPositionRelativeToParent: true,
|
|
400
|
-
getParentNode: (node) => node.parentElement.parentElement.parentElement,
|
|
401
|
-
getProxy: getReorderProxy,
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
return <WhichTreeNode
|
|
406
|
-
nodeProps={nodeProps}
|
|
407
|
-
bg={bg}
|
|
408
|
-
datum={datum}
|
|
409
|
-
onToggle={onToggle}
|
|
406
|
+
const regex = new RegExp(text, 'i'); // instead of matching based on full text match, search for a partial match
|
|
410
407
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
408
|
+
function searchChildren(children, found = []) {
|
|
409
|
+
_.each(children, (child) => {
|
|
410
|
+
if (child.text.match(regex)) {
|
|
411
|
+
found.push(child);
|
|
412
|
+
}
|
|
413
|
+
if (child.children) {
|
|
414
|
+
searchChildren(child.children, found);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
return found;
|
|
418
|
+
}
|
|
419
|
+
return searchChildren(treeNodeData);
|
|
416
420
|
},
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if (datum.children.length && datum.isExpanded) {
|
|
424
|
-
const childTreeNodes = renderTreeNodes(datum.children); // recursion
|
|
425
|
-
nodes = nodes.concat(childTreeNodes);
|
|
426
|
-
}
|
|
427
|
-
});
|
|
428
|
-
return nodes;
|
|
421
|
+
getTreeNodeByNodeId = (node_id) => {
|
|
422
|
+
if (Repository) {
|
|
423
|
+
return Repository.getById(node_id);
|
|
424
|
+
}
|
|
425
|
+
return data[node_id]; // TODO: This is probably not right!
|
|
429
426
|
},
|
|
430
427
|
getDatumChildIds = (datum) => {
|
|
431
428
|
let ids = [];
|
|
@@ -439,30 +436,6 @@ function TreeComponent(props) {
|
|
|
439
436
|
});
|
|
440
437
|
return ids;
|
|
441
438
|
},
|
|
442
|
-
datumContainsSelection = (datum) => {
|
|
443
|
-
if (_.isEmpty(selection)) {
|
|
444
|
-
return false;
|
|
445
|
-
}
|
|
446
|
-
const
|
|
447
|
-
selectionIds = _.map(selection, (item) => item.id),
|
|
448
|
-
datumIds = getDatumChildIds(datum),
|
|
449
|
-
intersection = selectionIds.filter(x => datumIds.includes(x));
|
|
450
|
-
|
|
451
|
-
return !_.isEmpty(intersection);
|
|
452
|
-
},
|
|
453
|
-
buildAndSetTreeNodeData = async () => {
|
|
454
|
-
let rootNodes;
|
|
455
|
-
if (Repository) {
|
|
456
|
-
if (!Repository.areRootNodesLoaded) {
|
|
457
|
-
rootNodes = await Repository.getRootNodes(1);
|
|
458
|
-
}
|
|
459
|
-
} else {
|
|
460
|
-
rootNodes = assembleDataTreeNodes();
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const treeNodeData = buildTreeNodeData(rootNodes);
|
|
464
|
-
setTreeNodeData(treeNodeData);
|
|
465
|
-
},
|
|
466
439
|
assembleDataTreeNodes = () => {
|
|
467
440
|
// Populates the TreeNodes with .parent and .children references
|
|
468
441
|
// NOTE: This is only for 'data', not for Repositories!
|
|
@@ -502,47 +475,27 @@ function TreeComponent(props) {
|
|
|
502
475
|
treeNode.depth = i;
|
|
503
476
|
treeNode.hash = treeNode[idIx];
|
|
504
477
|
|
|
505
|
-
if (treeNode.isRoot) {
|
|
506
|
-
treeNodes.push(treeNode);
|
|
507
|
-
}
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
return treeNodes;
|
|
511
|
-
},
|
|
512
|
-
reloadTree = () => {
|
|
513
|
-
Repository.areRootNodesLoaded = false;
|
|
514
|
-
return buildAndSetTreeNodeData();
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
// Button handlers
|
|
518
|
-
onToggle = (datum) => {
|
|
519
|
-
if (datum.isLoading) {
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
datum.isExpanded = !datum.isExpanded;
|
|
524
|
-
|
|
525
|
-
if (datum.isExpanded && datum.item.repository?.isRemote && datum.item.hasChildren && !datum.item.areChildrenLoaded) {
|
|
526
|
-
loadChildren(datum, 1);
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
478
|
+
if (treeNode.isRoot) {
|
|
479
|
+
treeNodes.push(treeNode);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
529
482
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
483
|
+
return treeNodes;
|
|
484
|
+
},
|
|
485
|
+
reloadTree = () => {
|
|
486
|
+
Repository.areRootNodesLoaded = false;
|
|
487
|
+
return buildAndSetTreeNodeData();
|
|
535
488
|
},
|
|
536
|
-
loadChildren = async (datum, depth) => {
|
|
489
|
+
loadChildren = async (datum, depth = 1) => {
|
|
537
490
|
// Show loading indicator (spinner underneath current node?)
|
|
538
491
|
datum.isLoading = true;
|
|
539
492
|
forceUpdate();
|
|
540
493
|
|
|
541
494
|
try {
|
|
542
495
|
|
|
543
|
-
const children = await datum.item.loadChildren(
|
|
544
|
-
|
|
545
|
-
datum.
|
|
496
|
+
const children = await datum.item.loadChildren(depth);
|
|
497
|
+
datum.children = buildTreeNodeData(children);
|
|
498
|
+
datum.isExpanded = true;
|
|
546
499
|
|
|
547
500
|
} catch (err) {
|
|
548
501
|
// TODO: how do I handle errors?
|
|
@@ -556,16 +509,6 @@ function TreeComponent(props) {
|
|
|
556
509
|
datum.isLoading = false;
|
|
557
510
|
forceUpdate();
|
|
558
511
|
},
|
|
559
|
-
onCollapseAll = (setNewTreeNodeData = true) => {
|
|
560
|
-
// Go through whole tree and collapse all nodes
|
|
561
|
-
const newTreeNodeData = _.clone(treeNodeData);
|
|
562
|
-
collapseNodes(newTreeNodeData);
|
|
563
|
-
|
|
564
|
-
if (setNewTreeNodeData) {
|
|
565
|
-
setTreeNodeData(newTreeNodeData);
|
|
566
|
-
}
|
|
567
|
-
return newTreeNodeData;
|
|
568
|
-
},
|
|
569
512
|
collapseNodes = (nodes) => {
|
|
570
513
|
_.each(nodes, (node) => {
|
|
571
514
|
node.isExpanded = false;
|
|
@@ -574,61 +517,9 @@ function TreeComponent(props) {
|
|
|
574
517
|
}
|
|
575
518
|
});
|
|
576
519
|
},
|
|
577
|
-
onSearchTree = async (value) => {
|
|
578
|
-
let found = [];
|
|
579
|
-
if (Repository?.isRemote) {
|
|
580
|
-
// Search tree on server
|
|
581
|
-
found = await Repository.searchNodes(value);
|
|
582
|
-
} else {
|
|
583
|
-
// Search local tree data
|
|
584
|
-
found = findTreeNodesByText(value);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
const isMultipleHits = found.length > 1;
|
|
588
|
-
if (!isMultipleHits) {
|
|
589
|
-
expandPath(found[0].path);
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
const searchFormData = [];
|
|
594
|
-
_.each(found, (item) => {
|
|
595
|
-
searchFormData.push([item.id, getNodeText(item)]);
|
|
596
|
-
});
|
|
597
|
-
setSearchFormData(searchFormData);
|
|
598
|
-
setSearchResults(found);
|
|
599
|
-
setIsSearchModalShown(true);
|
|
600
|
-
},
|
|
601
|
-
findTreeNodesByText = (text) => {
|
|
602
|
-
// Helper for onSearchTree
|
|
603
|
-
// Searches whole treeNodeData for any matching items
|
|
604
|
-
// Returns multiple nodes
|
|
605
|
-
|
|
606
|
-
const regex = new RegExp(text, 'i'); // instead of matching based on full text match, search for a partial match
|
|
607
|
-
|
|
608
|
-
function searchChildren(children, found = []) {
|
|
609
|
-
_.each(children, (child) => {
|
|
610
|
-
if (child.text.match(regex)) {
|
|
611
|
-
found.push(child);
|
|
612
|
-
}
|
|
613
|
-
if (child.children) {
|
|
614
|
-
searchChildren(child.children, found);
|
|
615
|
-
}
|
|
616
|
-
});
|
|
617
|
-
return found;
|
|
618
|
-
}
|
|
619
|
-
return searchChildren(treeNodeData);
|
|
620
|
-
},
|
|
621
|
-
getTreeNodeByNodeId = (node_id) => {
|
|
622
|
-
if (Repository) {
|
|
623
|
-
return Repository.getById(node_id);
|
|
624
|
-
}
|
|
625
|
-
return data[node_id]; // TODO: This is probably not right!
|
|
626
|
-
},
|
|
627
520
|
expandPath = async (path) => {
|
|
628
|
-
// Helper for onSearchTree
|
|
629
|
-
|
|
630
521
|
// First, close thw whole tree.
|
|
631
|
-
let newTreeNodeData = _.clone(
|
|
522
|
+
let newTreeNodeData = _.clone(getTreeNodeData());
|
|
632
523
|
collapseNodes(newTreeNodeData);
|
|
633
524
|
|
|
634
525
|
// As it navigates down, it will expand the appropriate branches,
|
|
@@ -692,7 +583,185 @@ function TreeComponent(props) {
|
|
|
692
583
|
|
|
693
584
|
},
|
|
694
585
|
|
|
695
|
-
//
|
|
586
|
+
// render
|
|
587
|
+
getHeaderToolbarItems = () => {
|
|
588
|
+
const
|
|
589
|
+
buttons = [
|
|
590
|
+
{
|
|
591
|
+
key: 'searchBtn',
|
|
592
|
+
text: 'Search tree',
|
|
593
|
+
handler: onSearchTree,
|
|
594
|
+
icon: MagnifyingGlass,
|
|
595
|
+
isDisabled: !treeSearchValue.length,
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
key: 'collapseBtn',
|
|
599
|
+
text: 'Collapse whole tree',
|
|
600
|
+
handler: onCollapseAll,
|
|
601
|
+
icon: Collapse,
|
|
602
|
+
isDisabled: false,
|
|
603
|
+
},
|
|
604
|
+
];
|
|
605
|
+
if (canNodesReorder) {
|
|
606
|
+
buttons.push({
|
|
607
|
+
key: 'reorderBtn',
|
|
608
|
+
text: 'Enter reorder mode',
|
|
609
|
+
handler: () => setIsReorderMode(!isReorderMode),
|
|
610
|
+
icon: isReorderMode ? NoReorderRows : ReorderRows,
|
|
611
|
+
isDisabled: false,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
const items = _.map(buttons, getIconButtonFromConfig);
|
|
615
|
+
|
|
616
|
+
items.unshift(<Input // Add text input to beginning of header items
|
|
617
|
+
key="searchNodes"
|
|
618
|
+
flex={1}
|
|
619
|
+
placeholder="Find tree node"
|
|
620
|
+
onChangeText={(val) => setTreeSearchValue(val)}
|
|
621
|
+
onKeyPress={(e, value) => {
|
|
622
|
+
if (e.key === 'Enter') {
|
|
623
|
+
onSearchTree(value);
|
|
624
|
+
}
|
|
625
|
+
}}
|
|
626
|
+
value={treeSearchValue}
|
|
627
|
+
autoSubmit={false}
|
|
628
|
+
/>);
|
|
629
|
+
|
|
630
|
+
return items;
|
|
631
|
+
},
|
|
632
|
+
getFooterToolbarItems = () => {
|
|
633
|
+
return _.map(additionalToolbarButtons, getIconButtonFromConfig);
|
|
634
|
+
},
|
|
635
|
+
renderTreeNode = (datum) => {
|
|
636
|
+
if (!datum.isVisible) {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
const item = datum.item;
|
|
640
|
+
if (item.isDestroyed) {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
const depth = item.depth;
|
|
644
|
+
|
|
645
|
+
let nodeProps = getNodeProps ? getNodeProps(item) : {},
|
|
646
|
+
isSelected = isInSelection(item);
|
|
647
|
+
|
|
648
|
+
return <Pressable
|
|
649
|
+
// {...testProps(Repository ? Repository.schema.name + '-' + item.id : item.id)}
|
|
650
|
+
key={item.hash}
|
|
651
|
+
onPress={(e) => {
|
|
652
|
+
if (e.preventDefault && e.cancelable) {
|
|
653
|
+
e.preventDefault();
|
|
654
|
+
}
|
|
655
|
+
if (isReorderMode) {
|
|
656
|
+
return
|
|
657
|
+
}
|
|
658
|
+
switch (e.detail) {
|
|
659
|
+
case 1: // single click
|
|
660
|
+
onNodeClick(item, e); // sets selection
|
|
661
|
+
break;
|
|
662
|
+
case 2: // double click
|
|
663
|
+
if (!isSelected) { // If a row was already selected when double-clicked, the first click will deselect it,
|
|
664
|
+
onNodeClick(item, e); // so reselect it
|
|
665
|
+
}
|
|
666
|
+
if (onEdit) {
|
|
667
|
+
onEdit();
|
|
668
|
+
}
|
|
669
|
+
break;
|
|
670
|
+
case 3: // triple click
|
|
671
|
+
break;
|
|
672
|
+
default:
|
|
673
|
+
}
|
|
674
|
+
}}
|
|
675
|
+
onLongPress={(e) => {
|
|
676
|
+
if (e.preventDefault && e.cancelable) {
|
|
677
|
+
e.preventDefault();
|
|
678
|
+
}
|
|
679
|
+
if (isReorderMode) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (!setSelection) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// context menu
|
|
688
|
+
const selection = [item];
|
|
689
|
+
setSelection(selection);
|
|
690
|
+
if (onContextMenu) {
|
|
691
|
+
onContextMenu(item, e, selection, setSelection);
|
|
692
|
+
}
|
|
693
|
+
}}
|
|
694
|
+
flexDirection="row"
|
|
695
|
+
ml={((areRootsVisible ? depth : depth -1) * 20) + 'px'}
|
|
696
|
+
>
|
|
697
|
+
{({
|
|
698
|
+
isHovered,
|
|
699
|
+
isFocused,
|
|
700
|
+
isPressed,
|
|
701
|
+
}) => {
|
|
702
|
+
let bg = nodeProps.bg || styles.TREE_NODE_BG,
|
|
703
|
+
mixWith;
|
|
704
|
+
if (isSelected) {
|
|
705
|
+
if (showHovers && isHovered) {
|
|
706
|
+
mixWith = styles.TREE_NODE_SELECTED_HOVER_BG;
|
|
707
|
+
} else {
|
|
708
|
+
mixWith = styles.TREE_NODE_SELECTED_BG;
|
|
709
|
+
}
|
|
710
|
+
} else if (showHovers && isHovered) {
|
|
711
|
+
mixWith = styles.TREE_NODE_HOVER_BG;
|
|
712
|
+
}
|
|
713
|
+
if (mixWith) {
|
|
714
|
+
const
|
|
715
|
+
mixWithObj = nbToRgb(mixWith),
|
|
716
|
+
ratio = mixWithObj.alpha ? 1 - mixWithObj.alpha : 0.5;
|
|
717
|
+
bg = colourMixer.blend(bg, ratio, mixWithObj.color);
|
|
718
|
+
}
|
|
719
|
+
let WhichTreeNode = TreeNode,
|
|
720
|
+
rowReorderProps = {};
|
|
721
|
+
if (canNodesReorder && isReorderMode) {
|
|
722
|
+
WhichTreeNode = ReorderableTreeNode;
|
|
723
|
+
rowReorderProps = {
|
|
724
|
+
mode: VERTICAL,
|
|
725
|
+
onDragStart: onNodeReorderDragStart,
|
|
726
|
+
onDrag: onNodeReorderDrag,
|
|
727
|
+
onDragStop: onNodeReorderDragStop,
|
|
728
|
+
proxyParent: treeRef.current?.getScrollableNode().children[0],
|
|
729
|
+
proxyPositionRelativeToParent: true,
|
|
730
|
+
getParentNode: (node) => node.parentElement.parentElement.parentElement,
|
|
731
|
+
getProxy: getReorderProxy,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return <WhichTreeNode
|
|
736
|
+
nodeProps={nodeProps}
|
|
737
|
+
bg={bg}
|
|
738
|
+
datum={datum}
|
|
739
|
+
onToggle={onToggle}
|
|
740
|
+
|
|
741
|
+
// fields={fields}
|
|
742
|
+
{...rowReorderProps}
|
|
743
|
+
/>;
|
|
744
|
+
}}
|
|
745
|
+
</Pressable>;
|
|
746
|
+
},
|
|
747
|
+
renderTreeNodes = (data) => {
|
|
748
|
+
let nodes = [];
|
|
749
|
+
_.each(data, (datum) => {
|
|
750
|
+
const node = renderTreeNode(datum);
|
|
751
|
+
if (!node) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
nodes.push(node);
|
|
755
|
+
|
|
756
|
+
if (datum.children.length && datum.isExpanded) {
|
|
757
|
+
const childTreeNodes = renderTreeNodes(datum.children); // recursion
|
|
758
|
+
nodes = nodes.concat(childTreeNodes);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
return nodes;
|
|
762
|
+
},
|
|
763
|
+
|
|
764
|
+
// drag/drop
|
|
696
765
|
getReorderProxy = (node) => {
|
|
697
766
|
const
|
|
698
767
|
row = node.parentElement.parentElement,
|
|
@@ -967,16 +1036,12 @@ function TreeComponent(props) {
|
|
|
967
1036
|
|
|
968
1037
|
Repository.on('beforeLoad', setTrue);
|
|
969
1038
|
Repository.on('load', setFalse);
|
|
970
|
-
Repository.ons(['changePage', 'changePageSize',], deselectAll);
|
|
971
|
-
Repository.ons(['changeData', 'change'], buildAndSetTreeNodeData);
|
|
972
1039
|
Repository.on('changeFilters', reloadTree);
|
|
973
1040
|
Repository.on('changeSorters', reloadTree);
|
|
974
1041
|
|
|
975
1042
|
return () => {
|
|
976
1043
|
Repository.off('beforeLoad', setTrue);
|
|
977
1044
|
Repository.off('load', setFalse);
|
|
978
|
-
Repository.offs(['changePage', 'changePageSize',], deselectAll);
|
|
979
|
-
Repository.offs(['changeData', 'change'], buildAndSetTreeNodeData);
|
|
980
1045
|
Repository.off('changeFilters', reloadTree);
|
|
981
1046
|
Repository.off('changeSorters', reloadTree);
|
|
982
1047
|
};
|
|
@@ -993,17 +1058,26 @@ function TreeComponent(props) {
|
|
|
993
1058
|
}
|
|
994
1059
|
Repository.filter(selectorId, id, false); // so it doesn't clear existing filters
|
|
995
1060
|
}
|
|
996
|
-
|
|
997
1061
|
}, [selectorId, selectorSelected]);
|
|
998
1062
|
|
|
1063
|
+
setWithEditListeners({ // Update withEdit's listeners on every render
|
|
1064
|
+
onBeforeAdd,
|
|
1065
|
+
onAfterAdd,
|
|
1066
|
+
onBeforeEditSave,
|
|
1067
|
+
onAfterEdit,
|
|
1068
|
+
onBeforeDeleteSave,
|
|
1069
|
+
onAfterDelete,
|
|
1070
|
+
});
|
|
1071
|
+
|
|
999
1072
|
const
|
|
1000
|
-
headerToolbarItemComponents = useMemo(() => getHeaderToolbarItems(), [treeSearchValue,
|
|
1001
|
-
footerToolbarItemComponents = useMemo(() => getFooterToolbarItems(), [additionalToolbarButtons, isReorderMode,
|
|
1073
|
+
headerToolbarItemComponents = useMemo(() => getHeaderToolbarItems(), [treeSearchValue, getTreeNodeData()]),
|
|
1074
|
+
footerToolbarItemComponents = useMemo(() => getFooterToolbarItems(), [additionalToolbarButtons, isReorderMode, getTreeNodeData()]);
|
|
1002
1075
|
|
|
1003
1076
|
if (!isReady) {
|
|
1004
1077
|
return null;
|
|
1005
1078
|
}
|
|
1006
|
-
|
|
1079
|
+
|
|
1080
|
+
const treeNodes = renderTreeNodes(getTreeNodeData());
|
|
1007
1081
|
|
|
1008
1082
|
// headers & footers
|
|
1009
1083
|
let treeFooterComponent = null;
|
|
@@ -1029,7 +1103,7 @@ function TreeComponent(props) {
|
|
|
1029
1103
|
deselectAll();
|
|
1030
1104
|
}
|
|
1031
1105
|
}}>
|
|
1032
|
-
{!treeNodes?.length ? <NoRecordsFound text={noneFoundText} onRefresh={
|
|
1106
|
+
{!treeNodes?.length ? <NoRecordsFound text={noneFoundText} onRefresh={reloadTree} /> :
|
|
1033
1107
|
treeNodes}
|
|
1034
1108
|
</Column>
|
|
1035
1109
|
|
package/src/Constants/Styles.js
CHANGED
|
@@ -95,7 +95,7 @@ const defaults = {
|
|
|
95
95
|
TREE_NODE_SELECTED_BG: 'selected',
|
|
96
96
|
TREE_NODE_SELECTED_HOVER_BG: 'selectedHover',
|
|
97
97
|
TOOLBAR_ITEMS_COLOR: 'trueGray.800',
|
|
98
|
-
TOOLBAR_ITEMS_DISABLED_COLOR: '
|
|
98
|
+
TOOLBAR_ITEMS_DISABLED_COLOR: 'trueGray.400',
|
|
99
99
|
TOOLBAR_ITEMS_ICON_SIZE: 'sm',
|
|
100
100
|
VIEWER_ANCILLARY_FONTSIZE: 22,
|
|
101
101
|
};
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import IconButton from '../Components/Buttons/IconButton.js';
|
|
2
2
|
import UiGlobals from '../UiGlobals.js';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
export default function getIconButtonFromConfig($config, ix) {
|
|
4
|
+
export default function getIconButtonFromConfig(config, ix) {
|
|
6
5
|
const
|
|
6
|
+
{
|
|
7
|
+
key,
|
|
8
|
+
text,
|
|
9
|
+
handler,
|
|
10
|
+
icon = null,
|
|
11
|
+
isDisabled = false,
|
|
12
|
+
} = config,
|
|
7
13
|
styles = UiGlobals.styles,
|
|
8
14
|
iconButtonProps = {
|
|
9
15
|
_hover: {
|
|
@@ -19,13 +25,6 @@ export default function getIconButtonFromConfig($config, ix) {
|
|
|
19
25
|
w: 20,
|
|
20
26
|
color: isDisabled ? styles.TOOLBAR_ITEMS_DISABLED_COLOR : styles.TOOLBAR_ITEMS_COLOR,
|
|
21
27
|
};
|
|
22
|
-
let {
|
|
23
|
-
key,
|
|
24
|
-
text,
|
|
25
|
-
handler,
|
|
26
|
-
icon = null,
|
|
27
|
-
isDisabled = false,
|
|
28
|
-
} = config;
|
|
29
28
|
return <IconButton
|
|
30
29
|
key={key || ix}
|
|
31
30
|
onPress={handler}
|