@onehat/ui 0.3.125 → 0.3.126
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/Hoc/Secondary/withSecondaryData.js +117 -0
- package/src/Components/Hoc/Secondary/withSecondaryEditor.js +455 -0
- package/src/Components/Hoc/Secondary/withSecondarySelection.js +377 -0
- package/src/Components/Hoc/Secondary/withSecondarySideEditor.js +50 -0
- package/src/Components/Hoc/Secondary/withSecondaryValue.js +154 -0
- package/src/Components/Hoc/Secondary/withSecondaryWindowedEditor.js +71 -0
- package/src/Components/Hoc/withEditor.js +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { useState, useEffect, } from 'react';
|
|
2
|
+
import oneHatData from '@onehat/data';
|
|
3
|
+
import _ from 'lodash';
|
|
4
|
+
|
|
5
|
+
// NOTE: This is a modified version of @onehat/ui/src/Hoc/withData
|
|
6
|
+
// This HOC will eventually get out of sync with that one, and may need to be updated.
|
|
7
|
+
|
|
8
|
+
export default function withSecondaryData(WrappedComponent) {
|
|
9
|
+
return (props) => {
|
|
10
|
+
|
|
11
|
+
if (props.secondaryDisableWithData) {
|
|
12
|
+
return <WrappedComponent {...props} />;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const
|
|
16
|
+
{
|
|
17
|
+
// For @onehat/data repositories
|
|
18
|
+
SecondaryRepository,
|
|
19
|
+
setSecondaryRepository,
|
|
20
|
+
uniqueSecondaryRepository = false,
|
|
21
|
+
secondaryModel,
|
|
22
|
+
secondaryAutoLoad, // bool
|
|
23
|
+
secondaryPageSize,
|
|
24
|
+
secondaryBaseParams,
|
|
25
|
+
|
|
26
|
+
// For plain JS data
|
|
27
|
+
secondaryData,
|
|
28
|
+
secondaryFields = ['id', 'value'],
|
|
29
|
+
secondaryIdField = 'id',
|
|
30
|
+
secondaryDisplayField = 'value',
|
|
31
|
+
secondaryIdIx,
|
|
32
|
+
secondaryDisplayIx,
|
|
33
|
+
|
|
34
|
+
// withComponent
|
|
35
|
+
self,
|
|
36
|
+
} = props,
|
|
37
|
+
propsToPass = _.omit(props, ['secondaryModel']), // passing 'secondaryModel' would mess things up if withData gets called twice (e.g. withData(...withData(...)) ), as we'd be trying to recreate SecondaryRepository twice
|
|
38
|
+
localIdIx = secondaryIdIx || (secondaryFields && secondaryIdField ? secondaryFields.indexOf(secondaryIdField) : null),
|
|
39
|
+
localDisplayIx = secondaryDisplayIx || (secondaryFields && secondaryDisplayField ? secondaryFields?.indexOf(secondaryDisplayField) : null),
|
|
40
|
+
[LocalSecondaryRepository, setLocalSecondaryRepository] = useState(SecondaryRepository || null), // simply pass on SecondaryRepository if it's already supplied
|
|
41
|
+
[isReady, setIsReady] = useState(!!LocalSecondaryRepository || !!secondaryData); // It's already ready if a LocalSecondaryRepository or secondaryData array is already set. Otherwise, we need to create the repository
|
|
42
|
+
|
|
43
|
+
// Create LocalSecondaryRepository
|
|
44
|
+
// If SecondaryRepository was submitted to this withData(), the useEffect has no effect.
|
|
45
|
+
// If it's empty, it tries to create a LocalSecondaryRepository
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!!LocalSecondaryRepository || !!secondaryData) {
|
|
48
|
+
return () => {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let repositoryId;
|
|
52
|
+
|
|
53
|
+
(async () => {
|
|
54
|
+
let SecondaryRepository;
|
|
55
|
+
if (uniqueSecondaryRepository) {
|
|
56
|
+
const schema = oneHatData.getSchema(secondaryModel);
|
|
57
|
+
SecondaryRepository = await oneHatData.createRepository({ schema });
|
|
58
|
+
repositoryId = SecondaryRepository.id;
|
|
59
|
+
} else {
|
|
60
|
+
SecondaryRepository = oneHatData.getRepository(secondaryModel);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (secondaryPageSize) {
|
|
64
|
+
SecondaryRepository.setPageSize(secondaryPageSize);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (secondaryBaseParams) {
|
|
68
|
+
SecondaryRepository.setBaseParams(secondaryBaseParams);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if (SecondaryRepository && !SecondaryRepository.isLoaded && SecondaryRepository.isRemote && !SecondaryRepository.isAutoLoad && !SecondaryRepository.isLoading) {
|
|
73
|
+
let doAutoLoad = SecondaryRepository.autoLoad;
|
|
74
|
+
if (!_.isNil(secondaryAutoLoad)) { // prop can override schema setting for secondaryAutoLoad
|
|
75
|
+
doAutoLoad = secondaryAutoLoad;
|
|
76
|
+
}
|
|
77
|
+
if (doAutoLoad) {
|
|
78
|
+
await SecondaryRepository.load();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
setLocalSecondaryRepository(SecondaryRepository);
|
|
83
|
+
if (setSecondaryRepository) { // pass it on up to higher components
|
|
84
|
+
setSecondaryRepository(SecondaryRepository);
|
|
85
|
+
}
|
|
86
|
+
if (self) {
|
|
87
|
+
self.repository = SecondaryRepository;
|
|
88
|
+
}
|
|
89
|
+
setIsReady(true);
|
|
90
|
+
})();
|
|
91
|
+
|
|
92
|
+
return () => {
|
|
93
|
+
if (repositoryId) {
|
|
94
|
+
oneHatData.deleteRepository(repositoryId);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
if (!isReady) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return <WrappedComponent
|
|
105
|
+
{...propsToPass}
|
|
106
|
+
secondaryDisableWithData={false}
|
|
107
|
+
SecondaryRepository={LocalSecondaryRepository}
|
|
108
|
+
secondaryModel={secondaryModel}
|
|
109
|
+
secondaryData={secondaryData}
|
|
110
|
+
secondaryFields={secondaryFields}
|
|
111
|
+
secondaryIdField={secondaryIdField}
|
|
112
|
+
secondaryDisplayField={secondaryDisplayField}
|
|
113
|
+
secondaryIdIx={localIdIx}
|
|
114
|
+
secondaryDisplayIx={localDisplayIx}
|
|
115
|
+
/>;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import { useEffect, useState, useRef, } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
} from 'native-base';
|
|
5
|
+
import {
|
|
6
|
+
EDITOR_MODE__VIEW,
|
|
7
|
+
EDITOR_MODE__ADD,
|
|
8
|
+
EDITOR_MODE__EDIT,
|
|
9
|
+
} from '../../../Constants/Editor.js';
|
|
10
|
+
import _ from 'lodash';
|
|
11
|
+
|
|
12
|
+
// NOTE: This is a modified version of @onehat/ui/src/Hoc/withEditor
|
|
13
|
+
// This HOC will eventually get out of sync with that one, and may need to be updated.
|
|
14
|
+
|
|
15
|
+
export default function withSecondaryEditor(WrappedComponent, isTree = false) {
|
|
16
|
+
return (props) => {
|
|
17
|
+
|
|
18
|
+
if (props.secondaryDisableWithEditor) {
|
|
19
|
+
return <WrappedComponent {...props} />;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let [secondaryEditorMode, secondarySetEditorMode] = useState(EDITOR_MODE__VIEW); // Can change below, so use 'let'
|
|
23
|
+
const {
|
|
24
|
+
secondaryUserCanEdit = true,
|
|
25
|
+
secondaryUserCanView = true,
|
|
26
|
+
secondaryCanEditorViewOnly = false, // whether the editor can *ever* change state out of 'View' mode
|
|
27
|
+
secondaryDisableAdd = false,
|
|
28
|
+
secondaryDisableEdit = false,
|
|
29
|
+
secondaryDisableDelete = false,
|
|
30
|
+
secondaryDisableDuplicate = false,
|
|
31
|
+
secondaryDisableView = false,
|
|
32
|
+
secondaryUseRemoteDuplicate = false, // call specific copyToNew function on server, rather than simple duplicate on client
|
|
33
|
+
secondaryGetRecordIdentifier = (secondarySelection) => {
|
|
34
|
+
if (secondarySelection.length > 1) {
|
|
35
|
+
return 'records?';
|
|
36
|
+
}
|
|
37
|
+
return 'record' + (secondarySelection[0].displayValue ? ' "' + secondarySelection[0].displayValue + '"' : '') + '?';
|
|
38
|
+
},
|
|
39
|
+
secondaryRecord,
|
|
40
|
+
secondaryOnChange,
|
|
41
|
+
secondaryOnSave,
|
|
42
|
+
secondaryNewEntityDisplayValue,
|
|
43
|
+
|
|
44
|
+
// withComponent
|
|
45
|
+
self,
|
|
46
|
+
|
|
47
|
+
// parent container
|
|
48
|
+
secondarySelectorId,
|
|
49
|
+
secondarySelectorSelected,
|
|
50
|
+
|
|
51
|
+
// withSecondaryData
|
|
52
|
+
SecondaryRepository,
|
|
53
|
+
|
|
54
|
+
// withSecondarySelection
|
|
55
|
+
secondarySelection,
|
|
56
|
+
secondarySetSelection,
|
|
57
|
+
|
|
58
|
+
// withAlert
|
|
59
|
+
alert,
|
|
60
|
+
confirm,
|
|
61
|
+
hideAlert,
|
|
62
|
+
} = props,
|
|
63
|
+
secondaryListeners = useRef({}),
|
|
64
|
+
secondaryEditorStateRef = useRef(),
|
|
65
|
+
secondaryNewEntityDisplayValueRef = useRef(),
|
|
66
|
+
[secondaryCurrentRecord, secondarySetCurrentRecord] = useState(null),
|
|
67
|
+
[secondaryIsAdding, setIsAdding] = useState(false),
|
|
68
|
+
[secondaryIsSaving, setIsSaving] = useState(false),
|
|
69
|
+
[secondaryIsEditorShown, secondarySetIsEditorShown] = useState(false),
|
|
70
|
+
[secondaryIsEditorViewOnly, setIsEditorViewOnly] = useState(secondaryCanEditorViewOnly), // current state of whether editor is in view-only mode
|
|
71
|
+
[secondaryLastSelection, setLastSelection] = useState(),
|
|
72
|
+
secondarySetSelectionDecorated = (newSelection) => {
|
|
73
|
+
function doIt() {
|
|
74
|
+
secondarySetSelection(newSelection);
|
|
75
|
+
}
|
|
76
|
+
const formState = secondaryEditorStateRef.current;
|
|
77
|
+
if (!_.isEmpty(formState?.dirtyFields) && newSelection !== secondarySelection && secondaryEditorMode === EDITOR_MODE__EDIT) {
|
|
78
|
+
confirm('This record has unsaved changes. Are you sure you want to cancel editing? Changes will be lost.', doIt);
|
|
79
|
+
} else {
|
|
80
|
+
doIt();
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
getListeners = () => {
|
|
84
|
+
return secondaryListeners.current;
|
|
85
|
+
},
|
|
86
|
+
setListeners = (obj) => {
|
|
87
|
+
secondaryListeners.current = obj;
|
|
88
|
+
// forceUpdate(); // we don't want to get into an infinite loop of renders. Simply directly assign the secondaryListeners in every child render
|
|
89
|
+
},
|
|
90
|
+
getNewEntityDisplayValue = () => {
|
|
91
|
+
return secondaryNewEntityDisplayValueRef.current;
|
|
92
|
+
},
|
|
93
|
+
secondaryOnAdd = async (e, values) => {
|
|
94
|
+
const defaultValues = SecondaryRepository.getSchema().getDefaultValues();
|
|
95
|
+
let addValues = values || _.clone(defaultValues);
|
|
96
|
+
|
|
97
|
+
if (secondarySelectorId && !_.isEmpty(secondarySelectorSelected)) {
|
|
98
|
+
addValues[secondarySelectorId] = secondarySelectorSelected.id;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!values && getNewEntityDisplayValue()) {
|
|
102
|
+
const displayPropertyName = SecondaryRepository.getSchema().model.displayProperty;
|
|
103
|
+
addValues[displayPropertyName] = getNewEntityDisplayValue();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (getListeners().onBeforeAdd) {
|
|
107
|
+
const listenerResult = await getListeners().onBeforeAdd();
|
|
108
|
+
if (listenerResult === false) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (isTree) {
|
|
114
|
+
if (!secondarySelection[0]) {
|
|
115
|
+
throw Error('Must select a parent node.');
|
|
116
|
+
}
|
|
117
|
+
addValues.parentId = secondarySelection[0].id;
|
|
118
|
+
} else {
|
|
119
|
+
// 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
|
|
120
|
+
const currentSorter = SecondaryRepository.sorters[0];
|
|
121
|
+
if (currentSorter.name !== SecondaryRepository.schema.model.idProperty || currentSorter.direction !== 'DESC') {
|
|
122
|
+
SecondaryRepository.pauseEvents();
|
|
123
|
+
SecondaryRepository.sort(SecondaryRepository.schema.model.idProperty, 'DESC');
|
|
124
|
+
SecondaryRepository.setPage(1);
|
|
125
|
+
SecondaryRepository.resumeEvents();
|
|
126
|
+
await SecondaryRepository.reload();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Unmap the values, so we can input true originalData
|
|
131
|
+
addValues = SecondaryRepository.unmapData(addValues);
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
setIsAdding(true);
|
|
135
|
+
setIsSaving(true);
|
|
136
|
+
const entity = await SecondaryRepository.add(addValues, false, true);
|
|
137
|
+
setIsSaving(false);
|
|
138
|
+
secondarySetSelection([entity]);
|
|
139
|
+
setIsEditorViewOnly(false);
|
|
140
|
+
secondarySetEditorMode(EDITOR_MODE__ADD);
|
|
141
|
+
secondarySetIsEditorShown(true);
|
|
142
|
+
|
|
143
|
+
if (getListeners().onAfterAdd) {
|
|
144
|
+
await getListeners().onAfterAdd(entity);
|
|
145
|
+
}
|
|
146
|
+
if (secondaryOnChange) {
|
|
147
|
+
secondaryOnChange();
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
secondaryOnEdit = async () => {
|
|
151
|
+
if (_.isEmpty(secondarySelection) || (_.isArray(secondarySelection) && (secondarySelection.length > 1 || secondarySelection[0]?.isDestroyed))) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (getListeners().onBeforeEdit) {
|
|
155
|
+
const listenerResult = await getListeners().onBeforeEdit();
|
|
156
|
+
if (listenerResult === false) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
setIsEditorViewOnly(false);
|
|
161
|
+
secondarySetEditorMode(EDITOR_MODE__EDIT);
|
|
162
|
+
secondarySetIsEditorShown(true);
|
|
163
|
+
},
|
|
164
|
+
secondaryOnDelete = async (args) => {
|
|
165
|
+
let cb = null;
|
|
166
|
+
if (_.isFunction(args)) {
|
|
167
|
+
cb = args;
|
|
168
|
+
}
|
|
169
|
+
if (_.isEmpty(secondarySelection) || (_.isArray(secondarySelection) && (secondarySelection.length > 1 || secondarySelection[0]?.isDestroyed))) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (getListeners().onBeforeDelete) {
|
|
173
|
+
const listenerResult = await getListeners().onBeforeDelete();
|
|
174
|
+
if (listenerResult === false) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const
|
|
179
|
+
isSingle = secondarySelection.length === 1,
|
|
180
|
+
firstSelection = secondarySelection[0],
|
|
181
|
+
isTree = firstSelection?.isTree,
|
|
182
|
+
hasChildren = isTree ? firstSelection?.hasChildren : false,
|
|
183
|
+
isPhantom = firstSelection?.isPhantom;
|
|
184
|
+
|
|
185
|
+
if (isSingle && isTree && hasChildren) {
|
|
186
|
+
alert({
|
|
187
|
+
title: 'Move up children?',
|
|
188
|
+
message: 'The node you have selected for deletion has children. ' +
|
|
189
|
+
'Should these children be moved up to this node\'s parent, or be deleted?',
|
|
190
|
+
buttons: [
|
|
191
|
+
<Button colorScheme="danger" onPress={() => secondaryOnMoveChildren(cb)} key="moveBtn">
|
|
192
|
+
Move Children
|
|
193
|
+
</Button>,
|
|
194
|
+
<Button colorScheme="danger" onPress={() => secondaryOnDeleteChildren(cb)} key="deleteBtn">
|
|
195
|
+
Delete Children
|
|
196
|
+
</Button>
|
|
197
|
+
],
|
|
198
|
+
includeCancel: true,
|
|
199
|
+
});
|
|
200
|
+
} else
|
|
201
|
+
if (isSingle && isPhantom) {
|
|
202
|
+
deleteRecord(cb);
|
|
203
|
+
} else {
|
|
204
|
+
const identifier = secondaryGetRecordIdentifier(secondarySelection);
|
|
205
|
+
confirm('Are you sure you want to delete the ' + identifier, () => deleteRecord(null, cb));
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
secondaryOnMoveChildren = (cb) => {
|
|
209
|
+
hideAlert();
|
|
210
|
+
deleteRecord(true, cb);
|
|
211
|
+
},
|
|
212
|
+
secondaryOnDeleteChildren = (cb) => {
|
|
213
|
+
hideAlert();
|
|
214
|
+
deleteRecord(false, cb);
|
|
215
|
+
},
|
|
216
|
+
deleteRecord = async (moveSubtreeUp, cb) => {
|
|
217
|
+
if (getListeners().onBeforeDeleteSave) {
|
|
218
|
+
await getListeners().onBeforeDeleteSave(secondarySelection);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
await SecondaryRepository.delete(secondarySelection, moveSubtreeUp);
|
|
222
|
+
if (!SecondaryRepository.isAutoSave) {
|
|
223
|
+
await SecondaryRepository.save();
|
|
224
|
+
}
|
|
225
|
+
if (getListeners().onAfterDelete) {
|
|
226
|
+
await getListeners().onAfterDelete(secondarySelection);
|
|
227
|
+
}
|
|
228
|
+
secondarySetSelection([]);
|
|
229
|
+
if (cb) {
|
|
230
|
+
cb();
|
|
231
|
+
}
|
|
232
|
+
if (secondaryOnChange) {
|
|
233
|
+
secondaryOnChange();
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
secondaryOnView = async () => {
|
|
237
|
+
if (!secondaryUserCanView) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (secondarySelection.length !== 1) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
setIsEditorViewOnly(true);
|
|
244
|
+
secondarySetEditorMode(EDITOR_MODE__VIEW);
|
|
245
|
+
secondarySetIsEditorShown(true);
|
|
246
|
+
|
|
247
|
+
if (getListeners().onAfterView) {
|
|
248
|
+
await getListeners().onAfterView();
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
secondaryOnDuplicate = async () => {
|
|
252
|
+
if (!secondaryUserCanEdit || secondaryDisableDuplicate) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (secondarySelection.length !== 1) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (secondaryUseRemoteDuplicate) {
|
|
259
|
+
return onRemoteDuplicate();
|
|
260
|
+
}
|
|
261
|
+
const
|
|
262
|
+
entity = secondarySelection[0],
|
|
263
|
+
idProperty = SecondaryRepository.getSchema().model.idProperty,
|
|
264
|
+
rawValues = _.omit(entity.rawValues, idProperty),
|
|
265
|
+
duplicate = await SecondaryRepository.add(rawValues, false, true);
|
|
266
|
+
secondarySetSelection([duplicate]);
|
|
267
|
+
secondarySetEditorMode(EDITOR_MODE__EDIT);
|
|
268
|
+
secondarySetIsEditorShown(true);
|
|
269
|
+
},
|
|
270
|
+
onRemoteDuplicate = async () => {
|
|
271
|
+
const
|
|
272
|
+
entity = secondarySelection[0],
|
|
273
|
+
duplicateEntity = await SecondaryRepository.remoteDuplicate(entity);
|
|
274
|
+
|
|
275
|
+
secondarySetSelection([duplicateEntity]);
|
|
276
|
+
secondaryOnEdit();
|
|
277
|
+
},
|
|
278
|
+
secondaryOnEditorSave = async (data, e) => {
|
|
279
|
+
const
|
|
280
|
+
what = secondaryRecord || secondarySelection,
|
|
281
|
+
isSingle = what.length === 1;
|
|
282
|
+
if (isSingle) {
|
|
283
|
+
// just update this one entity
|
|
284
|
+
what[0].setValues(data);
|
|
285
|
+
|
|
286
|
+
} else if (secondarySelection.length > 1) {
|
|
287
|
+
// Edit multiple entities
|
|
288
|
+
|
|
289
|
+
// Loop through all entities and change fields that are not null
|
|
290
|
+
const propertyNames = Object.getOwnPropertyNames(data);
|
|
291
|
+
_.each(propertyNames, (propertyName) => {
|
|
292
|
+
if (!_.isNil(data[propertyName])) {
|
|
293
|
+
_.each(what, (rec) => {
|
|
294
|
+
rec[propertyName] = data[propertyName]
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (getListeners().onBeforeEditSave) {
|
|
301
|
+
await getListeners().onBeforeEditSave(what);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
setIsSaving(true);
|
|
305
|
+
let success;
|
|
306
|
+
try {
|
|
307
|
+
await SecondaryRepository.save();
|
|
308
|
+
success = true;
|
|
309
|
+
} catch (e) {
|
|
310
|
+
success = false;
|
|
311
|
+
}
|
|
312
|
+
setIsSaving(false);
|
|
313
|
+
|
|
314
|
+
if (success) {
|
|
315
|
+
setIsAdding(false);
|
|
316
|
+
|
|
317
|
+
secondarySetEditorMode(EDITOR_MODE__EDIT);
|
|
318
|
+
// secondarySetIsEditorShown(false);
|
|
319
|
+
|
|
320
|
+
if (getListeners().onAfterEdit) {
|
|
321
|
+
await getListeners().onAfterEdit(what);
|
|
322
|
+
}
|
|
323
|
+
if (secondaryOnChange) {
|
|
324
|
+
secondaryOnChange();
|
|
325
|
+
}
|
|
326
|
+
if (secondaryOnSave) {
|
|
327
|
+
secondaryOnSave(what);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return success;
|
|
332
|
+
},
|
|
333
|
+
secondaryOnEditorCancel = () => {
|
|
334
|
+
async function doIt() {
|
|
335
|
+
const
|
|
336
|
+
isSingle = secondarySelection.length === 1,
|
|
337
|
+
isPhantom = secondarySelection[0] && !secondarySelection[0]?.isDestroyed && secondarySelection[0].isPhantom;
|
|
338
|
+
if (isSingle && isPhantom) {
|
|
339
|
+
await deleteRecord();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
setIsAdding(false);
|
|
343
|
+
secondarySetIsEditorShown(false);
|
|
344
|
+
}
|
|
345
|
+
const formState = secondaryEditorStateRef.current;
|
|
346
|
+
if (!_.isEmpty(formState.dirtyFields)) {
|
|
347
|
+
confirm('This record has unsaved changes. Are you sure you want to cancel editing? Changes will be lost.', doIt);
|
|
348
|
+
} else {
|
|
349
|
+
doIt();
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
secondaryOnEditorClose = () => {
|
|
353
|
+
if (secondaryIsAdding) {
|
|
354
|
+
secondaryOnEditorCancel();
|
|
355
|
+
}
|
|
356
|
+
secondarySetIsEditorShown(false);
|
|
357
|
+
},
|
|
358
|
+
secondaryOnEditorDelete = async () => {
|
|
359
|
+
secondaryOnDelete(() => {
|
|
360
|
+
secondarySetEditorMode(EDITOR_MODE__VIEW);
|
|
361
|
+
secondarySetIsEditorShown(false);
|
|
362
|
+
});
|
|
363
|
+
},
|
|
364
|
+
calculateEditorMode = () => {
|
|
365
|
+
let mode = secondaryEditorMode;
|
|
366
|
+
if (!secondaryCanEditorViewOnly && secondaryUserCanEdit) {
|
|
367
|
+
if (secondarySelection.length > 1) {
|
|
368
|
+
if (!secondaryDisableEdit) {
|
|
369
|
+
// For multiple entities selected, change it to edit multiple mode
|
|
370
|
+
mode = EDITOR_MODE__EDIT;
|
|
371
|
+
}
|
|
372
|
+
} else if (secondarySelection.length === 1 && !secondarySelection[0].isDestroyed && secondarySelection[0].isPhantom) {
|
|
373
|
+
if (!secondaryDisableAdd) {
|
|
374
|
+
// When a phantom entity is selected, change it to add mode.
|
|
375
|
+
mode = EDITOR_MODE__ADD;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return mode;
|
|
380
|
+
},
|
|
381
|
+
secondaryOnEditMode = () => {
|
|
382
|
+
secondarySetEditorMode(EDITOR_MODE__EDIT);
|
|
383
|
+
},
|
|
384
|
+
secondaryOnViewMode = () => {
|
|
385
|
+
function doIt() {
|
|
386
|
+
secondarySetEditorMode(EDITOR_MODE__VIEW);
|
|
387
|
+
}
|
|
388
|
+
const formState = secondaryEditorStateRef.current;
|
|
389
|
+
if (!_.isEmpty(formState.dirtyFields)) {
|
|
390
|
+
confirm('This record has unsaved changes. Are you sure you want to switch to "View" mode? Changes will be lost.', doIt);
|
|
391
|
+
} else {
|
|
392
|
+
doIt();
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
useEffect(() => {
|
|
397
|
+
// When secondarySelection changes, set the mode appropriately
|
|
398
|
+
const mode = calculateEditorMode();
|
|
399
|
+
secondarySetEditorMode(mode);
|
|
400
|
+
|
|
401
|
+
setLastSelection(secondarySelection);
|
|
402
|
+
}, [secondarySelection]);
|
|
403
|
+
|
|
404
|
+
if (self) {
|
|
405
|
+
self.secondaryAdd = secondaryOnAdd;
|
|
406
|
+
self.secondaryEdit = secondaryOnEdit;
|
|
407
|
+
self.secondaryDelete = secondaryOnDelete;
|
|
408
|
+
self.secondarnMoveChildren = secondaryOnMoveChildren;
|
|
409
|
+
self.secondaryDeleteChildren = secondaryOnDeleteChildren;
|
|
410
|
+
self.secondaryDuplicate = secondaryOnDuplicate;
|
|
411
|
+
}
|
|
412
|
+
secondaryNewEntityDisplayValueRef.current = secondaryNewEntityDisplayValue;
|
|
413
|
+
|
|
414
|
+
if (secondaryLastSelection !== secondarySelection) {
|
|
415
|
+
// NOTE: If I don't calculate this on the fly for secondarySelection changes,
|
|
416
|
+
// we see a flash of the previous state, since useEffect hasn't yet run.
|
|
417
|
+
secondaryEditorMode = calculateEditorMode();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return <WrappedComponent
|
|
421
|
+
{...props}
|
|
422
|
+
secondaryDisableWithEditor={false}
|
|
423
|
+
secondaryCurrentRecord={secondaryCurrentRecord}
|
|
424
|
+
secondarySetCurrentRecord={secondarySetCurrentRecord}
|
|
425
|
+
secondaryIsEditorShown={secondaryIsEditorShown}
|
|
426
|
+
secondaryIsEditorViewOnly={secondaryIsEditorViewOnly}
|
|
427
|
+
secondaryIsAdding={secondaryIsAdding}
|
|
428
|
+
secondaryIsSaving={secondaryIsSaving}
|
|
429
|
+
secondaryEditorMode={secondaryEditorMode}
|
|
430
|
+
secondaryOnEditMode={secondaryOnEditMode}
|
|
431
|
+
secondaryOnViewMode={secondaryOnViewMode}
|
|
432
|
+
secondaryEditorStateRef={secondaryEditorStateRef}
|
|
433
|
+
secondarySetIsEditorShown={secondarySetIsEditorShown}
|
|
434
|
+
secondaryOnAdd={(!secondaryUserCanEdit || secondaryDisableAdd) ? null : secondaryOnAdd}
|
|
435
|
+
secondaryOnEdit={(!secondaryUserCanEdit || secondaryDisableEdit) ? null : secondaryOnEdit}
|
|
436
|
+
secondaryOnDelete={(!secondaryUserCanEdit || secondaryDisableDelete) ? null : secondaryOnDelete}
|
|
437
|
+
secondaryOnView={secondaryOnView}
|
|
438
|
+
secondaryOnDuplicate={secondaryOnDuplicate}
|
|
439
|
+
secondaryOnEditorSave={secondaryOnEditorSave}
|
|
440
|
+
secondaryOnEditorCancel={secondaryOnEditorCancel}
|
|
441
|
+
secondaryOnEditorDelete={(!secondaryUserCanEdit || secondaryDisableDelete) ? null : secondaryOnEditorDelete}
|
|
442
|
+
secondaryOnEditorClose={secondaryOnEditorClose}
|
|
443
|
+
secondarySetWithEditListeners={setListeners}
|
|
444
|
+
secondaryIsEditor={true}
|
|
445
|
+
secondaryUserCanEdit={secondaryUserCanEdit}
|
|
446
|
+
secondaryUserCanView={secondaryUserCanView}
|
|
447
|
+
secondaryDisableAdd={secondaryDisableAdd}
|
|
448
|
+
secondaryDisableEdit={secondaryDisableEdit}
|
|
449
|
+
secondaryDisableDelete={secondaryDisableDelete}
|
|
450
|
+
secondaryDisableDuplicate={secondaryDisableDuplicate}
|
|
451
|
+
secondaryDisableView ={secondaryDisableView}
|
|
452
|
+
secondarySetSelection={secondarySetSelectionDecorated}
|
|
453
|
+
/>;
|
|
454
|
+
};
|
|
455
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { useState, useEffect, } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
SELECTION_MODE_SINGLE,
|
|
4
|
+
SELECTION_MODE_MULTI,
|
|
5
|
+
SELECT_UP,
|
|
6
|
+
SELECT_DOWN,
|
|
7
|
+
} from '../../../Constants/Selection.js';
|
|
8
|
+
import inArray from '../../../Functions/inArray.js';
|
|
9
|
+
import _ from 'lodash';
|
|
10
|
+
|
|
11
|
+
// NOTE: This is a modified version of @onehat/ui/src/Hoc/withSelection
|
|
12
|
+
// This HOC will eventually get out of sync with that one, and may need to be updated.
|
|
13
|
+
|
|
14
|
+
export default function withSelection(WrappedComponent) {
|
|
15
|
+
return (props) => {
|
|
16
|
+
|
|
17
|
+
if (props.secondaryDisableWithSelection) {
|
|
18
|
+
return <WrappedComponent {...props} />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (props.secondarySetSelection) {
|
|
22
|
+
// bypass everything, since we're already using withSelection() in hierarchy.
|
|
23
|
+
// For example, Combo has withSelection(), and intenally it uses Grid which also has withSelection(),
|
|
24
|
+
// but we only need it defined once for the whole thing.
|
|
25
|
+
return <WrappedComponent {...props} />;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const
|
|
29
|
+
{
|
|
30
|
+
secondarySelection,
|
|
31
|
+
secondaryDefaultSelection,
|
|
32
|
+
secondaryOnChangeSelection,
|
|
33
|
+
secondarySelectionMode = SELECTION_MODE_SINGLE, // SELECTION_MODE_MULTI, SELECTION_MODE_SINGLE
|
|
34
|
+
secondaryAutoSelectFirstItem = false,
|
|
35
|
+
fireEvent,
|
|
36
|
+
|
|
37
|
+
// withComponent
|
|
38
|
+
self,
|
|
39
|
+
|
|
40
|
+
// withSecondaryValue
|
|
41
|
+
secondaryValue,
|
|
42
|
+
secondarySetValue,
|
|
43
|
+
|
|
44
|
+
// withSecondaryData
|
|
45
|
+
SecondaryRepository,
|
|
46
|
+
secondaryData,
|
|
47
|
+
secondaryIdIx,
|
|
48
|
+
secondaryDisplayIx,
|
|
49
|
+
} = props,
|
|
50
|
+
usesWithValue = !!secondarySetValue,
|
|
51
|
+
initialSelection = secondarySelection || secondaryDefaultSelection || [],
|
|
52
|
+
[secondaryLocalSelection, setLocalSelection] = useState(initialSelection),
|
|
53
|
+
[isReady, setIsReady] = useState(secondarySelection || false), // if secondarySelection is already defined, or secondaryValue is not null and we don't need to load repository, it's ready
|
|
54
|
+
secondarySetSelection = (secondarySelection) => {
|
|
55
|
+
if (_.isEqual(secondarySelection, secondaryLocalSelection)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setLocalSelection(secondarySelection);
|
|
60
|
+
if (secondaryOnChangeSelection) {
|
|
61
|
+
secondaryOnChangeSelection(secondarySelection);
|
|
62
|
+
}
|
|
63
|
+
if (fireEvent) {
|
|
64
|
+
fireEvent('secondaryChangeSelection', secondarySelection);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
secondarySelectNext = () => {
|
|
68
|
+
secondarySelectDirection(SELECT_DOWN);
|
|
69
|
+
},
|
|
70
|
+
secondarySelectPrev = () => {
|
|
71
|
+
secondarySelectDirection(SELECT_UP);
|
|
72
|
+
},
|
|
73
|
+
secondarySelectDirection = (which) => {
|
|
74
|
+
const { items, max, min, noSelection, } = getMaxMinSelectionIndices();
|
|
75
|
+
let newIx;
|
|
76
|
+
if (which === SELECT_DOWN) {
|
|
77
|
+
if (noSelection || max === items.length -1) {
|
|
78
|
+
// select first
|
|
79
|
+
newIx = 0;
|
|
80
|
+
} else {
|
|
81
|
+
newIx = max +1;
|
|
82
|
+
}
|
|
83
|
+
} else if (which === SELECT_UP) {
|
|
84
|
+
if (noSelection || min === 0) {
|
|
85
|
+
// select last
|
|
86
|
+
newIx = items.length -1;
|
|
87
|
+
} else {
|
|
88
|
+
newIx = min -1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (items[newIx]) {
|
|
92
|
+
secondarySetSelection([items[newIx]]);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
secondaryAddToSelection = (item) => {
|
|
96
|
+
let newSelection = [];
|
|
97
|
+
newSelection = _.clone(secondaryLocalSelection); // so we get a new object, so descendants rerender
|
|
98
|
+
newSelection.push(item);
|
|
99
|
+
secondarySetSelection(newSelection);
|
|
100
|
+
},
|
|
101
|
+
secondaryRemoveFromSelection = (item) => {
|
|
102
|
+
let newSelection = [];
|
|
103
|
+
if (SecondaryRepository) {
|
|
104
|
+
newSelection = _.remove(secondaryLocalSelection, (sel) => sel !== item);
|
|
105
|
+
} else {
|
|
106
|
+
newSelection = _.remove(secondaryLocalSelection, (sel) => sel[secondaryIdIx] !== item[secondaryIdIx]);
|
|
107
|
+
}
|
|
108
|
+
secondarySetSelection(newSelection);
|
|
109
|
+
},
|
|
110
|
+
secondaryDeselectAll = () => {
|
|
111
|
+
if (!_.isEmpty(secondaryLocalSelection)) {
|
|
112
|
+
secondarySetSelection([]);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
getMaxMinSelectionIndices = () => {
|
|
116
|
+
let items,
|
|
117
|
+
currentlySelectedRowIndices = [];
|
|
118
|
+
if (SecondaryRepository) {
|
|
119
|
+
items = SecondaryRepository.getEntitiesOnPage();
|
|
120
|
+
} else {
|
|
121
|
+
items = secondaryData;
|
|
122
|
+
}
|
|
123
|
+
_.each(items, (item, ix) => {
|
|
124
|
+
if (secondaryIsInSelection(item)) {
|
|
125
|
+
currentlySelectedRowIndices.push(ix);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
if (currentlySelectedRowIndices.length === 0) {
|
|
129
|
+
return { items, noSelection: true, };
|
|
130
|
+
}
|
|
131
|
+
const
|
|
132
|
+
max = Math.max(...currentlySelectedRowIndices),
|
|
133
|
+
min = Math.min(...currentlySelectedRowIndices);
|
|
134
|
+
|
|
135
|
+
return { items, max, min, noSelection: false, };
|
|
136
|
+
},
|
|
137
|
+
secondarySelectRangeTo = (item) => {
|
|
138
|
+
// Select above max or below min to this one
|
|
139
|
+
const
|
|
140
|
+
currentSelectionLength = secondaryLocalSelection.length,
|
|
141
|
+
index = getIndexOfSelectedItem(item);
|
|
142
|
+
let newSelection = _.clone(secondaryLocalSelection); // so we get a new object, so descendants rerender
|
|
143
|
+
|
|
144
|
+
if (currentSelectionLength) {
|
|
145
|
+
const { items, max, min, } = getMaxMinSelectionIndices();
|
|
146
|
+
let i,
|
|
147
|
+
itemAtIx;
|
|
148
|
+
if (max < index) {
|
|
149
|
+
// all other secondarySelections are below the current;
|
|
150
|
+
// Range is from max+1 up to index
|
|
151
|
+
for (i = max +1; i < index; i++) {
|
|
152
|
+
itemAtIx = items[i];
|
|
153
|
+
newSelection.push(itemAtIx);
|
|
154
|
+
}
|
|
155
|
+
} else if (min > index) {
|
|
156
|
+
// all other secondarySelections are above the current;
|
|
157
|
+
// Range is from min-1 down to index
|
|
158
|
+
for (i = min -1; i > index; i--) {
|
|
159
|
+
itemAtIx = items[i];
|
|
160
|
+
newSelection.push(itemAtIx);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
newSelection.push(item);
|
|
165
|
+
secondarySetSelection(newSelection);
|
|
166
|
+
},
|
|
167
|
+
secondaryIsInSelection = (item) => {
|
|
168
|
+
if (SecondaryRepository) {
|
|
169
|
+
return inArray(item, secondaryLocalSelection);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const found = _.find(secondaryLocalSelection, (selectedItem) => {
|
|
173
|
+
return selectedItem[secondaryIdIx] === item[secondaryIdIx];
|
|
174
|
+
});
|
|
175
|
+
return !!found;
|
|
176
|
+
},
|
|
177
|
+
getIndexOfSelectedItem = (item) => {
|
|
178
|
+
// Gets ix of entity on page, or element in secondaryData array
|
|
179
|
+
if (SecondaryRepository) {
|
|
180
|
+
const entities = SecondaryRepository.getEntitiesOnPage();
|
|
181
|
+
return entities.indexOf(item);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let found;
|
|
185
|
+
_.each(secondaryData, (datum, ix) => {
|
|
186
|
+
if (datum[secondaryIdIx] === item[secondaryIdIx]) {
|
|
187
|
+
found = ix;
|
|
188
|
+
return false; // break loop
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
return found;
|
|
192
|
+
},
|
|
193
|
+
secondaryGetIdsFromLocalSelection = () => {
|
|
194
|
+
if (!secondaryLocalSelection[0]) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
const secondaryValues = _.map(secondaryLocalSelection, (item) => {
|
|
198
|
+
if (SecondaryRepository) {
|
|
199
|
+
return item.id;
|
|
200
|
+
}
|
|
201
|
+
return item[secondaryIdIx];
|
|
202
|
+
});
|
|
203
|
+
if (secondaryValues.length === 1) {
|
|
204
|
+
return secondaryValues[0];
|
|
205
|
+
}
|
|
206
|
+
return secondaryValues;
|
|
207
|
+
},
|
|
208
|
+
secondaryGetDisplayValuesFromLocalSelection = (secondarySelection) => {
|
|
209
|
+
if (!secondarySelection[0]) {
|
|
210
|
+
return '';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return _.map(secondarySelection, (item) => {
|
|
214
|
+
if (SecondaryRepository) {
|
|
215
|
+
return item.displayValue;
|
|
216
|
+
}
|
|
217
|
+
return item[secondaryDisplayIx];
|
|
218
|
+
})
|
|
219
|
+
.join(', ');
|
|
220
|
+
},
|
|
221
|
+
conformValueToLocalSelection = () => {
|
|
222
|
+
if (!secondarySetValue) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const localValue = secondaryGetIdsFromLocalSelection();
|
|
227
|
+
if (!_.isEqual(localValue, secondaryValue)) {
|
|
228
|
+
secondarySetValue(localValue);
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
conformSelectionToValue = async () => {
|
|
232
|
+
let newSelection = [];
|
|
233
|
+
if (SecondaryRepository) {
|
|
234
|
+
if (SecondaryRepository.isLoading) {
|
|
235
|
+
await SecondaryRepository.waitUntilDoneLoading();
|
|
236
|
+
}
|
|
237
|
+
// Get entity or entities that match secondaryValue
|
|
238
|
+
if ((_.isArray(secondaryValue) && !_.isEmpty(secondaryValue)) || !!secondaryValue) {
|
|
239
|
+
if (_.isArray(secondaryValue)) {
|
|
240
|
+
newSelection = SecondaryRepository.getBy((entity) => inArray(entity.id, secondaryValue));
|
|
241
|
+
} else {
|
|
242
|
+
let found = SecondaryRepository.getById(secondaryValue);
|
|
243
|
+
if (found) {
|
|
244
|
+
newSelection.push(found);
|
|
245
|
+
// } else if (SecondaryRepository?.isRemote && SecondaryRepository?.entities.length) {
|
|
246
|
+
|
|
247
|
+
// // Value cannot be found in SecondaryRepository, but actually exists on server
|
|
248
|
+
// // Try to get this secondaryValue from the server directly
|
|
249
|
+
// SecondaryRepository.filter(SecondaryRepository.schema.model.idProperty, secondaryValue);
|
|
250
|
+
// await SecondaryRepository.load();
|
|
251
|
+
// found = SecondaryRepository.getById(secondaryValue);
|
|
252
|
+
// if (found) {
|
|
253
|
+
// newSelection.push(found);
|
|
254
|
+
// }
|
|
255
|
+
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
// Get secondaryData item or items that match secondaryValue
|
|
261
|
+
if (!_.isNil(secondaryValue) && (_.isBoolean(secondaryValue) || _.isNumber(secondaryValue) || !_.isEmpty(secondaryValue))) {
|
|
262
|
+
let currentValue = secondaryValue;
|
|
263
|
+
if (!_.isArray(currentValue)) {
|
|
264
|
+
currentValue = [currentValue];
|
|
265
|
+
}
|
|
266
|
+
_.each(currentValue, (val) => {
|
|
267
|
+
// Search through secondaryData
|
|
268
|
+
const found = _.find(secondaryData, (item) => {
|
|
269
|
+
if (_.isString(item[secondaryIdIx]) && _.isString(val)) {
|
|
270
|
+
return item[secondaryIdIx].toLowerCase() === val.toLowerCase();
|
|
271
|
+
}
|
|
272
|
+
return item[secondaryIdIx] === val;
|
|
273
|
+
});
|
|
274
|
+
if (found) {
|
|
275
|
+
newSelection.push(found);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!_.isEqual(newSelection, secondaryLocalSelection)) {
|
|
282
|
+
secondarySetSelection(newSelection);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
|
|
288
|
+
(async () => {
|
|
289
|
+
|
|
290
|
+
if (usesWithValue && SecondaryRepository?.isRemote
|
|
291
|
+
&& !SecondaryRepository.isAutoLoad && !SecondaryRepository.isLoaded && !SecondaryRepository.isLoading && (!_.isNil(secondaryValue) || !_.isEmpty(secondarySelection)) || secondaryAutoSelectFirstItem) {
|
|
292
|
+
// on initialization, we can't conformSelectionToValue if the repository is not yet loaded,
|
|
293
|
+
// so first load repo, then conform to secondaryValue
|
|
294
|
+
await SecondaryRepository.load();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!_.isNil(secondaryValue)) {
|
|
298
|
+
|
|
299
|
+
await conformSelectionToValue();
|
|
300
|
+
|
|
301
|
+
} else if (!_.isEmpty(secondarySelection)) {
|
|
302
|
+
|
|
303
|
+
conformValueToLocalSelection();
|
|
304
|
+
|
|
305
|
+
} else if (secondaryAutoSelectFirstItem) {
|
|
306
|
+
let newSelection = [];
|
|
307
|
+
if (SecondaryRepository) {
|
|
308
|
+
const entitiesOnPage = SecondaryRepository.getEntitiesOnPage();
|
|
309
|
+
newSelection = entitiesOnPage[0] ? [entitiesOnPage[0]] : [];
|
|
310
|
+
} else {
|
|
311
|
+
newSelection = secondaryData[0] ? [secondaryData[0]] : [];
|
|
312
|
+
}
|
|
313
|
+
secondarySetSelection(newSelection);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
setIsReady(true);
|
|
317
|
+
|
|
318
|
+
})();
|
|
319
|
+
|
|
320
|
+
}, [secondaryValue]);
|
|
321
|
+
|
|
322
|
+
if (self) {
|
|
323
|
+
self.secondarySelection = secondaryLocalSelection;
|
|
324
|
+
self.secondarySetSelection = secondarySetSelection;
|
|
325
|
+
self.secondarySelectNext = secondarySelectNext;
|
|
326
|
+
self.secondarySelectPrev = secondarySelectPrev;
|
|
327
|
+
self.secondaryAddToSelection = secondaryAddToSelection;
|
|
328
|
+
self.secondaryRemoveFromSelection = secondaryRemoveFromSelection;
|
|
329
|
+
self.secondaryDeselectAll = secondaryDeselectAll;
|
|
330
|
+
self.secondarySelectRangeTo = secondarySelectRangeTo;
|
|
331
|
+
self.secondaryIsInSelection = secondaryIsInSelection;
|
|
332
|
+
self.secondaryGetIdsFromLocalSelection = secondaryGetIdsFromLocalSelection;
|
|
333
|
+
self.secondaryGetDisplayValuesFromSelection = secondaryGetDisplayValuesFromLocalSelection;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (usesWithValue) {
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
if (!isReady) {
|
|
339
|
+
return () => {};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
conformSelectionToValue();
|
|
343
|
+
|
|
344
|
+
}, [secondaryValue]);
|
|
345
|
+
|
|
346
|
+
useEffect(() => {
|
|
347
|
+
if (!isReady) {
|
|
348
|
+
return () => {};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
conformValueToLocalSelection();
|
|
352
|
+
|
|
353
|
+
}, [secondarySelection]);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (!isReady) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return <WrappedComponent
|
|
361
|
+
{...props}
|
|
362
|
+
secondaryDisableWithSelection={false}
|
|
363
|
+
secondarySelection={secondaryLocalSelection}
|
|
364
|
+
secondarySetSelection={secondarySetSelection}
|
|
365
|
+
secondarySelectionMode={secondarySelectionMode}
|
|
366
|
+
secondarySelectNext={secondarySelectNext}
|
|
367
|
+
secondarySelectPrev={secondarySelectPrev}
|
|
368
|
+
secondaryRemoveFromSelection={secondaryRemoveFromSelection}
|
|
369
|
+
secondaryAddToSelection={secondaryAddToSelection}
|
|
370
|
+
secondaryDeselectAll={secondaryDeselectAll}
|
|
371
|
+
secondarySelectRangeTo={secondarySelectRangeTo}
|
|
372
|
+
secondaryIsInSelection={secondaryIsInSelection}
|
|
373
|
+
secondaryGetIdsFromSelection={secondaryGetIdsFromLocalSelection}
|
|
374
|
+
secondaryGetDisplayValuesFromSelection={secondaryGetDisplayValuesFromLocalSelection}
|
|
375
|
+
/>;
|
|
376
|
+
};
|
|
377
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EDITOR_TYPE__SIDE,
|
|
3
|
+
} from '@onehat/ui/src/Constants/Editor.js';
|
|
4
|
+
import Container from '../../Container/Container.js';
|
|
5
|
+
import withSecondaryEditor from './withSecondaryEditor.js';
|
|
6
|
+
import _ from 'lodash';
|
|
7
|
+
|
|
8
|
+
// NOTE: This is a modified version of @onehat/ui/src/Hoc/withSideEditor
|
|
9
|
+
// This HOC will eventually get out of sync with that one, and may need to be updated.
|
|
10
|
+
|
|
11
|
+
export default function withSideEditor(WrappedComponent, isTree = false) {
|
|
12
|
+
return withSecondaryEditor((props) => {
|
|
13
|
+
const {
|
|
14
|
+
SecondaryEditor,
|
|
15
|
+
secondaryEditorProps = {},
|
|
16
|
+
secondarySideFlex = 100,
|
|
17
|
+
|
|
18
|
+
// withComponent
|
|
19
|
+
self,
|
|
20
|
+
|
|
21
|
+
// pull these out, as we don't want them going to the Editor
|
|
22
|
+
secondarySelectorId,
|
|
23
|
+
secondarySelectorSelected,
|
|
24
|
+
|
|
25
|
+
...propsToPass
|
|
26
|
+
} = props;
|
|
27
|
+
|
|
28
|
+
if (!SecondaryEditor) {
|
|
29
|
+
throw Error('SecondaryEditor is not defined');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return <Container
|
|
33
|
+
center={<WrappedComponent
|
|
34
|
+
isTree={isTree}
|
|
35
|
+
isSideEditor={true}
|
|
36
|
+
{...props}
|
|
37
|
+
/>}
|
|
38
|
+
east={<Editor
|
|
39
|
+
{...propsToPass}
|
|
40
|
+
editorType={EDITOR_TYPE__SIDE}
|
|
41
|
+
flex={secondarySideFlex}
|
|
42
|
+
borderLeftWidth={1}
|
|
43
|
+
borderLeftColor="#ccc"
|
|
44
|
+
{...secondaryEditorProps}
|
|
45
|
+
parent={self}
|
|
46
|
+
reference="secondaryEditor"
|
|
47
|
+
/>}
|
|
48
|
+
/>;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useContext, useCallback, } from 'react';
|
|
2
|
+
import natsort from 'natsort';
|
|
3
|
+
import useForceUpdate from '../../../Hooks/useForceUpdate.js';
|
|
4
|
+
import FieldSetContext from '../../../Contexts/FieldSetContext.js';
|
|
5
|
+
import _ from 'lodash';
|
|
6
|
+
|
|
7
|
+
// NOTE: This is a modified version of @onehat/ui/src/Hoc/withValue
|
|
8
|
+
// This HOC will eventually get out of sync with that one, and may need to be updated.
|
|
9
|
+
|
|
10
|
+
export default function withSecondaryValue(WrappedComponent) {
|
|
11
|
+
return (props) => {
|
|
12
|
+
|
|
13
|
+
if (props.secondaryDisableWithValue) {
|
|
14
|
+
return <WrappedComponent {...props} />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (props.secondarySetValue) {
|
|
18
|
+
// bypass everything, since we're already using withSecondaryValue() in hierarchy.
|
|
19
|
+
// For example, Combo has withSecondaryValue(), and intenally it uses Input which also has withSecondaryValue(),
|
|
20
|
+
// but we only need it defined once for the whole thing.
|
|
21
|
+
return <WrappedComponent {...props} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const
|
|
25
|
+
{
|
|
26
|
+
secondaryOnChangeValue,
|
|
27
|
+
secondaryValue,
|
|
28
|
+
secondaryStartingValue = null,
|
|
29
|
+
secondaryIsValueAlwaysArray = false,
|
|
30
|
+
secondaryIsValueAsStringifiedJson = false,
|
|
31
|
+
|
|
32
|
+
// withComponent
|
|
33
|
+
self,
|
|
34
|
+
|
|
35
|
+
// withData
|
|
36
|
+
SecondaryRepository,
|
|
37
|
+
secondaryIdIx,
|
|
38
|
+
} = props,
|
|
39
|
+
forceUpdate = useForceUpdate(),
|
|
40
|
+
childRef = useRef({}),
|
|
41
|
+
secondaryOnChangeValueRef = useRef(),
|
|
42
|
+
localValueRef = useRef(secondaryStartingValue || secondaryValue),
|
|
43
|
+
fieldSetOnChangeValueRef = useRef(),
|
|
44
|
+
fieldSetContext = useContext(FieldSetContext),
|
|
45
|
+
fieldSetRegisterChild = fieldSetContext?.registerChild,
|
|
46
|
+
fieldSetOnChangeValue = fieldSetContext?.secondaryOnChangeValue,
|
|
47
|
+
getLocalValue = () => {
|
|
48
|
+
return localValueRef.current;
|
|
49
|
+
},
|
|
50
|
+
secondarySetLocalValue = (secondaryValue) => {
|
|
51
|
+
localValueRef.current = secondaryValue;
|
|
52
|
+
forceUpdate();
|
|
53
|
+
},
|
|
54
|
+
secondarySetValueRef = useRef((newValue) => {
|
|
55
|
+
// NOTE: We useRef so that this function stays current after renders
|
|
56
|
+
if (secondaryIsValueAlwaysArray && !_.isArray(newValue)) {
|
|
57
|
+
newValue = _.isNil(newValue) ? [] : [newValue];
|
|
58
|
+
}
|
|
59
|
+
if (_.isArray(newValue)) {
|
|
60
|
+
const sortFn = natsort.default || natsort; // was having trouble with webpack and this solves it
|
|
61
|
+
|
|
62
|
+
// TODO: sort by the sortProperty, whatever that is, instead of just value
|
|
63
|
+
newValue.sort(sortFn()); // Only sort if we're using id/text arrangement. Otherwise, keep sort order as specified in SecondaryRepository.
|
|
64
|
+
}
|
|
65
|
+
if (secondaryIsValueAsStringifiedJson) {
|
|
66
|
+
newValue = JSON.stringify(newValue);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (newValue === getLocalValue()) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
secondarySetLocalValue(newValue);
|
|
74
|
+
|
|
75
|
+
if (secondaryOnChangeValueRef.current) {
|
|
76
|
+
secondaryOnChangeValueRef.current(newValue, childRef.current);
|
|
77
|
+
}
|
|
78
|
+
if (fieldSetOnChangeValueRef.current) {
|
|
79
|
+
fieldSetOnChangeValueRef.current(newValue, childRef.current);
|
|
80
|
+
}
|
|
81
|
+
}),
|
|
82
|
+
secondarySetValue = (args) => {
|
|
83
|
+
secondarySetValueRef.current(args);
|
|
84
|
+
},
|
|
85
|
+
secondaryOnChangeSelection = (selection) => {
|
|
86
|
+
let secondaryValue = null,
|
|
87
|
+
secondaryValues;
|
|
88
|
+
if (selection.length) {
|
|
89
|
+
if (SecondaryRepository) {
|
|
90
|
+
if (selection.length === 1) {
|
|
91
|
+
secondaryValue = selection[0].id;
|
|
92
|
+
} else {
|
|
93
|
+
secondaryValues = _.map(selection, (entity) => entity.id);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
if (selection.length === 1) {
|
|
97
|
+
secondaryValue = selection[0][secondaryIdIx];
|
|
98
|
+
} else {
|
|
99
|
+
secondaryValues = _.map(selection, (item) => item[secondaryIdIx]);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (secondaryValues) {
|
|
104
|
+
secondaryValue = secondaryValues;
|
|
105
|
+
}
|
|
106
|
+
secondarySetValue(secondaryValue);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Ensure these passed functions stay current after render
|
|
110
|
+
secondaryOnChangeValueRef.current = secondaryOnChangeValue;
|
|
111
|
+
fieldSetOnChangeValueRef.current = fieldSetOnChangeValue;
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!_.isEqual(secondaryValue, getLocalValue())) {
|
|
115
|
+
secondarySetLocalValue(secondaryValue);
|
|
116
|
+
}
|
|
117
|
+
}, [secondaryValue]);
|
|
118
|
+
|
|
119
|
+
if (fieldSetRegisterChild) {
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
fieldSetRegisterChild({
|
|
122
|
+
childRef: childRef.current,
|
|
123
|
+
value: secondaryValue,
|
|
124
|
+
setValue: secondarySetValueRef.current,
|
|
125
|
+
});
|
|
126
|
+
}, []);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (self) {
|
|
130
|
+
self.secondarySetValue = secondarySetValue;
|
|
131
|
+
self.secondaryValue = getLocalValue();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
// Convert localValue to normal JS primitives for field components
|
|
136
|
+
let convertedValue = getLocalValue();
|
|
137
|
+
if (_.isString(convertedValue) && secondaryIsValueAsStringifiedJson && !_.isNil(convertedValue)) {
|
|
138
|
+
convertedValue = JSON.parse(convertedValue);
|
|
139
|
+
}
|
|
140
|
+
if (secondaryIsValueAlwaysArray) {
|
|
141
|
+
if (_.isEmpty(convertedValue) || _.isNil(convertedValue)) {
|
|
142
|
+
convertedValue = [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return <WrappedComponent
|
|
147
|
+
{...props}
|
|
148
|
+
secondaryDisableWithValue={false}
|
|
149
|
+
secondaryValue={convertedValue}
|
|
150
|
+
secondarySetValue={secondarySetValue}
|
|
151
|
+
secondaryOnChangeSelection={secondaryOnChangeSelection}
|
|
152
|
+
/>;
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Column,
|
|
3
|
+
Modal,
|
|
4
|
+
Text,
|
|
5
|
+
} from 'native-base';
|
|
6
|
+
import {
|
|
7
|
+
EDITOR_TYPE__WINDOWED,
|
|
8
|
+
} from '../../../Constants/Editor.js';
|
|
9
|
+
import withSecondaryEditor from './withSecondaryEditor.js';
|
|
10
|
+
// import withDraggable from './withDraggable.js';
|
|
11
|
+
import _ from 'lodash';
|
|
12
|
+
|
|
13
|
+
// NOTE: This is a modified version of @onehat/ui/src/Hoc/withWindowedEditor
|
|
14
|
+
// This HOC will eventually get out of sync with that one, and may need to be updated.
|
|
15
|
+
|
|
16
|
+
export default function withSecondaryWindowedEditor(WrappedComponent, isTree = false) {
|
|
17
|
+
return withSecondaryEditor((props) => {
|
|
18
|
+
const {
|
|
19
|
+
secondaryIsEditorShown = false,
|
|
20
|
+
secondarySetIsEditorShown,
|
|
21
|
+
SecondaryEditor,
|
|
22
|
+
secondaryEditorProps = {},
|
|
23
|
+
|
|
24
|
+
// withComponent
|
|
25
|
+
self,
|
|
26
|
+
|
|
27
|
+
// pull these out, as we don't want them going to the SecondaryEditor
|
|
28
|
+
secondarySelectorId,
|
|
29
|
+
secondarySelectorSelected,
|
|
30
|
+
h,
|
|
31
|
+
|
|
32
|
+
...propsToPass
|
|
33
|
+
} = props;
|
|
34
|
+
|
|
35
|
+
if (!SecondaryEditor) {
|
|
36
|
+
throw Error('SecondaryEditor is not defined');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (secondaryIsEditorShown) {
|
|
40
|
+
// Move the 'secondary' props over to primary
|
|
41
|
+
// for the sake of the Editor
|
|
42
|
+
function lcfirst(str) {
|
|
43
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
44
|
+
}
|
|
45
|
+
_.each(props, (prop, ix) => {
|
|
46
|
+
if (ix.match(/^secondary/)) {
|
|
47
|
+
const name = lcfirst(ix.replace(/^secondary/, ''));
|
|
48
|
+
secondaryEditorProps[name] = prop;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
secondaryEditorProps.Repository = props.SecondaryRepository;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return <>
|
|
55
|
+
<WrappedComponent {...props} />
|
|
56
|
+
{secondaryIsEditorShown &&
|
|
57
|
+
<Modal
|
|
58
|
+
isOpen={true}
|
|
59
|
+
onClose={() => secondarySetIsEditorShown(false)}
|
|
60
|
+
>
|
|
61
|
+
<SecondaryEditor
|
|
62
|
+
editorType={EDITOR_TYPE__WINDOWED}
|
|
63
|
+
{...propsToPass}
|
|
64
|
+
{...secondaryEditorProps}
|
|
65
|
+
parent={self}
|
|
66
|
+
reference="secondaryEditor"
|
|
67
|
+
/>
|
|
68
|
+
</Modal>}
|
|
69
|
+
</>;
|
|
70
|
+
}, isTree);
|
|
71
|
+
}
|
|
@@ -242,7 +242,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
242
242
|
setIsEditorShown(true);
|
|
243
243
|
|
|
244
244
|
if (getListeners().onAfterView) {
|
|
245
|
-
await getListeners().onAfterView(
|
|
245
|
+
await getListeners().onAfterView();
|
|
246
246
|
}
|
|
247
247
|
},
|
|
248
248
|
onDuplicate = async () => {
|