@onehat/ui 0.4.81 → 0.4.83

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.
Files changed (31) hide show
  1. package/package.json +7 -6
  2. package/src/Components/Container/Container.js +4 -4
  3. package/src/Components/Form/Field/Combo/Combo.js +88 -16
  4. package/src/Components/Form/Field/Combo/MeterTypesCombo.js +1 -0
  5. package/src/Components/Form/Field/Date.js +1 -1
  6. package/src/Components/Form/Field/Json.js +2 -1
  7. package/src/Components/Form/Field/Select/PageSizeSelect.js +6 -1
  8. package/src/Components/Form/Field/Select/Select.js +14 -38
  9. package/src/Components/Form/Field/Tag/Tag.js +234 -14
  10. package/src/Components/Form/Field/Tag/ValueBox.js +20 -1
  11. package/src/Components/Form/Form.js +26 -13
  12. package/src/Components/Grid/Grid.js +316 -106
  13. package/src/Components/Grid/GridHeaderRow.js +42 -22
  14. package/src/Components/Grid/GridRow.js +16 -6
  15. package/src/Components/Grid/RowHandle.js +16 -4
  16. package/src/Components/Hoc/Secondary/withSecondaryEditor.js +137 -43
  17. package/src/Components/Hoc/Secondary/withSecondarySideEditor.js +1 -1
  18. package/src/Components/Hoc/withData.js +7 -0
  19. package/src/Components/Hoc/withEditor.js +19 -4
  20. package/src/Components/Hoc/withPresetButtons.js +1 -1
  21. package/src/Components/Hoc/withSideEditor.js +1 -1
  22. package/src/Components/Icons/Join.js +10 -0
  23. package/src/Components/Layout/AsyncOperation.js +61 -14
  24. package/src/Components/Layout/CenterBox.js +1 -1
  25. package/src/Components/Screens/Manager.js +1 -1
  26. package/src/Components/Toolbar/Pagination.js +108 -106
  27. package/src/Components/Toolbar/PaginationToolbar.js +3 -1
  28. package/src/Components/Toolbar/Toolbar.js +10 -6
  29. package/src/Components/Tree/TreeNode.js +39 -9
  30. package/src/Components/Viewer/Viewer.js +7 -2
  31. package/src/Constants/Progress.js +2 -1
