@onehat/ui 0.4.96 → 0.4.98

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onehat/ui",
3
- "version": "0.4.96",
3
+ "version": "0.4.98",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -1,4 +1,4 @@
1
- import { cloneElement, useState, useEffect, useRef, } from 'react';
1
+ import { cloneElement, useState, useEffect, useRef, useCallback, } from 'react';
2
2
  import {
3
3
  BoxNative,
4
4
  HStack,
@@ -16,6 +16,7 @@ import {
16
16
  } from '../../Constants/UiModes.js';
17
17
  import withComponent from '../Hoc/withComponent.js';
18
18
  import useForceUpdate from '../../Hooks/useForceUpdate.js';
19
+ import getComponentFromType from '../../Functions/getComponentFromType.js';
19
20
  import getSaved from '../../Functions/getSaved.js';
20
21
  import setSaved from '../../Functions/setSaved.js';
21
22
  import Splitter from './Splitter.js';
@@ -106,6 +107,8 @@ function Container(props) {
106
107
  } = props,
107
108
  id = props.id || props.self?.path,
108
109
  isWeb = CURRENT_MODE === UI_MODE_WEB,
110
+ useWindowSize = getComponentFromType('useWindowSize'),
111
+ windowSize = useWindowSize(),
109
112
  forceUpdate = useForceUpdate(),
110
113
  centerRef = useRef(null),
111
114
  northRef = useRef(null),
@@ -118,77 +121,111 @@ function Container(props) {
118
121
  westWidthRef = useRef(westInitialWidth),
119
122
  [isReady, setIsReady] = useState(false),
120
123
  [isComponentsDisabled, setIsComponentsDisabled] = useState(false),
121
- [localNorthIsCollapsed, setLocalNorthIsCollapsed] = useState(northInitialIsCollapsed),
122
- [localSouthIsCollapsed, setLocalSouthIsCollapsed] = useState(southInitialIsCollapsed),
123
- [localEastIsCollapsed, setLocalEastIsCollapsed] = useState(eastInitialIsCollapsed),
124
- [localWestIsCollapsed, setLocalWestIsCollapsed] = useState(westInitialIsCollapsed),
124
+ localNorthIsCollapsedRef = useRef(northInitialIsCollapsed),
125
+ localSouthIsCollapsedRef = useRef(southInitialIsCollapsed),
126
+ localEastIsCollapsedRef = useRef(eastInitialIsCollapsed),
127
+ localWestIsCollapsedRef = useRef(westInitialIsCollapsed),
128
+ onLayout = async (e) => {
129
+ console.log('Container onLayout', e.nativeEvent.layout.width);
130
+ if (id) {
131
+ // save prevScreenSize if changed
132
+ const
133
+ height = parseFloat(e.nativeEvent.layout.height),
134
+ width = parseFloat(e.nativeEvent.layout.width),
135
+ key = id + '-prevScreenSize',
136
+ prevScreenSize = await getSaved(key);
137
+ if (!prevScreenSize || prevScreenSize.width !== width || prevScreenSize.height !== height) {
138
+ await setSaved(key, {
139
+ height,
140
+ width,
141
+ });
142
+
143
+ // reset all sizes to null, so they recalculate
144
+ setNorthHeight(null);
145
+ setSouthHeight(null);
146
+ setEastWidth(null);
147
+ setWestWidth(null);
148
+ forceUpdate();
149
+ }
150
+ }
151
+ },
152
+ debouncedOnLayout = useCallback(
153
+ _.debounce((e) => {
154
+ onLayout(e);
155
+ }, 2000), // delay is signficant, as all we're trying to do is catch screen size changes
156
+ []
157
+ ),
125
158
  setNorthIsCollapsed = (bool) => {
126
159
  if (setExternalNorthIsCollapsed) {
127
160
  setExternalNorthIsCollapsed(bool);
128
161
  } else {
129
- setLocalNorthIsCollapsed(bool);
162
+ localNorthIsCollapsedRef.current = bool;
130
163
  }
131
164
 
132
165
  if (id) {
133
166
  setSaved(id + '-northIsCollapsed', bool);
134
167
  }
168
+ forceUpdate();
135
169
  },
136
170
  getNorthIsCollapsed = () => {
137
171
  if (setExternalNorthIsCollapsed) {
138
172
  return northIsCollapsed;
139
173
  }
140
- return localNorthIsCollapsed;
174
+ return localNorthIsCollapsedRef.current;
141
175
  },
142
176
  setSouthIsCollapsed = (bool) => {
143
177
  if (setExternalSouthIsCollapsed) {
144
178
  setExternalSouthIsCollapsed(bool);
145
179
  } else {
146
- setLocalSouthIsCollapsed(bool);
180
+ localSouthIsCollapsedRef.current = bool;
147
181
  }
148
182
 
149
183
  if (id) {
150
184
  setSaved(id + '-southIsCollapsed', bool);
151
185
  }
186
+ forceUpdate();
152
187
  },
153
188
  getSouthIsCollapsed = () => {
154
189
  if (setExternalSouthIsCollapsed) {
155
190
  return southIsCollapsed;
156
191
  }
157
- return localSouthIsCollapsed;
192
+ return localSouthIsCollapsedRef.current;
158
193
  },
159
194
  setEastIsCollapsed = (bool) => {
160
195
  if (setExternalEastIsCollapsed) {
161
196
  setExternalEastIsCollapsed(bool);
162
197
  } else {
163
- setLocalEastIsCollapsed(bool);
198
+ localEastIsCollapsedRef.current = bool;
164
199
  }
165
200
 
166
201
  if (id) {
167
202
  setSaved(id + '-eastIsCollapsed', bool);
168
203
  }
204
+ forceUpdate();
169
205
  },
170
206
  getEastIsCollapsed = () => {
171
207
  if (setExternalEastIsCollapsed) {
172
208
  return eastIsCollapsed;
173
209
  }
174
- return localEastIsCollapsed;
210
+ return localEastIsCollapsedRef.current;
175
211
  },
176
212
  setWestIsCollapsed = (bool) => {
177
213
  if (setExternalWestIsCollapsed) {
178
214
  setExternalWestIsCollapsed(bool);
179
215
  } else {
180
- setLocalWestIsCollapsed(bool);
216
+ localWestIsCollapsedRef.current = bool;
181
217
  }
182
218
 
183
219
  if (id) {
184
220
  setSaved(id + '-westIsCollapsed', bool);
185
221
  }
222
+ forceUpdate();
186
223
  },
187
224
  getWestIsCollapsed = () => {
188
225
  if (setExternalWestIsCollapsed) {
189
226
  return westIsCollapsed;
190
227
  }
191
- return localWestIsCollapsed;
228
+ return localWestIsCollapsedRef.current;
192
229
  },
193
230
  setNorthHeight = (height) => {
194
231
  if (!getNorthIsCollapsed()) {
@@ -289,53 +326,72 @@ function Container(props) {
289
326
 
290
327
  if (id) {
291
328
  let key, val;
292
- key = id + '-northIsCollapsed';
293
- val = await getSaved(key);
294
- if (!_.isNil(val)) {
295
- setNorthIsCollapsed(val);
296
- }
297
329
 
298
- key = id + '-southIsCollapsed';
330
+ // does screensize from previous render exist?
331
+ key = id + '-prevScreenSize';
299
332
  val = await getSaved(key);
333
+ let prevScreenSize = null;
300
334
  if (!_.isNil(val)) {
301
- setSouthIsCollapsed(val);
335
+ prevScreenSize = val;
302
336
  }
337
+ const currentScreenSize = {
338
+ width: windowSize?.width ?? null,
339
+ height: windowSize?.height ?? null,
340
+ };
341
+ if (!prevScreenSize || (prevScreenSize.width === currentScreenSize.width && prevScreenSize.height === currentScreenSize.height)) {
342
+
343
+ // only load these saved settings if the screen size is the same as when they were saved
344
+ key = id + '-northIsCollapsed';
345
+ val = await getSaved(key);
346
+ if (!_.isNil(val)) {
347
+ setNorthIsCollapsed(val);
348
+ }
303
349
 
304
- key = id + '-eastIsCollapsed';
305
- val = await getSaved(key);
306
- if (!_.isNil(val)) {
307
- setEastIsCollapsed(val);
308
- }
350
+ key = id + '-southIsCollapsed';
351
+ val = await getSaved(key);
352
+ if (!_.isNil(val)) {
353
+ setSouthIsCollapsed(val);
354
+ }
309
355
 
310
- key = id + '-westIsCollapsed';
311
- val = await getSaved(key);
312
- if (!_.isNil(val)) {
313
- setWestIsCollapsed(val);
314
- }
356
+ key = id + '-eastIsCollapsed';
357
+ val = await getSaved(key);
358
+ if (!_.isNil(val)) {
359
+ setEastIsCollapsed(val);
360
+ }
315
361
 
316
- key = id + '-northHeight';
317
- val = await getSaved(key);
318
- if (!_.isNil(val)) {
319
- setNorthHeight(val);
320
- }
362
+ key = id + '-westIsCollapsed';
363
+ val = await getSaved(key);
364
+ if (!_.isNil(val)) {
365
+ setWestIsCollapsed(val);
366
+ }
321
367
 
322
- key = id + '-southHeight';
323
- val = await getSaved(key);
324
- if (!_.isNil(val)) {
325
- setSouthHeight(val);
326
- }
368
+ key = id + '-northHeight';
369
+ val = await getSaved(key);
370
+ if (!_.isNil(val)) {
371
+ setNorthHeight(val);
372
+ }
327
373
 
328
- key = id + '-eastWidth';
329
- val = await getSaved(key);
330
- if (!_.isNil(val)) {
331
- setEastWidth(val);
332
- }
374
+ key = id + '-southHeight';
375
+ val = await getSaved(key);
376
+ if (!_.isNil(val)) {
377
+ setSouthHeight(val);
378
+ }
333
379
 
334
- key = id + '-westWidth';
335
- val = await getSaved(key);
336
- if (!_.isNil(val)) {
337
- setWestWidth(val);
380
+ key = id + '-eastWidth';
381
+ val = await getSaved(key);
382
+ if (!_.isNil(val)) {
383
+ setEastWidth(val);
384
+ }
385
+
386
+ key = id + '-westWidth';
387
+ val = await getSaved(key);
388
+ if (!_.isNil(val)) {
389
+ setWestWidth(val);
390
+ }
391
+
338
392
  }
393
+
394
+
339
395
  }
340
396
 
341
397
  if (!isReady) {
@@ -348,7 +404,7 @@ function Container(props) {
348
404
  return null;
349
405
  }
350
406
 
351
- let componentProps = {},
407
+ let componentProps = { _panel: { ...center?.props?._panel }, },
352
408
  wrapperProps = null,
353
409
  centerComponent = null,
354
410
  northComponent = null,
@@ -360,15 +416,16 @@ function Container(props) {
360
416
  westComponent = null,
361
417
  westSplitter = null;
362
418
 
363
- componentProps.isCollapsible = false;
364
- componentProps.isDisabled = isDisabled || isComponentsDisabled;
419
+ componentProps._panel.isCollapsible = false;
420
+ componentProps._panel.isDisabled = isDisabled || isComponentsDisabled;
421
+ componentProps.onLayout = debouncedOnLayout;
365
422
  centerComponent = cloneElement(center, componentProps);
366
423
  if (north) {
367
- componentProps = {};
424
+ componentProps = { _panel: { ...north.props?._panel }, };
368
425
  wrapperProps = {};
369
426
 
370
- componentProps.isDisabled = isDisabled || isComponentsDisabled;
371
- componentProps.className = 'h-full w-full ' + (north.props.className || '');
427
+ componentProps._panel.isDisabled = isDisabled || isComponentsDisabled;
428
+ componentProps._panel.className = 'h-full w-full ' + (north.props.className || '');
372
429
  wrapperProps.onLayout = (e) => {
373
430
  const height = parseFloat(e.nativeEvent.layout.height);
374
431
  if (height && height !== northHeight) {
@@ -387,9 +444,9 @@ function Container(props) {
387
444
  wrapperProps.style = { height: northHeight, };
388
445
  }
389
446
  }
390
- componentProps.collapseDirection = VERTICAL;
391
- componentProps.isCollapsed = getNorthIsCollapsed();
392
- componentProps.setIsCollapsed = setNorthIsCollapsed;
447
+ componentProps._panel.collapseDirection = VERTICAL;
448
+ componentProps._panel.isCollapsed = getNorthIsCollapsed();
449
+ componentProps._panel.setIsCollapsed = setNorthIsCollapsed;
393
450
  if (isWeb && northIsResizable) {
394
451
  northSplitter = <Splitter
395
452
  mode={VERTICAL}
@@ -402,11 +459,11 @@ function Container(props) {
402
459
  </BoxNative>;
403
460
  }
404
461
  if (south) {
405
- componentProps = {};
462
+ componentProps = { _panel: { ...south.props?._panel }, };
406
463
  wrapperProps = {};
407
464
 
408
- componentProps.isDisabled = isDisabled || isComponentsDisabled;
409
- componentProps.className = 'h-full w-full ' + (south.props.className || '');
465
+ componentProps._panel.isDisabled = isDisabled || isComponentsDisabled;
466
+ componentProps._panel.className = 'h-full w-full ' + (south.props.className || '');
410
467
  wrapperProps.onLayout = (e) => {
411
468
  const height = parseFloat(e.nativeEvent.layout.height);
412
469
  if (height && height !== getSouthHeight()) {
@@ -425,9 +482,9 @@ function Container(props) {
425
482
  wrapperProps.style = { height: southHeight, };
426
483
  }
427
484
  }
428
- componentProps.collapseDirection = VERTICAL;
429
- componentProps.isCollapsed = getSouthIsCollapsed();
430
- componentProps.setIsCollapsed = setSouthIsCollapsed;
485
+ componentProps._panel.collapseDirection = VERTICAL;
486
+ componentProps._panel.isCollapsed = getSouthIsCollapsed();
487
+ componentProps._panel.setIsCollapsed = setSouthIsCollapsed;
431
488
  if (isWeb && southIsResizable) {
432
489
  southSplitter = <Splitter
433
490
  mode={VERTICAL}
@@ -440,11 +497,11 @@ function Container(props) {
440
497
  </BoxNative>;
441
498
  }
442
499
  if (east) {
443
- componentProps = {};
500
+ componentProps = { _panel: { ...east.props?._panel }, };
444
501
  wrapperProps = {};
445
502
 
446
- componentProps.isDisabled = isDisabled || isComponentsDisabled;
447
- componentProps.className = 'h-full w-full ' + (east.props.className || '');
503
+ componentProps._panel.isDisabled = isDisabled || isComponentsDisabled;
504
+ componentProps._panel.className = 'h-full w-full ' + (east.props.className || '');
448
505
  wrapperProps.onLayout = (e) => {
449
506
  const width = parseFloat(e.nativeEvent.layout.width);
450
507
  if (width && width !== getEastWidth()) {
@@ -463,9 +520,9 @@ function Container(props) {
463
520
  wrapperProps.style = { width: eastWidth, };
464
521
  }
465
522
  }
466
- componentProps.collapseDirection = HORIZONTAL;
467
- componentProps.isCollapsed = getEastIsCollapsed();
468
- componentProps.setIsCollapsed = setEastIsCollapsed;
523
+ componentProps._panel.collapseDirection = HORIZONTAL;
524
+ componentProps._panel.isCollapsed = getEastIsCollapsed();
525
+ componentProps._panel.setIsCollapsed = setEastIsCollapsed;
469
526
  if (isWeb && eastIsResizable) {
470
527
  eastSplitter = <Splitter
471
528
  mode={HORIZONTAL}
@@ -478,11 +535,11 @@ function Container(props) {
478
535
  </BoxNative>;
479
536
  }
480
537
  if (west) {
481
- componentProps = {};
538
+ componentProps = { _panel: { ...west.props?._panel }, };
482
539
  wrapperProps = {};
483
540
 
484
- componentProps.isDisabled = isDisabled || isComponentsDisabled;
485
- componentProps.className = 'h-full w-full ' + (west.props.className || '');
541
+ componentProps._panel.isDisabled = isDisabled || isComponentsDisabled;
542
+ componentProps._panel.className = 'h-full w-full ' + (west.props.className || '');
486
543
  wrapperProps.onLayout = (e) => {
487
544
  const width = parseFloat(e.nativeEvent.layout.width);
488
545
  if (width && width !== getWestWidth()) {
@@ -501,9 +558,9 @@ function Container(props) {
501
558
  wrapperProps.style = { width: westWidth, };
502
559
  }
503
560
  }
504
- componentProps.collapseDirection = HORIZONTAL;
505
- componentProps.isCollapsed = getWestIsCollapsed();
506
- componentProps.setIsCollapsed = setWestIsCollapsed;
561
+ componentProps._panel.collapseDirection = HORIZONTAL;
562
+ componentProps._panel.isCollapsed = getWestIsCollapsed();
563
+ componentProps._panel.setIsCollapsed = setWestIsCollapsed;
507
564
  if (isWeb && westIsResizable) {
508
565
  westSplitter = <Splitter
509
566
  mode={HORIZONTAL}
@@ -517,17 +574,17 @@ function Container(props) {
517
574
  }
518
575
  return <VStack className="Container-all w-full flex-1">
519
576
  {northComponent}
520
- {!northIsCollapsed && !localNorthIsCollapsed && northSplitter}
577
+ {!getNorthIsCollapsed() && northSplitter}
521
578
  <HStack className="Container-mid w-full flex-[100]">
522
579
  {westComponent}
523
- {!westIsCollapsed && !localWestIsCollapsed && westSplitter}
580
+ {!getWestIsCollapsed() && westSplitter}
524
581
  <VStack className="Container-center h-full overflow-auto flex-[100]">
525
582
  {centerComponent}
526
583
  </VStack>
527
- {!eastIsCollapsed && !localEastIsCollapsed && eastSplitter}
584
+ {!getEastIsCollapsed() && eastSplitter}
528
585
  {eastComponent}
529
586
  </HStack>
530
- {!southIsCollapsed && !localSouthIsCollapsed && southSplitter}
587
+ {!getSouthIsCollapsed() && southSplitter}
531
588
  {southComponent}
532
589
  </VStack>;
533
590
  }
@@ -522,6 +522,8 @@ function Form(props) {
522
522
  }
523
523
  let {
524
524
  type,
525
+ editorType: itemEditorType,
526
+ viewerType,
525
527
  title,
526
528
  name,
527
529
  isEditable = true,
@@ -560,12 +562,18 @@ function Form(props) {
560
562
  }
561
563
  if (!type) {
562
564
  if (isEditable) {
563
- const {
564
- type: t,
565
- ...p
566
- } = propertyDef?.editorType;
567
- type = t;
568
- editorTypeProps = p;
565
+ if (itemEditorType) {
566
+ type = itemEditorType;
567
+ } else {
568
+ const {
569
+ type: t,
570
+ ...p
571
+ } = propertyDef?.editorType;
572
+ type = t;
573
+ editorTypeProps = p;
574
+ }
575
+ } else if (viewerType) {
576
+ type = viewerType;
569
577
  } else if (propertyDef?.viewerType) {
570
578
  const {
571
579
  type: t,
@@ -1390,6 +1398,7 @@ function Form(props) {
1390
1398
  onPress={() => doReset()}
1391
1399
  icon={Rotate}
1392
1400
  isDisabled={!formState.isDirty}
1401
+ tooltip="Reset Form"
1393
1402
  />}
1394
1403
 
1395
1404
  {showCancelBtn &&
@@ -423,7 +423,16 @@ function GridComponent(props) {
423
423
  }
424
424
  },
425
425
  getFooterToolbarItems = () => {
426
- const items = _.map(additionalToolbarButtons, (config, ix) => getIconButtonFromConfig(config, ix, self));
426
+ // Process additionalToolbarButtons to evaluate getIsButtonDisabled functions
427
+ const processedButtons = _.map(additionalToolbarButtons, (config) => {
428
+ const processedConfig = { ...config };
429
+ // If the button has an getIsButtonDisabled function, evaluate it with current selection
430
+ if (_.isFunction(config.getIsButtonDisabled)) {
431
+ processedConfig.isDisabled = config.getIsButtonDisabled(selection);
432
+ }
433
+ return processedConfig;
434
+ });
435
+ const items = _.map(processedButtons, (config, ix) => getIconButtonFromConfig(config, ix, self));
427
436
  if (canRowsReorder && CURRENT_MODE === UI_MODE_WEB) { // DND is currently web-only TODO: implement for RN
428
437
  items.unshift(<IconButton
429
438
  {...testProps('reorderBtn')}
@@ -476,33 +485,41 @@ function GridComponent(props) {
476
485
  }
477
486
  break;
478
487
  case DOUBLE_CLICK:
479
- if (!isSelected) { // If a row was already selected when double-clicked, the first click will deselect it,
480
- onRowClick(item, e); // so reselect it
481
- }
482
-
483
- if (UiGlobals.doubleClickingGridRowOpensEditorInViewMode) { // global setting
484
- if (onView) {
485
- if (canUser && !canUser(VIEW)) { // permissions
486
- return;
487
- }
488
- onView(!props.isEditorViewOnly);
488
+ if (editorType === EDITOR_TYPE__SIDE) {
489
+ // For side-editors, a double-click just acts like a single click
490
+ if (!getIsEditorShown()) {
491
+ onRowClick(item, e); // sets selection
489
492
  }
490
493
  } else {
491
- let canDoEdit = false,
492
- canDoView = false;
493
- if (onEdit && canUser && canUser(EDIT) && (!canRecordBeEdited || canRecordBeEdited(selection)) && !props.disableEdit && !isEditorViewOnly) {
494
- canDoEdit = true;
495
- } else
496
- if (onView && canUser && canUser(VIEW) && !props.disableView) {
497
- canDoView = true;
494
+ if (!isSelected) { // If a row was already selected when double-clicked, the first click will deselect it,
495
+ onRowClick(item, e); // so reselect it
498
496
  }
497
+ if (UiGlobals.doubleClickingGridRowOpensEditorInViewMode) { // global setting
498
+ if (onView) {
499
+ if (canUser && !canUser(VIEW)) { // permissions
500
+ return;
501
+ }
502
+ onView(!props.isEditorViewOnly);
503
+ }
504
+ } else {
505
+ let canDoEdit = false,
506
+ canDoView = false;
507
+ if (onEdit && canUser && canUser(EDIT) && (!canRecordBeEdited || canRecordBeEdited(selection)) && !props.disableEdit && !isEditorViewOnly) {
508
+ canDoEdit = true;
509
+ } else
510
+ if (onView && canUser && canUser(VIEW) && !props.disableView) {
511
+ canDoView = true;
512
+ }
499
513
 
500
- if (canDoEdit) {
501
- onEdit();
502
- } else if (canDoView) {
503
- onView();
514
+ if (canDoEdit) {
515
+ onEdit();
516
+ } else if (canDoView) {
517
+ onView();
518
+ }
504
519
  }
520
+
505
521
  }
522
+
506
523
  break;
507
524
  case TRIPLE_CLICK:
508
525
  break;
@@ -1562,10 +1579,19 @@ function GridComponent(props) {
1562
1579
  const
1563
1580
  currentLength = entities?.length || 0,
1564
1581
  wasEmpty = previousEntitiesLength.current === 0,
1565
- isNowPopulated = currentLength > 0;
1582
+ isNowPopulated = currentLength > 0,
1583
+ hasPhantomRecord = entities?.some(entity => entity?.isPhantom);
1584
+
1585
+ // NOTE: The Repository was reloading when a phantom record was added,
1586
+ // and this broke the Editor because selection was being reset to zero.
1587
+ // This is because adjustPageSizeToHeight calls setPageSize,
1588
+ // which calls _onChangePagination, which calls reload.
1589
+ // The reloaded repository doesn’t get the new phantom record,
1590
+ // so it’s not found, and selection goes to zero.
1591
+ // So we skip this adjustment when there is a phantom record.
1566
1592
 
1567
1593
  // Only remeasure the FIRST time rows appear after being empty
1568
- if (autoAdjustPageSizeToHeight && wasEmpty && isNowPopulated && !hasRemeasuredAfterRowsAppeared.current) {
1594
+ if (!hasPhantomRecord && autoAdjustPageSizeToHeight && wasEmpty && isNowPopulated && !hasRemeasuredAfterRowsAppeared.current) {
1569
1595
  // Rows just appeared for the first time - restart measurement cycle to use actual heights
1570
1596
  if (DEBUG) {
1571
1597
  console.log(`${getMeasurementPhase()}, useEffect 5 - rows appeared for first time, restarting measurement cycle`);
@@ -1602,13 +1628,24 @@ function GridComponent(props) {
1602
1628
  // first time through, render a placeholder so we can get container dimensions
1603
1629
  return <VStackNative
1604
1630
  onLayout={(e) => {
1605
- if (DEBUG) {
1606
- console.log(`${getMeasurementPhase()}, placeholder onLayout, call adjustPageSizeToHeight()`);
1607
- }
1608
- const containerHeight = e.nativeEvent.layout.height;
1609
- adjustPageSizeToHeight(containerHeight);
1610
- if (DEBUG) {
1611
- console.log(`${getMeasurementPhase()}, placeholder onLayout, call setIsInited(true)`);
1631
+ const hasPhantomRecord = entities?.some(entity => entity?.isPhantom);
1632
+ if (!hasPhantomRecord) {
1633
+ // NOTE: The Repository was reloading when a phantom record was added,
1634
+ // and this broke the Editor because selection was being reset to zero.
1635
+ // This is because adjustPageSizeToHeight calls setPageSize,
1636
+ // which calls _onChangePagination, which calls reload.
1637
+ // The reloaded repository doesn’t get the new phantom record,
1638
+ // so it’s not found, and selection goes to zero.
1639
+ // So we skip this adjustment when there is a phantom record.
1640
+
1641
+ if (DEBUG) {
1642
+ console.log(`${getMeasurementPhase()}, placeholder onLayout, call adjustPageSizeToHeight()`);
1643
+ }
1644
+ const containerHeight = e.nativeEvent.layout.height;
1645
+ adjustPageSizeToHeight(containerHeight);
1646
+ if (DEBUG) {
1647
+ console.log(`${getMeasurementPhase()}, placeholder onLayout, call setIsInited(true)`);
1648
+ }
1612
1649
  }
1613
1650
  setIsInited(true);
1614
1651
  }}
@@ -1785,14 +1822,11 @@ function GridComponent(props) {
1785
1822
  )}
1786
1823
  >
1787
1824
  {grid}
1788
- {/* Loading overlay during measurement phases to prevent visual flashing */}
1789
- {autoAdjustPageSizeToHeight &&
1790
- (getMeasurementPhase() === PHASES__INITIAL || getMeasurementPhase() === PHASES__MEASURING) &&
1791
- entities?.length > 0 && (
1825
+ {/* Load overlay during initial phase to prevent visual flashing * /
1826
+ autoAdjustPageSizeToHeight && getMeasurementPhase() === PHASES__INITIAL &&
1792
1827
  <VStack className="absolute inset-0 z-10 bg-white">
1793
1828
  <Loading />
1794
- </VStack>
1795
- )}
1829
+ </VStack>*/}
1796
1830
  </VStack>
1797
1831
 
1798
1832
  {listFooterComponent}
@@ -38,6 +38,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
38
38
  disableDuplicate = false,
39
39
  disableView = false,
40
40
  useRemoteDuplicate = false, // call specific copyToNew function on server, rather than simple duplicate on client
41
+ getDuplicateValues, // fn(entity) to get default values for duplication
41
42
  getRecordIdentifier = (selection) => {
42
43
  if (selection.length > 1) {
43
44
  return 'records?';
@@ -89,6 +90,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
89
90
  editorModeRef = useRef(initialEditorMode),
90
91
  isIgnoreNextSelectionChangeRef = useRef(false),
91
92
  isEditorShownRef = useRef(false),
93
+ defaultValuesRef = useRef(defaultValues),
92
94
  canEditorBeInEditModeRef = useRef(true), // whether the editor is allowed to be in edit mode based on canRecordBeEdited
93
95
  [currentRecord, setCurrentRecord] = useState(null),
94
96
  [isAdding, setIsAdding] = useState(false),
@@ -111,6 +113,12 @@ export default function withEditor(WrappedComponent, isTree = false) {
111
113
  getIsEditorShown = () => {
112
114
  return isEditorShownRef.current;
113
115
  },
116
+ getDefaultValues = () => {
117
+ return defaultValuesRef.current;
118
+ },
119
+ setDefaultValues = (vals) => {
120
+ defaultValuesRef.current = vals;
121
+ },
114
122
  setCanEditorBeInEditMode = (bool) => {
115
123
  canEditorBeInEditModeRef.current = bool;
116
124
  forceUpdate();
@@ -164,6 +172,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
164
172
  getNewEntityDisplayValue = () => {
165
173
  return newEntityDisplayValueRef.current;
166
174
  },
175
+ setNewEntityDisplayValue = (val) => {
176
+ newEntityDisplayValueRef.current = val;
177
+ },
167
178
  doAdd = async (e, values) => {
168
179
  if (canUser && !canUser(ADD)) {
169
180
  showPermissionsError(ADD);
@@ -191,8 +202,8 @@ export default function withEditor(WrappedComponent, isTree = false) {
191
202
  // 2. Use the repository's default values (defined on each property as 'defaultValue'), or
192
203
  // 3. Individually override the repository's default values with submitted 'defaultValues' (given as a prop to this HOC)
193
204
  let defaultValuesToUse = Repository.getSchema().getDefaultValues();
194
- if (defaultValues) {
195
- _.merge(defaultValuesToUse, defaultValues);
205
+ if (getDefaultValues()) {
206
+ _.merge(defaultValuesToUse, getDefaultValues());
196
207
  }
197
208
  addValues = {...defaultValuesToUse};
198
209
  }
@@ -458,7 +469,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
458
469
  const
459
470
  entity = selection[0],
460
471
  idProperty = Repository.getSchema().model.idProperty,
461
- rawValues = _.omit(entity.getOriginalData(), idProperty);
472
+ rawValues = getDuplicateValues ? getDuplicateValues(entity) : _.omit(entity.getOriginalData(), idProperty);
462
473
  rawValues.id = null; // unset the id of the duplicate
463
474
 
464
475
  setIsWaitModalShown(true);
@@ -738,7 +749,8 @@ export default function withEditor(WrappedComponent, isTree = false) {
738
749
  self.duplicate = doDuplicate;
739
750
  self.setIsEditorShown = setIsEditorShown;
740
751
  }
741
- newEntityDisplayValueRef.current = newEntityDisplayValue;
752
+ setNewEntityDisplayValue(newEntityDisplayValue);
753
+ setDefaultValues(defaultValues);
742
754
 
743
755
  if (lastSelection !== selection) {
744
756
  // NOTE: If I don't calculate this on the fly for selection changes,