@@ -105,12 +105,19 @@ export default forwardRef(function GridHeaderRow(props, ref) {
105
105
  columnHeaders = _.filter(columnHeader.parentElement.children, (childNode) => {
106
106
  return childNode.getBoundingClientRect().width !== 0; // Skip zero-width children
107
107
  }),
108
- currentX = proxyRect.left; // left position of pointer
108
+ currentX = proxyRect.left, // left position of pointer
109
+ rowHandleOffset = showRowHandle ? 1 : 0; // Account for RowHandleSpacer
109
110
 
110
111
  // Figure out which index the user wants
111
112
  let newIx = 0;
112
113
  _.each(columnHeaders, (child, ix, all) => {
114
+ // Skip the row handle spacer if present
115
+ if (showRowHandle && ix === 0) {
116
+ return true; // continue to next iteration
117
+ }
118
+
113
119
  const
120
+ adjustedIx = ix - rowHandleOffset, // Adjust index for row handle offset
114
121
  rect = child.getBoundingClientRect(), // rect of the columnHeader of this iteration
115
122
  {
116
123
  left,
@@ -119,8 +126,8 @@ export default forwardRef(function GridHeaderRow(props, ref) {
119
126
  } = rect,
120
127
  halfWidth = width /2;
121
128
 
122
- if (ix === 0) {
123
- // first column
129
+ if (adjustedIx === 0) {
130
+ // first actual data column
124
131
  if (currentX < left + halfWidth) {
125
132
  newIx = 0;
126
133
  return false;
@@ -128,22 +135,22 @@ export default forwardRef(function GridHeaderRow(props, ref) {
128
135
  newIx = 1;
129
136
  return false;
130
137
  }
131
- } else if (ix === all.length -1) {
132
- // last column
138
+ } else if (adjustedIx === all.length - 1 - rowHandleOffset) {
139
+ // last actual data column (account for nav column and row handle)
133
140
  if (currentX < left + halfWidth) {
134
- newIx = ix;
141
+ newIx = adjustedIx;
135
142
  return false;
136
143
  }
137
- newIx = ix +1;
144
+ newIx = adjustedIx + 1;
138
145
  return false;
139
146
  }
140
147
 
141
148
  // all other columns
142
149
  if (left <= currentX && currentX < left + halfWidth) {
143
- newIx = ix;
150
+ newIx = adjustedIx;
144
151
  return false;
145
152
  } else if (currentX < right) {
146
- newIx = ix +1;
153
+ newIx = adjustedIx + 1;
147
154
  return false;
148
155
  }
149
156
  });
@@ -155,7 +162,8 @@ export default forwardRef(function GridHeaderRow(props, ref) {
155
162
 
156
163
  // Render marker showing destination location (can't use regular render cycle because this div is absolutely positioned on page)
157
164
  const
158
- columnHeaderRect = columnHeaders[newIx].getBoundingClientRect(),
165
+ targetColumnIndex = newIx + rowHandleOffset, // Add back offset for visual positioning
166
+ columnHeaderRect = columnHeaders[targetColumnIndex].getBoundingClientRect(),
159
167
  left = columnHeaderRect.left,
160
168
  gridRowsContainer = gridRef.current._listRef._scrollRef.childNodes[0],
161
169
  gridRowsContainerRect = gridRowsContainer.getBoundingClientRect(),
@@ -180,12 +188,19 @@ export default forwardRef(function GridHeaderRow(props, ref) {
180
188
  columnHeaders = _.filter(columnHeader.parentElement.children, (childNode) => {
181
189
  return childNode.getBoundingClientRect().width !== 0; // Skip zero-width children
182
190
  }),
183
- currentX = proxyRect.left; // left position of pointer
191
+ currentX = proxyRect.left, // left position of pointer
192
+ rowHandleOffset = showRowHandle ? 1 : 0; // Account for RowHandleSpacer
184
193
 
185
194
  // Figure out which index the user wants
186
195
  let newIx = 0;
187
196
  _.each(columnHeaders, (child, ix, all) => {
197
+ // Skip the row handle spacer if present
198
+ if (showRowHandle && ix === 0) {
199
+ return true; // continue to next iteration
200
+ }
201
+
188
202
  const
203
+ adjustedIx = ix - rowHandleOffset, // Adjust index for row handle offset
189
204
  rect = child.getBoundingClientRect(), // rect of the columnHeader of this iteration
190
205
  {
191
206
  left,
@@ -194,8 +209,8 @@ export default forwardRef(function GridHeaderRow(props, ref) {
194
209
  } = rect,
195
210
  halfWidth = width /2;
196
211
 
197
- if (ix === 0) {
198
- // first column
212
+ if (adjustedIx === 0) {
213
+ // first actual data column
199
214
  if (currentX < left + halfWidth) {
200
215
  newIx = 0;
201
216
  return false;
@@ -203,22 +218,22 @@ export default forwardRef(function GridHeaderRow(props, ref) {
203
218
  newIx = 1;
204
219
  return false;
205
220
  }
206
- } else if (ix === all.length -1) {
207
- // last column
221
+ } else if (adjustedIx === all.length - 1 - rowHandleOffset) {
222
+ // last actual data column (account for nav column and row handle)
208
223
  if (currentX < left + halfWidth) {
209
- newIx = ix;
224
+ newIx = adjustedIx;
210
225
  return false;
211
226
  }
212
- newIx = ix +1;
227
+ newIx = adjustedIx + 1;
213
228
  return false;
214
229
  }
215
230
 
216
231
  // all other columns
217
232
  if (left <= currentX && currentX < left + halfWidth) {
218
- newIx = ix;
233
+ newIx = adjustedIx;
219
234
  return false;
220
235
  } else if (currentX < right) {
221
- newIx = ix +1;
236
+ newIx = adjustedIx + 1;
222
237
  return false;
223
238
  }
224
239
  });
@@ -230,7 +245,8 @@ export default forwardRef(function GridHeaderRow(props, ref) {
230
245
 
231
246
  // Render marker showing destination location (can't use regular render cycle because this div is absolutely positioned on page)
232
247
  const
233
- columnHeaderRect = columnHeaders[newIx].getBoundingClientRect(),
248
+ targetColumnIndex = newIx + rowHandleOffset, // Add back offset for visual positioning
249
+ columnHeaderRect = columnHeaders[targetColumnIndex].getBoundingClientRect(),
234
250
  left = columnHeaderRect.left;
235
251
  let marker = dragColumnSlot && dragColumnSlot.marker;
236
252
  if (marker) {
@@ -468,7 +484,12 @@ export default forwardRef(function GridHeaderRow(props, ref) {
468
484
  if (showRowHandle) {
469
485
  headerColumns.unshift(<Box
470
486
  key="RowHandleSpacer"
471
- className="Spacer-RowHandle w-[40px] flex-none"
487
+ className={clsx(
488
+ 'Spacer-RowHandle',
489
+ 'w-[40px]',
490
+ 'flex-none',
491
+ styles.ROW_HANDLE_CLASSNAME,
492
+ )}
472
493
  />);
473
494
  }
474
495
  if (!hideNavColumn) {
@@ -510,4 +531,3 @@ export default forwardRef(function GridHeaderRow(props, ref) {
510
531
  showRowHandle,
511
532
  ]);
512
533
  });
513
-
@@ -29,7 +29,7 @@ let getEmptyImage = null;
29
29
 
30
30
  // This was broken out from Grid simply so we can memoize it
31
31
 
32
- const GridRow = forwardRef(function GridRow(props, ref) {
32
+ const GridRow = forwardRef((props, ref) => {
33
33
  const {
34
34
  columnsConfig,
35
35
  columnProps,
@@ -37,6 +37,7 @@ const GridRow = forwardRef(function GridRow(props, ref) {
37
37
  rowProps,
38
38
  hideNavColumn,
39
39
  showRowHandle,
40
+ areCellsScrollable,
40
41
  rowCanSelect,
41
42
  rowCanDrag,
42
43
  isRowHoverable,
@@ -141,7 +142,7 @@ const GridRow = forwardRef(function GridRow(props, ref) {
141
142
  'justify-center',
142
143
  'border-r-black-100',
143
144
  'block',
144
- 'overflow-auto',
145
+ areCellsScrollable ? 'overflow-auto' : 'overflow-hidden',
145
146
  whichCursor,
146
147
  styles.GRID_ROW_MAX_HEIGHT_EXTRA,
147
148
  );
@@ -207,7 +208,7 @@ const GridRow = forwardRef(function GridRow(props, ref) {
207
208
  let textClassName = clsx(
208
209
  'GridRow-TextNative',
209
210
  'self-center',
210
- 'overflow-hidden',
211
+ areCellsScrollable ? 'overflow-auto' : 'overflow-hidden',
211
212
  colClassName,
212
213
  styles.GRID_CELL_CLASSNAME,
213
214
  styles.GRID_ROW_MAX_HEIGHT_EXTRA,
@@ -216,7 +217,7 @@ const GridRow = forwardRef(function GridRow(props, ref) {
216
217
  textClassName += ' ' + config.className;
217
218
  }
218
219
  const rendererProps = {
219
- ...testProps('rendererCol-' + config.fieldName),
220
+ ...testProps('rendererCol-' + (config.fieldName || config.id || key)),
220
221
  className: textClassName,
221
222
  ...propsToPass,
222
223
  ...extraProps,
@@ -283,7 +284,10 @@ const GridRow = forwardRef(function GridRow(props, ref) {
283
284
  'px-2',
284
285
  'py-3',
285
286
  'block',
286
- 'overflow-scroll',
287
+ areCellsScrollable ? 'overflow-auto' : 'overflow-hidden',
288
+ '[&::-webkit-scrollbar]:h-2',
289
+ '[&::-webkit-scrollbar-thumb]:bg-gray-400',
290
+ '[&::-webkit-scrollbar-thumb]:rounded-full',
287
291
  colClassName,
288
292
  styles.GRID_CELL_CLASSNAME,
289
293
  styles.GRID_ROW_MAX_HEIGHT_NORMAL,
@@ -291,6 +295,9 @@ const GridRow = forwardRef(function GridRow(props, ref) {
291
295
  if (config.className) {
292
296
  elementClassName += ' ' + config.className;
293
297
  }
298
+ if (rowProps?._cell?.className) {
299
+ elementClassName += ' ' + rowProps._cell.className;
300
+ }
294
301
  if (cellProps.className) {
295
302
  elementClassName += ' ' + cellProps.className;
296
303
  }
@@ -346,11 +353,14 @@ const GridRow = forwardRef(function GridRow(props, ref) {
346
353
  let textClassName = clsx(
347
354
  'GridRow-TextNative',
348
355
  'self-center',
349
- 'overflow-hidden',
356
+ areCellsScrollable ? 'overflow-auto' : 'overflow-hidden',
350
357
  colClassName,
351
358
  styles.GRID_CELL_CLASSNAME,
352
359
  styles.GRID_ROW_MAX_HEIGHT_EXTRA,
353
360
  );
361
+ if (rowProps?._cell?.className) {
362
+ textClassName += ' ' + rowProps._cell.className;
363
+ }
354
364
  if (config.className) {
355
365
  textClassName += ' ' + config.className;
356
366
  }
@@ -3,15 +3,17 @@ import {
3
3
  Icon,
4
4
  VStack,
5
5
  } from '@project-components/Gluestack';
6
- import withTooltip from '@onehat/ui/src/Components/Hoc/withTooltip';
6
+ import withTooltip from '../Hoc/withTooltip';
7
7
  import clsx from 'clsx';
8
+ import UiGlobals from '../../UiGlobals.js';
8
9
  import Arcs from '../Icons/Arcs.js';
9
10
 
10
11
  const RowHandle = forwardRef((props, ref) => {
11
12
  const {
12
13
  isDragSource,
13
14
  isDraggable
14
- } = props;
15
+ } = props,
16
+ styles = UiGlobals.styles;
15
17
  let className = clsx(
16
18
  'RowHandle',
17
19
  'h-full',
@@ -20,13 +22,23 @@ const RowHandle = forwardRef((props, ref) => {
20
22
  'items-center',
21
23
  'justify-center',
22
24
  'select-none',
23
- 'cursor-pointer'
25
+ 'cursor-pointer',
26
+ styles.ROW_HANDLE_CLASSNAME,
24
27
  );
25
28
  return <VStack
26
29
  ref={isDragSource || isDraggable ? ref : undefined}
27
30
  className={className}
28
31
  >
29
- <Icon as={Arcs} size="xs" className="w-full h-full text-[#ddd]" />
32
+ <Icon
33
+ as={Arcs}
34
+ size="xs"
35
+ className={clsx(
36
+ 'w-full',
37
+ 'h-full',
38
+ 'text-[#ddd]',
39
+ styles.ROW_HANDLE_ICON_CLASSNAME,
40
+ )}
41
+ />
30
42
  </VStack>;
31
43
  });
32
44
 
@@ -32,6 +32,7 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
32
32
  secondaryUserCanEdit = true, // not permissions, but capability
33
33
  secondaryUserCanView = true,
34
34
  secondaryCanEditorViewOnly = false, // whether the editor can *ever* change state out of 'View' mode
35
+ secondaryCanProceedWithCrud, // fn returns bool on if the CRUD operation can proceed
35
36
  secondaryDisableAdd = false,
36
37
  secondaryDisableEdit = false,
37
38
  secondaryDisableDelete = false,
@@ -47,12 +48,14 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
47
48
  secondaryEditorType,
48
49
  secondaryOnAdd,
49
50
  secondaryOnChange, // any kind of crud change
51
+ secondaryOnBeforeDelete,
50
52
  secondaryOnDelete,
51
53
  secondaryOnSave, // this could also be called 'onEdit'
52
54
  secondaryOnEditorClose,
53
55
  secondaryNewEntityDisplayValue,
54
56
  secondaryNewEntityDisplayProperty, // in case the field to set for newEntityDisplayValue is different from model
55
57
  secondaryDefaultValues,
58
+ secondaryInitialEditorMode = EDITOR_MODE__VIEW,
56
59
  secondaryStayInEditModeOnSelectionChange = false,
57
60
 
58
61
  // withComponent
@@ -86,11 +89,11 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
86
89
  secondaryNewEntityDisplayValueRef = useRef(),
87
90
  secondaryEditorModeRef = useRef(EDITOR_MODE__VIEW),
88
91
  secondaryIsIgnoreNextSelectionChangeRef = useRef(false),
92
+ secondaryIsEditorShownRef = useRef(false),
89
93
  secondaryModel = SecondaryRepository?.schema?.name,
90
94
  [secondaryCurrentRecord, secondarySetCurrentRecord] = useState(null),
91
95
  [secondaryIsAdding, setIsAdding] = useState(false),
92
96
  [secondaryIsSaving, setIsSaving] = useState(false),
93
- [secondaryIsEditorShown, secondarySetIsEditorShownRaw] = useState(false),
94
97
  [secondaryIsEditorViewOnly, setIsEditorViewOnly] = useState(secondaryCanEditorViewOnly), // current state of whether editor is in view-only mode
95
98
  [secondaryLastSelection, setLastSelection] = useState(),
96
99
  secondarySetIsIgnoreNextSelectionChange = (bool) => {
@@ -100,11 +103,24 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
100
103
  return secondaryIsIgnoreNextSelectionChangeRef.current;
101
104
  },
102
105
  secondarySetIsEditorShown = (bool) => {
103
- secondarySetIsEditorShownRaw(bool);
106
+ secondaryIsEditorShownRef.current = bool;
107
+ forceUpdate();
104
108
  if (!bool && secondaryOnEditorClose) {
105
109
  secondaryOnEditorClose();
106
110
  }
107
111
  },
112
+ secondaryGetIsEditorShown = () => {
113
+ return secondaryIsEditorShownRef.current;
114
+ },
115
+ secondarySetIsWaitModalShown = (bool) => {
116
+ const
117
+ dispatch = UiGlobals.redux?.dispatch,
118
+ setIsWaitModalShownAction = UiGlobals.systemReducer?.setIsWaitModalShownAction;
119
+ if (setIsWaitModalShownAction) {
120
+ console.log('withSecondaryEditor:setIsWaitModalShownAction', bool);
121
+ dispatch(setIsWaitModalShownAction(bool));
122
+ }
123
+ },
108
124
  secondarySetSelectionDecorated = (newSelection) => {
109
125
  function doIt() {
110
126
  secondarySetSelection(newSelection);
@@ -139,7 +155,7 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
139
155
  forceUpdate();
140
156
  }
141
157
  },
142
- getNewEntityDisplayValue = () => {
158
+ secondaryGetNewEntityDisplayValue = () => {
143
159
  return secondaryNewEntityDisplayValueRef.current;
144
160
  },
145
161
  secondaryDoAdd = async (e, values) => {
@@ -147,6 +163,9 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
147
163
  showPermissionsError(ADD, secondaryModel);
148
164
  return;
149
165
  }
166
+ if (secondaryCanProceedWithCrud && !secondaryCanProceedWithCrud()) {
167
+ return;
168
+ }
150
169
 
151
170
  const secondarySelection = secondaryGetSelection();
152
171
  let addValues = values;
@@ -165,20 +184,20 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
165
184
  // 1. directlty submit 'values' to use in secondaryDoAdd(), or
166
185
  // 2. Use the repository's default values (defined on each property as 'defaultValue'), or
167
186
  // 3. Individually override the repository's default values with submitted 'defaultValues' (given as a prop to this HOC)
168
- let defaultValuesToUse = SecondaryRepository.getSchema().getDefaultValues();
187
+ let secondaryDefaultValuesToUse = SecondaryRepository.getSchema().getDefaultValues();
169
188
  if (secondaryDefaultValues) {
170
- _.merge(defaultValuesToUse, secondaryDefaultValues);
189
+ _.merge(secondaryDefaultValuesToUse, secondaryDefaultValues);
171
190
  }
172
- addValues = {...defaultValuesToUse};
191
+ addValues = {...secondaryDefaultValuesToUse};
173
192
  }
174
193
 
175
194
  if (secondarySelectorId && !_.isEmpty(secondarySelectorSelected)) {
176
195
  addValues[secondarySelectorId] = secondarySelectorSelected[secondarySelectorSelectedField];
177
196
  }
178
197
 
179
- if (getNewEntityDisplayValue()) {
198
+ if (secondaryGetNewEntityDisplayValue()) {
180
199
  const displayPropertyName = secondaryNewEntityDisplayProperty || SecondaryRepository.getSchema().model.displayProperty;
181
- addValues[displayPropertyName] = getNewEntityDisplayValue();
200
+ addValues[displayPropertyName] = secondaryGetNewEntityDisplayValue();
182
201
  }
183
202
 
184
203
  if (getListeners().onBeforeAdd) {
@@ -246,6 +265,9 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
246
265
  showPermissionsError(EDIT, secondaryModel);
247
266
  return;
248
267
  }
268
+ if (secondaryCanProceedWithCrud && !secondaryCanProceedWithCrud()) {
269
+ return;
270
+ }
249
271
  const secondarySelection = secondaryGetSelection();
250
272
  if (_.isEmpty(secondarySelection) || (_.isArray(secondarySelection) && (secondarySelection.length > 1 || secondarySelection[0]?.isDestroyed))) {
251
273
  return;
@@ -265,6 +287,9 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
265
287
  showPermissionsError(DELETE, secondaryModel);
266
288
  return;
267
289
  }
290
+ if (secondaryCanProceedWithCrud && !secondaryCanProceedWithCrud()) {
291
+ return;
292
+ }
268
293
  let cb = null;
269
294
  if (_.isFunction(args)) {
270
295
  cb = args;
@@ -273,7 +298,15 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
273
298
  if (_.isEmpty(secondarySelection) || (_.isArray(secondarySelection) && (secondarySelection.length > 1 || secondarySelection[0]?.isDestroyed))) {
274
299
  return;
275
300
  }
301
+ if (secondaryOnBeforeDelete) {
302
+ // This listener is set by parent components using a prop
303
+ const listenerResult = await secondaryOnBeforeDelete(secondarySelection);
304
+ if (listenerResult === false) {
305
+ return;
306
+ }
307
+ }
276
308
  if (getListeners().onBeforeDelete) {
309
+ // This listener is set by child components using setWithEditListeners()
277
310
  const listenerResult = await getListeners().onBeforeDelete();
278
311
  if (listenerResult === false) {
279
312
  return;
@@ -309,26 +342,33 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
309
342
  });
310
343
  } else
311
344
  if (isSingle && isPhantom) {
312
- deleteRecord(cb);
345
+ secondaryDeleteRecord(cb);
313
346
  } else {
314
347
  const identifier = secondaryGetRecordIdentifier(secondarySelection);
315
- confirm('Are you sure you want to delete the ' + identifier, () => deleteRecord(null, cb));
348
+ confirm('Are you sure you want to delete the ' + identifier, () => secondaryDeleteRecord(null, cb));
316
349
  }
317
350
  },
318
351
  secondaryDoMoveChildren = (cb) => {
319
352
  hideAlert();
320
- deleteRecord(true, cb);
353
+ secondaryDeleteRecord(true, cb);
321
354
  },
322
355
  secondaryDoDeleteChildren = (cb) => {
323
356
  hideAlert();
324
- deleteRecord(false, cb);
357
+ secondaryDeleteRecord(false, cb);
325
358
  },
326
- deleteRecord = async (moveSubtreeUp, cb) => {
359
+ secondaryDeleteRecord = async (moveSubtreeUp, cb) => {
327
360
  if (canUser && !canUser(DELETE, secondaryModel)) {
328
361
  showPermissionsError(DELETE, secondaryModel);
329
362
  return;
330
363
  }
331
364
  const secondarySelection = secondaryGetSelection();
365
+ if (secondaryOnBeforeDelete) {
366
+ // This listener is set by parent components using a prop
367
+ const listenerResult = await secondaryOnBeforeDelete(secondarySelection);
368
+ if (listenerResult === false) {
369
+ return;
370
+ }
371
+ }
332
372
  if (getListeners().onBeforeDelete) {
333
373
  const listenerResult = await getListeners().onBeforeDelete(secondarySelection);
334
374
  if (listenerResult === false) {
@@ -362,6 +402,9 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
362
402
  showPermissionsError(VIEW, secondaryModel);
363
403
  return;
364
404
  }
405
+ if (secondaryCanProceedWithCrud && !secondaryCanProceedWithCrud()) {
406
+ return;
407
+ }
365
408
  if (secondaryEditorType === EDITOR_TYPE__INLINE) {
366
409
  alert('Cannot view in inline editor.');
367
410
  return; // inline editor doesn't have a view mode
@@ -389,37 +432,70 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
389
432
  showPermissionsError(DUPLICATE, secondaryModel);
390
433
  return;
391
434
  }
392
-
393
- // check permissions for duplicate
435
+ if (secondaryCanProceedWithCrud && !secondaryCanProceedWithCrud()) {
436
+ return;
437
+ }
394
438
 
395
439
  const secondarySelection = secondaryGetSelection();
396
440
  if (secondarySelection.length !== 1) {
397
441
  return;
398
442
  }
443
+
399
444
  if (secondaryUseRemoteDuplicate) {
400
- const results = await onRemoteDuplicate();
401
- return results;
445
+ return await secondaryOnRemoteDuplicate();
446
+ }
447
+
448
+ let isSuccess = false,
449
+ duplicateEntity;
450
+ try {
451
+ const
452
+ entity = secondarySelection[0],
453
+ idProperty = SecondaryRepository.getSchema().model.idProperty,
454
+ rawValues = _.omit(entity.getOriginalData(), idProperty);
455
+ rawValues.id = null; // unset the id of the duplicate
456
+
457
+ setIsWaitModalShown(true);
458
+
459
+ duplicateEntity = await SecondaryRepository.add(rawValues, false, true);
460
+ isSuccess = true;
461
+
462
+ } catch(err) {
463
+ // do nothing
464
+ } finally {
465
+ setIsWaitModalShown(false);
466
+ }
467
+
468
+ if (isSuccess) {
469
+ secondarySetIsIgnoreNextSelectionChange(true);
470
+ secondarySetSelection([duplicateEntity]);
471
+ secondarySetEditorMode(EDITOR_MODE__EDIT);
472
+ secondarySetIsEditorShown(true);
402
473
  }
403
- const
404
- entity = secondarySelection[0],
405
- idProperty = SecondaryRepository.getSchema().model.idProperty,
406
- rawValues = _.omit(entity.getOriginalData(), idProperty);
407
- rawValues.id = null; // unset the id of the duplicate
408
- const duplicate = await SecondaryRepository.add(rawValues, false, true);
409
- secondarySetIsIgnoreNextSelectionChange(true);
410
- secondarySetSelection([duplicate]);
411
- secondarySetEditorMode(EDITOR_MODE__EDIT);
412
- secondarySetIsEditorShown(true);
413
474
  },
414
- onRemoteDuplicate = async () => {
415
- const
416
- secondarySelection = secondaryGetSelection(),
417
- entity = secondarySelection[0],
418
- duplicateEntity = await SecondaryRepository.remoteDuplicate(entity);
475
+ secondaryOnRemoteDuplicate = async () => {
476
+ let isSuccess = false,
477
+ duplicateEntity;
478
+ try {
479
+ const
480
+ secondarySelection = secondaryGetSelection(),
481
+ entity = secondarySelection[0];
482
+
483
+ setIsWaitModalShown(true);
419
484
 
420
- secondarySetIsIgnoreNextSelectionChange(true);
421
- secondarySetSelection([duplicateEntity]);
422
- secondaryDoEdit();
485
+ duplicateEntity = await SecondaryRepository.remoteDuplicate(entity);
486
+ isSuccess = true;
487
+
488
+ } catch(err) {
489
+ // do nothing
490
+ } finally {
491
+ setIsWaitModalShown(false);
492
+ }
493
+ if (isSuccess) {
494
+ secondarySetIsIgnoreNextSelectionChange(true);
495
+ secondarySetSelection([duplicateEntity]);
496
+ secondaryDoEdit();
497
+ return duplicateEntity;
498
+ }
423
499
  },
424
500
  secondaryDoEditorSave = async (data, e) => {
425
501
  let mode = secondaryGetEditorMode() === EDITOR_MODE__ADD ? ADD : EDIT;
@@ -516,7 +592,7 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
516
592
  isSingle = secondarySelection.length === 1,
517
593
  isPhantom = secondarySelection[0] && !secondarySelection[0]?.isDestroyed && secondarySelection[0].isPhantom;
518
594
  if (isSingle && isPhantom) {
519
- await deleteRecord();
595
+ await secondaryDeleteRecord();
520
596
  }
521
597
 
522
598
  setIsAdding(false);
@@ -551,7 +627,7 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
551
627
  secondarySetIsEditorShown(false);
552
628
  });
553
629
  },
554
- calculateEditorMode = () => {
630
+ secondaryCalculateEditorMode = () => {
555
631
 
556
632
  let secondaryIsIgnoreNextSelectionChange = secondaryGetIsIgnoreNextSelectionChange(),
557
633
  doStayInEditModeOnSelectionChange = secondaryStayInEditModeOnSelectionChange;
@@ -563,7 +639,7 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
563
639
  secondaryIsIgnoreNextSelectionChange = true;
564
640
  }
565
641
 
566
- // calculateEditorMode gets called only on selection changes
642
+ // secondaryCalculateEditorMode gets called only on selection changes
567
643
  const secondarySelection = secondaryGetSelection();
568
644
  let mode;
569
645
  if (secondaryEditorType === EDITOR_TYPE__SIDE && !_.isNil(UiGlobals.isSideEditorAlwaysEditMode) && UiGlobals.isSideEditorAlwaysEditMode) {
@@ -617,10 +693,26 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
617
693
  };
618
694
 
619
695
  useEffect(() => {
620
- secondarySetEditorMode(calculateEditorMode());
621
696
 
622
- secondarySetIsIgnoreNextSelectionChange(false);
697
+ if (secondaryEditorType === EDITOR_TYPE__SIDE) {
698
+ if (secondarySelection?.length) { // || isAdding
699
+ // there is a selection, so show the editor
700
+ secondarySetIsEditorShown(true);
701
+ } else {
702
+ // no selection, so close the editor
703
+ secondarySetIsEditorShown(false);
704
+ }
705
+ }
706
+
707
+ secondarySetEditorMode(secondaryCalculateEditorMode());
623
708
  setLastSelection(secondarySelection);
709
+
710
+ // Push isIgnoreNextSelectionChange until after a microtask to ensure all
711
+ // synchronous operations (including listener callbacks) are complete
712
+ // (this is to prevent the editor from immediately switching modes on doAdd in Tree)
713
+ Promise.resolve().then(() => {
714
+ secondarySetIsIgnoreNextSelectionChange(false);
715
+ });
624
716
  }, [secondarySelection]);
625
717
 
626
718
  if (self) {
@@ -638,21 +730,23 @@ export default function withSecondaryEditor(WrappedComponent, isTree = false) {
638
730
  // NOTE: If I don't calculate this on the fly for selection changes,
639
731
  // we see a flash of the previous state, since useEffect hasn't yet run.
640
732
  // (basically redo what's in the useEffect, above)
641
- secondarySetEditorMode(calculateEditorMode());
733
+ secondarySetEditorMode(secondaryCalculateEditorMode());
642
734
  }
643
735
 
644
736
  return <WrappedComponent
645
737
  {...props}
738
+ ref={ref}
646
739
  secondaryDisableWithEditor={false}
647
740
  secondaryAlreadyHasWithEditor={true}
648
- ref={ref}
649
741
  secondaryCurrentRecord={secondaryCurrentRecord}
650
742
  secondarySetCurrentRecord={secondarySetCurrentRecord}
651
- secondaryIsEditorShown={secondaryIsEditorShown}
743
+ secondaryIsEditorShown={secondaryGetIsEditorShown()}
744
+ secondaryGetIsEditorShown={secondaryGetIsEditorShown}
652
745
  secondaryIsEditorViewOnly={secondaryIsEditorViewOnly}
653
746
  secondaryIsAdding={secondaryIsAdding}
654
747
  secondaryIsSaving={secondaryIsSaving}
655
748
  secondaryEditorMode={secondaryGetEditorMode()}
749
+ secondaryGetEditorMode={secondaryGetEditorMode}
656
750
  secondaryOnEditMode={secondarySetEditMode}
657
751
  secondaryOnViewMode={secondarySetViewMode}
658
752
  secondaryEditorStateRef={secondaryEditorStateRef}
@@ -68,7 +68,7 @@ export default function withSecondarySideEditor(WrappedComponent, isTree = false
68
68
  isSideEditor={true}
69
69
  {...props}
70
70
  />}
71
- east={<Editor
71
+ east={props.secondaryIsEditorShown && <Editor
72
72
  {...propsToPass}
73
73
  editorType={EDITOR_TYPE__SIDE}
74
74
  {...secondaryEditorProps}
@@ -130,6 +130,13 @@ export default function withData(WrappedComponent) {
130
130
  displayField={displayField}
131
131
  idIx={localIdIx}
132
132
  displayIx={localDisplayIx}
133
+ setBaseParams={(baseParams) => {
134
+ // This allows components down the hierarchy to dynamically set the baseParams
135
+ LocalRepository.setBaseParams(baseParams);
136
+ if (LocalRepository.isRemote) {
137
+ LocalRepository.load();
138
+ }
139
+ }}
133
140
  />;
134
141
  });
135
142
  }