@onehat/ui 0.4.82 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onehat/ui",
3
- "version": "0.4.82",
3
+ "version": "0.4.83",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -58,17 +58,18 @@
58
58
  "@k-renwick/colour-mixer": "^1.2.1",
59
59
  "@legendapp/motion": "^2.4.0",
60
60
  "@onehat/data": "^1.22.0",
61
- "clsx": "^2.1.1",
62
- "nativewind": "4.1.23",
63
- "normalize-css-color": "^1.0.2",
64
61
  "@react-native-community/slider": "^4.5.2",
65
62
  "@reduxjs/toolkit": "^2.6.1",
63
+ "clsx": "^2.1.1",
66
64
  "decimal.js": "^10.5.0",
67
65
  "inflector-js": "^1.0.1",
68
66
  "js-cookie": "^3.0.5",
67
+ "nativewind": "4.1.23",
68
+ "normalize-css-color": "^1.0.2",
69
69
  "react-hook-form": "^7.55.0",
70
70
  "react-native-progress": "^5.0.1",
71
71
  "react-redux": "^9.2.0",
72
+ "tailwind-scrollbar": "^3.1.0",
72
73
  "tailwindcss": "^3.4.17",
73
74
  "yup": "^1.6.1"
74
75
  },
@@ -79,10 +80,10 @@
79
80
  "react": "*",
80
81
  "react-color": "^2.19.3",
81
82
  "react-datetime": "^3.2.0",
82
- "react-dom": "*",
83
83
  "react-dnd": "^16.0.1",
84
84
  "react-dnd-html5-backend": "^16.0.1",
85
- "react-dnd-touch-backend":"16.0.1",
85
+ "react-dnd-touch-backend": "16.0.1",
86
+ "react-dom": "*",
86
87
  "react-draggable": "^4.4.5",
87
88
  "react-native": "*",
88
89
  "react-native-draggable": "^3.3.0",
@@ -24,6 +24,7 @@ import testProps from '../../../../Functions/testProps.js';
24
24
  import UiGlobals from '../../../../UiGlobals.js';
25
25
  import Input from '../Input.js';
26
26
  import { Grid, WindowedGridEditor } from '../../../Grid/Grid.js';
27
+ import useForceUpdate from '../../../../Hooks/useForceUpdate.js';
27
28
  import withAlert from '../../../Hoc/withAlert.js';
28
29
  import withComponent from '../../../Hoc/withComponent.js';
29
30
  import withData from '../../../Hoc/withData.js';
@@ -121,13 +122,16 @@ export const ComboComponent = forwardRef((props, ref) => {
121
122
  setValue,
122
123
  } = props,
123
124
  styles = UiGlobals.styles,
125
+ forceUpdate = useForceUpdate(),
124
126
  inputRef = useRef(),
125
127
  inputCloneRef = useRef(),
126
128
  triggerRef = useRef(),
127
129
  menuRef = useRef(),
128
130
  displayValueRef = useRef(),
129
131
  typingTimeout = useRef(),
130
- [isMenuShown, setIsMenuShown] = useState(false),
132
+ isMenuShown = useRef(false),
133
+ isGridLayoutRunWithRender = useRef(false),
134
+ [isMenuAbove, setIsMenuAbove] = useState(false),
131
135
  [isViewerShown, setIsViewerShown] = useState(false),
132
136
  [viewerSelection, setViewerSelection] = useState([]),
133
137
  [isRendered, setIsRendered] = useState(false),
@@ -140,15 +144,39 @@ export const ComboComponent = forwardRef((props, ref) => {
140
144
  [newEntityDisplayValue, setNewEntityDisplayValue] = useState(null),
141
145
  [filteredData, setFilteredData] = useState(data),
142
146
  [inputHeight, setInputHeight] = useState(0),
147
+ [menuRenderedHeight, setMenuRenderedHeight] = useState(0),
143
148
  [width, setWidth] = useState(0),
144
149
  [top, setTop] = useState(0),
145
150
  [left, setLeft] = useState(0),
151
+ getIsMenuShown = () => {
152
+ return isMenuShown.current;
153
+ },
154
+ setIsMenuShown = (bool) => {
155
+ isMenuShown.current = bool;
156
+
157
+ if (!bool) {
158
+ // The menu's onLayout runs every time there's a change in its size or position.
159
+ // We're only interested in the *first* time it runs with a rendered height.
160
+ // So if hiding the menu, reset isGridLayoutRunWithRender here and we'll set it to true
161
+ // the first time onLayout runs with a height.
162
+ setIsGridLayoutRunWithRender(false);
163
+ setIsMenuAbove(false); // reset this so the next time the menu opens, it starts below the input
164
+ }
165
+
166
+ forceUpdate();
167
+ },
168
+ getIsGridLayoutRunWithRender = () => {
169
+ return isGridLayoutRunWithRender.current;
170
+ },
171
+ setIsGridLayoutRunWithRender = (bool) => {
172
+ isGridLayoutRunWithRender.current = bool;
173
+ },
146
174
  onLayout = (e) => {
147
175
  setIsRendered(true);
148
176
  setContainerWidth(e.nativeEvent.layout.width);
149
177
  },
150
178
  showMenu = async () => {
151
- if (isMenuShown) {
179
+ if (getIsMenuShown()) {
152
180
  return;
153
181
  }
154
182
  if (CURRENT_MODE === UI_MODE_WEB && inputRef.current?.getBoundingClientRect) {
@@ -181,13 +209,13 @@ export const ComboComponent = forwardRef((props, ref) => {
181
209
  setIsMenuShown(true);
182
210
  },
183
211
  hideMenu = () => {
184
- if (!isMenuShown) {
212
+ if (!getIsMenuShown()) {
185
213
  return;
186
214
  }
187
215
  setIsMenuShown(false);
188
216
  },
189
217
  toggleMenu = () => {
190
- setIsMenuShown(!isMenuShown);
218
+ setIsMenuShown(!getIsMenuShown());
191
219
  },
192
220
  temporarilySetIsNavigatingViaKeyboard = () => {
193
221
  setIsNavigatingViaKeyboard(true);
@@ -359,14 +387,14 @@ export const ComboComponent = forwardRef((props, ref) => {
359
387
  if (reloadOnTrigger && Repository) {
360
388
  await Repository.reload();
361
389
  }
362
- if (isMenuShown) {
390
+ if (getIsMenuShown()) {
363
391
  hideMenu();
364
392
  } else {
365
393
  showMenu();
366
394
  }
367
395
  },
368
396
  onTriggerBlur = (e) => {
369
- if (!isMenuShown) {
397
+ if (!getIsMenuShown()) {
370
398
  return;
371
399
  }
372
400
 
@@ -409,6 +437,33 @@ export const ComboComponent = forwardRef((props, ref) => {
409
437
  onCheckButtonPress = () => {
410
438
  hideMenu();
411
439
  },
440
+ onGridLayout = (e) => {
441
+ // This method is to determine if we need to flip the grid above the input
442
+ // because the menu is partially offscreen
443
+
444
+ if (CURRENT_MODE !== UI_MODE_WEB || !e.nativeEvent.layout.height) {
445
+ return;
446
+ }
447
+
448
+ // we reach this point only if the grid has rendered with a height.
449
+
450
+ if (!getIsGridLayoutRunWithRender()) {
451
+ // we reach this point only on the *first* time onGridLayout runs with a height.
452
+ // determine if the menu is partially offscreen
453
+ const
454
+ menuRect = menuRef.current.getBoundingClientRect(),
455
+ inputRect = inputRef.current.getBoundingClientRect(),
456
+ menuOverflows = menuRect.bottom > window.innerHeight;
457
+ if (menuOverflows) {
458
+ // flip it
459
+ setIsMenuAbove(true);
460
+ } else {
461
+ setIsMenuAbove(false);
462
+ }
463
+ setMenuRenderedHeight(e.nativeEvent.layout.height);
464
+ setIsGridLayoutRunWithRender(true);
465
+ }
466
+ },
412
467
  isEventStillInComponent = (e) => {
413
468
  const {
414
469
  relatedTarget
@@ -518,7 +573,7 @@ export const ComboComponent = forwardRef((props, ref) => {
518
573
  setFilteredData(found);
519
574
  }
520
575
 
521
- if (!isMenuShown) {
576
+ if (!getIsMenuShown()) {
522
577
  showMenu();
523
578
  }
524
579
  setIsSearchMode(true);
@@ -734,7 +789,7 @@ export const ComboComponent = forwardRef((props, ref) => {
734
789
  </Pressable>;
735
790
  }
736
791
 
737
- if (isMenuShown) {
792
+ if (getIsMenuShown()) {
738
793
  const gridProps = _.pick(props, [
739
794
  'Editor',
740
795
  'model',
@@ -743,6 +798,9 @@ export const ComboComponent = forwardRef((props, ref) => {
743
798
  'idIx',
744
799
  'displayIx',
745
800
  // 'value',
801
+ 'disableAdd',
802
+ 'disableEdit',
803
+ 'disableDelete',
746
804
  'disableView',
747
805
  'disableCopy',
748
806
  'disableDuplicate',
@@ -761,8 +819,9 @@ export const ComboComponent = forwardRef((props, ref) => {
761
819
  if (!Repository) {
762
820
  gridProps.data = filteredData;
763
821
  }
764
- const WhichGrid = isEditor ? WindowedGridEditor : Grid;
765
- const gridStyle = {};
822
+ const
823
+ WhichGrid = isEditor ? WindowedGridEditor : Grid,
824
+ gridStyle = {};
766
825
  if (CURRENT_MODE === UI_MODE_WEB) {
767
826
  gridStyle.height = menuHeight || styles.FORM_COMBO_MENU_HEIGHT;
768
827
  }
@@ -786,6 +845,7 @@ export const ComboComponent = forwardRef((props, ref) => {
786
845
  disablePresetButtons={!isEditor}
787
846
  alternateRowBackgrounds={false}
788
847
  showSelectHandle={false}
848
+ onLayout={onGridLayout}
789
849
  onChangeSelection={(selection) => {
790
850
 
791
851
  if (Repository && selection[0]?.isPhantom) {
@@ -938,7 +998,7 @@ export const ComboComponent = forwardRef((props, ref) => {
938
998
  </Box>;
939
999
  }
940
1000
  dropdownMenu = <Popover
941
- isOpen={isMenuShown}
1001
+ isOpen={getIsMenuShown()}
942
1002
  onClose={() => {
943
1003
  hideMenu();
944
1004
  }}
@@ -962,15 +1022,24 @@ export const ComboComponent = forwardRef((props, ref) => {
962
1022
  'max-w-full',
963
1023
  )}
964
1024
  style={{
965
- top,
1025
+ // If flipped, position above input; otherwise, below
1026
+ top: isMenuAbove
1027
+ ? (top - menuRenderedHeight) // above
1028
+ : top, // below
966
1029
  left,
967
1030
  width,
968
- // height: (menuHeight || styles.FORM_COMBO_MENU_HEIGHT) + inputHeight,
969
1031
  minWidth: 100,
970
1032
  }}
971
1033
  >
972
- {inputClone}
973
- {grid}
1034
+ {isMenuAbove ?
1035
+ <>
1036
+ {grid}
1037
+ {inputClone}
1038
+ </> :
1039
+ <>
1040
+ {inputClone}
1041
+ {grid}
1042
+ </>}
974
1043
  </Box>
975
1044
  </Popover>;
976
1045
  }
@@ -21,6 +21,7 @@ export function JsonElement(props) {
21
21
  tooltip = null,
22
22
  isDisabled = false,
23
23
  isViewOnly = false,
24
+ isCollapsed = true,
24
25
  tooltipPlacement = 'bottom',
25
26
  testID,
26
27
 
@@ -59,7 +60,7 @@ export function JsonElement(props) {
59
60
  editable={!isViewOnly}
60
61
  src={src}
61
62
  enableClipboard={false}
62
- collapsed={true}
63
+ collapsed={isCollapsed}
63
64
  onEdit={(obj) => {
64
65
  setValue(JSON.stringify(obj.updated_src));
65
66
  }}
@@ -12,6 +12,8 @@ import {
12
12
  import {
13
13
  EDITOR_TYPE__PLAIN,
14
14
  } from '../../../../Constants/Editor.js';
15
+ import Button from '../../../Buttons/Button.js';
16
+ import testProps from '../../../../Functions/testProps.js';
15
17
  import Form from '../../Form.js';
16
18
  import Viewer from '../../../Viewer/Viewer.js';
17
19
  import withAlert from '../../../Hoc/withAlert.js';
@@ -36,11 +38,13 @@ function TagComponent(props) {
36
38
  Editor,
37
39
  _combo = {},
38
40
  SourceRepository,
41
+ mustSaveBeforeEditingJoinData = false,
39
42
  joinDataConfig,
40
- outerValueId, // for recursion only. See note in useEffect
43
+ getBaseParams, // See note in useEffect
44
+ outerValueId, // See note in useEffect
41
45
  tooltip,
42
46
  testID,
43
- getBaseParams,
47
+ isDirty = false,
44
48
 
45
49
  // parent Form
46
50
  onChangeValue,
@@ -77,11 +81,11 @@ function TagComponent(props) {
77
81
  }
78
82
  return null;
79
83
  }),
80
- [isInited, setIsInited] = useState(false),
84
+ [isInited, setIsInited] = useState(_.isUndefined(getBaseParams)), // default to true unless getBaseParams is defined
81
85
  modelFieldStartsWith = hasJoinData ? Inflector.underscore(JoinRepository.getSchema().name) + '__' : '',
82
86
  valueRef = useRef(value),
83
87
  onView = async (item, e) => {
84
- // This method shows the record viewer
88
+ // show the joined record's viewer
85
89
  const
86
90
  id = item.id,
87
91
  repository = TargetRepository;
@@ -142,7 +146,7 @@ function TagComponent(props) {
142
146
  }
143
147
 
144
148
  // The value we get from combo is a simple int
145
- // Convert this to id and displayValue from either Repository or data array.
149
+ // Convert this to { id, text} from either Repository or data array.
146
150
  const
147
151
  data = props.data,
148
152
  idIx = props.idIx,
@@ -171,32 +175,36 @@ function TagComponent(props) {
171
175
 
172
176
  let joinData = {};
173
177
  if (hasJoinData) {
174
- // build up the default starting values,
178
+ // build up the default starting values for joinData,
175
179
  // first with schema defaultValues...
176
180
  const
177
181
  allSchemaDefaults = JoinRepository.getSchema().getDefaultValues(),
178
182
  modelSchemaDefaults = _.pickBy(allSchemaDefaults, (value, key) => {
179
183
  return key.startsWith(modelFieldStartsWith);
180
184
  }),
181
- joinFieldNames = joinDataConfig.map(fieldConfig => fieldConfig.name || fieldConfig),
182
- schemaDefaultValues = _.pick(modelSchemaDefaults, joinFieldNames),
183
- strippedSchemaDefaultValues = _.mapKeys(schemaDefaultValues, (value, key) => {
184
- return key.startsWith(modelFieldStartsWith) ? key.slice(modelFieldStartsWith.length) : key;
185
- });
186
-
187
- // then with default values in joinDataConfig, if they exist
188
- _.each(joinDataConfig, (fieldConfig) => {
189
- if (!_.isNil(fieldConfig.defaultValue)) {
190
- joinData[fieldConfig.name] = fieldConfig.defaultValue;
191
- }
185
+ fullFieldNames = propertyDef.joinData.map((fieldName) => { // add the 'model_name__' prefix so we can get schema default values
186
+ return modelFieldStartsWith + fieldName;
187
+ }),
188
+ schemaDefaultValues = _.pick(modelSchemaDefaults, fullFieldNames);
189
+ joinData = _.mapKeys(schemaDefaultValues, (value, key) => { // strip out the 'model_name__' prefix from field names
190
+ return key.startsWith(modelFieldStartsWith) ? key.slice(modelFieldStartsWith.length) : key;
192
191
  });
193
- joinData = { ...strippedSchemaDefaultValues, ...joinData };
192
+
193
+ // then override with default values in joinDataConfig, if they exist
194
+ if (joinDataConfig) {
195
+ _.each(Object.keys(joinDataConfig), (fieldName) => {
196
+ const fieldConfig = joinDataConfig[fieldName];
197
+ if (!_.isUndefined(fieldConfig.defaultValue)) { // null in jsonDataConfig will override a default value in schema!
198
+ joinData[fieldName] = fieldConfig.defaultValue;
199
+ }
200
+ });
201
+ }
194
202
  }
195
203
 
196
204
 
197
205
  // add new value
198
206
  const
199
- newValue = [...value], // clone, so we trigger a re-render
207
+ newValue = [...value], // clone Tag's full current value (array), so we trigger a re-render after adding the new value
200
208
  newItem = {
201
209
  id,
202
210
  text: displayValue,
@@ -215,8 +223,8 @@ function TagComponent(props) {
215
223
  });
216
224
  setValue(newValue);
217
225
  },
218
- onJoin = async (item, e) => {
219
- // This method shows the joinData viewer/editor
226
+ onViewEditJoinData = async (item, e) => {
227
+ // show the joinData viewer/editor
220
228
 
221
229
  /* item format:
222
230
  item = {
@@ -229,22 +237,27 @@ function TagComponent(props) {
229
237
  }
230
238
  */
231
239
 
232
- // prepend 'model_name__' to the field names, so they match the JoinRepository property names
240
+ // Prepare Form to edit the joinData
233
241
  const
234
- record = _.mapKeys(item.joinData, (value, key) => {
242
+ // create the Form.record, format: { meters_pm_schedules__also_resets: null, meters_pm_schedules__hide_every_n: 5 }
243
+ record = _.mapKeys(item.joinData, (value, key) => { // add the 'model_name__' prefix so we can match JoinRepository property names
235
244
  return modelFieldStartsWith + key;
236
245
  }),
246
+ // create the Form.items
237
247
  items = propertyDef.joinData.map((fieldName) => {
238
248
  let obj = {
239
249
  name: modelFieldStartsWith + fieldName,
240
250
  };
241
- // add in any config from joinDataConfig for this field
251
+
252
+ // add in any specific config for joinData[fieldName]], if it exists
253
+ // (The outer *Editor can configure each Tag field's joinData Form item.
254
+ // This moves that configuration down and adds outerValueId)
242
255
  if (joinDataConfig?.[fieldName]) {
243
- const jdcf = _.clone(joinDataConfig[fieldName]); // don't mutate original
244
- jdcf.outerValueId = item.id;
256
+ const joinDataConfigFieldname = _.clone(joinDataConfig[fieldName]); // don't mutate original
257
+ joinDataConfigFieldname.outerValueId = item.id; // so that joinData can be aware of the value of the inspected ValueBox; see note in useEffect, below
245
258
  obj = {
246
259
  ...obj,
247
- ...jdcf,
260
+ ...joinDataConfigFieldname,
248
261
  };
249
262
  }
250
263
 
@@ -253,6 +266,7 @@ function TagComponent(props) {
253
266
 
254
267
  let height = 300;
255
268
  let body;
269
+ const extraModalProps = {};
256
270
  if (isViewOnly) {
257
271
  // show Viewer
258
272
  body = <Viewer
@@ -263,12 +277,17 @@ function TagComponent(props) {
263
277
  labelWidth: 200,
264
278
  }}
265
279
  />;
280
+
281
+ extraModalProps.customButtons = [
282
+ <Button
283
+ {...testProps('closeBtn')}
284
+ key="closeBtn"
285
+ onPress={hideModal}
286
+ text="Close"
287
+ className="text-white"
288
+ />,
289
+ ];
266
290
  } else {
267
- switch (items.length) {
268
- case 1: height = 250; break;
269
- case 2: height = 400; break;
270
- default: height = 600; break;
271
- }
272
291
  body = <Form
273
292
  editorType={EDITOR_TYPE__PLAIN}
274
293
  isEditorViewOnly={false}
@@ -285,7 +304,7 @@ function TagComponent(props) {
285
304
  ]}
286
305
  onSave={(values)=> {
287
306
 
288
- // strip the 'model__' prefix from the field names
307
+ // strip the 'model_name__' prefix from the field names
289
308
  values = _.mapKeys(values, (value, key) => {
290
309
  return key.startsWith(modelFieldStartsWith) ? key.slice(modelFieldStartsWith.length) : key;
291
310
  });
@@ -303,6 +322,11 @@ function TagComponent(props) {
303
322
  }}
304
323
  />;
305
324
  }
325
+ switch (items.length) {
326
+ case 1: height = 250; break;
327
+ case 2: height = 400; break;
328
+ default: height = 600; break;
329
+ }
306
330
 
307
331
  showModal({
308
332
  title: 'Extra data for "' + item.text + '"',
@@ -312,6 +336,7 @@ function TagComponent(props) {
312
336
  includeReset: false,
313
337
  includeCancel: false,
314
338
  body,
339
+ ...extraModalProps,
315
340
  });
316
341
  },
317
342
  onGridAdd = (selection) => {
@@ -381,54 +406,47 @@ function TagComponent(props) {
381
406
  text={val.text}
382
407
  onView={() => onView(val)}
383
408
  showEye={showEye}
384
- onJoin={() => onJoin(val)}
385
- showJoin={hasJoinData}
409
+ onViewEditJoinData={() => onViewEditJoinData(val)}
410
+ showJoin={hasJoinData && (!mustSaveBeforeEditingJoinData || !isDirty)}
386
411
  onDelete={!isViewOnly ? () => onDelete(val) : null}
387
412
  minimizeForRow={minimizeForRow}
388
413
  />;
389
414
  });
390
415
 
391
- useEffect(() => {
392
-
393
- // NOTE: This useEffect is so we can set the Target baseParams before it loads
394
- // We did this for cases where the Tag field has joinData that's managing a nested Tag field.
395
- // ... This deals with recursion, so gets "alice in wonderland" quickly!
396
- // If that inner Tag field has getBaseParams defined on the joinDataConfig of the outer Tag,
397
- // then that means it needs to set its baseParams dynamically, based on the values that are
398
- // currently set, as well as the value of the outer ValueBox that was clicked on.
399
-
400
- // For example: in the MetersEditor:
401
- // {
402
- // name: 'meters__pm_schedules',
403
- // parent: self,
404
- // reference: 'meters__pm_schedules',
405
- // joinDataConfig: {
406
- // also_resets: {
407
- // getBaseParams: (values, outerValueId) => {
408
- // const
409
- // baseParams = {
410
- // 'conditions[MetersPmSchedules.meter_id]': meter_id, // limit also_resets to those MetersPmSchedules related to this meter
411
- // },
412
- // ids = values.map((value) => value.id),
413
- // mpsValues = JSON.parse(self.children.meters__pm_schedules?.value || '[]');
414
- // if (outerValueId) {
415
- // ids.push(outerValueId);
416
- // }
417
- // if (!_.isEmpty(ids)) {
418
- // baseParams['conditions[MetersPmSchedules.pm_schedule_id NOT IN]'] = ids;
419
- // }
420
- // return baseParams;
421
- // },
422
- // },
423
- // },
424
- // }
425
-
426
-
427
- if (getBaseParams) {
416
+ if (!_.isUndefined(getBaseParams) && outerValueId) {
417
+ useEffect(() => {
418
+
419
+ // NOTE: This useEffect is so we can dynamically set the TargetRepository's baseParams,
420
+ // based on outerValueId, before it loads.
421
+ // We did this for cases where the Tag field has joinData that's managing a nested Tag field.
422
+ // ... This deals with recursion, so gets "alice in wonderland" quickly!
423
+ // If that inner Tag field has getBaseParams defined on a joinDataConfig field of the outer Tag,
424
+ // then that means it needs to set its baseParams dynamically, based on the value of the outer ValueBox.
425
+
426
+ // For example: in the MetersEditor:
427
+ // {
428
+ // name: 'meters__pm_schedules',
429
+ // mustSaveBeforeEditingJoinData: true,
430
+ // joinDataConfig: {
431
+ // also_resets: {
432
+ // getBaseParams: (values, outerValueId) => {
433
+ // const baseParams = {
434
+ // 'conditions[MetersPmSchedules.meter_id]': meter_id, // limit also_resets to those MetersPmSchedules related to this meter
435
+ // };
436
+ // if (outerValueId) {
437
+ // baseParams['conditions[MetersPmSchedules.id <>]'] = outerValueId; // exclude the ValueBox that was clicked on
438
+ // }
439
+ // return baseParams;
440
+ // },
441
+ // },
442
+ // },
443
+ // }
444
+
428
445
  TargetRepository.setBaseParams(getBaseParams(value, outerValueId));
429
- }
430
- setIsInited(true);
431
- }, [value]);
446
+ setIsInited(true);
447
+
448
+ }, [value]);
449
+ }
432
450
 
433
451
  if (!isInited) {
434
452
  return null;
@@ -16,7 +16,7 @@ export default function ValueBox(props) {
16
16
  text,
17
17
  onView,
18
18
  showEye = false,
19
- onJoin,
19
+ onViewEditJoinData,
20
20
  showJoin = false,
21
21
  onDelete,
22
22
  minimizeForRow = false,
@@ -60,7 +60,7 @@ export default function ValueBox(props) {
60
60
  size: styles.FORM_TAG_VALUEBOX_ICON_SIZE,
61
61
  className: 'text-grey-600',
62
62
  }}
63
- onPress={onJoin}
63
+ onPress={onViewEditJoinData}
64
64
  className={clsx(
65
65
  'ValueBox-joinBtn',
66
66
  'h-full',
@@ -816,6 +816,7 @@ function Form(props) {
816
816
  {...testProps('field-' + name)}
817
817
  name={name}
818
818
  value={value}
819
+ isDirty={isDirty}
819
820
  onChangeValue={(newValue) => {
820
821
  if (newValue === undefined) {
821
822
  newValue = null; // React Hook Form doesn't respond well when setting value to undefined
@@ -133,6 +133,7 @@ function GridComponent(props) {
133
133
  flatListProps = {},
134
134
  onRowPress,
135
135
  onRender,
136
+ onLayout,
136
137
  disableLoadOnRender = false,
137
138
  forceLoadOnRender = false,
138
139
  pullToRefresh = true,
@@ -1092,6 +1093,9 @@ function GridComponent(props) {
1092
1093
  if (!Repository || Repository.isDestroyed) { // This method gets delayed, so it's possible for Repository to have been destroyed. Check for this
1093
1094
  return;
1094
1095
  }
1096
+ if (onLayout) {
1097
+ onLayout(e);
1098
+ }
1095
1099
  if (DEBUG) {
1096
1100
  console.log(`${getMeasurementPhase()}, adjustPageSizeToHeight A`);
1097
1101
  }
@@ -285,6 +285,9 @@ const GridRow = forwardRef((props, ref) => {
285
285
  'py-3',
286
286
  'block',
287
287
  areCellsScrollable ? 'overflow-auto' : 'overflow-hidden',
288
+ '[&::-webkit-scrollbar]:h-2',
289
+ '[&::-webkit-scrollbar-thumb]:bg-gray-400',
290
+ '[&::-webkit-scrollbar-thumb]:rounded-full',
288
291
  colClassName,
289
292
  styles.GRID_CELL_CLASSNAME,
290
293
  styles.GRID_ROW_MAX_HEIGHT_NORMAL,
@@ -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}
@@ -56,8 +56,8 @@ export default function Pagination(props) {
56
56
  let items = [],
57
57
  isDisabled = false;
58
58
  if (showMoreOnly) {
59
- isDisabled = (pageEnd === total);
60
- if (showPagination) {
59
+ if (totalPages > 1 && showPagination) {
60
+ isDisabled = (pageEnd === total);
61
61
  items.push(<Button
62
62
  {...testProps('showMoreBtn')}
63
63
  key="showMoreBtn"
@@ -88,120 +88,122 @@ export default function Pagination(props) {
88
88
  self={self}
89
89
  />);
90
90
  }
91
- isDisabled = page === 1;
92
- if (showPagination) {
93
- items.push(<IconButton
94
- {...testProps('firstPageBtn')}
95
- key="firstPageBtn"
96
- reference="firstPageBtn"
97
- className="Pagination-firstPageBtn"
98
- parent={self}
99
- isDisabled={isDisabled}
100
- icon={AnglesLeft}
101
- _icon={iconProps}
102
- onPress={() => Repository.setPage(1)}
103
- tooltip="First Page"
104
- />);
105
- items.push(<IconButton
106
- {...testProps('prevPageBtn')}
107
- key="prevPageBtn"
108
- reference="prevPageBtn"
109
- className="Pagination-prevPageBtn"
110
- parent={self}
111
- isDisabled={isDisabled}
112
- icon={AngleLeft}
113
- _icon={iconProps}
114
- onPress={() => Repository.prevPage()}
115
- tooltip="Previous Page"
116
- />);
91
+ if (totalPages > 1) {
92
+ isDisabled = page === 1;
93
+ if (showPagination) {
94
+ items.push(<IconButton
95
+ {...testProps('firstPageBtn')}
96
+ key="firstPageBtn"
97
+ reference="firstPageBtn"
98
+ className="Pagination-firstPageBtn"
99
+ parent={self}
100
+ isDisabled={isDisabled}
101
+ icon={AnglesLeft}
102
+ _icon={iconProps}
103
+ onPress={() => Repository.setPage(1)}
104
+ tooltip="First Page"
105
+ />);
106
+ items.push(<IconButton
107
+ {...testProps('prevPageBtn')}
108
+ key="prevPageBtn"
109
+ reference="prevPageBtn"
110
+ className="Pagination-prevPageBtn"
111
+ parent={self}
112
+ isDisabled={isDisabled}
113
+ icon={AngleLeft}
114
+ _icon={iconProps}
115
+ onPress={() => Repository.prevPage()}
116
+ tooltip="Previous Page"
117
+ />);
117
118
 
118
- isDisabled = page === totalPages || totalPages <= 1;
119
- items.push(<IconButton
120
- {...testProps('nextPageBtn')}
121
- key="nextPageBtn"
122
- reference="nextPageBtn"
123
- className="Pagination-nextPageBtn"
124
- parent={self}
125
- isDisabled={isDisabled}
126
- icon={AngleRight}
127
- _icon={iconProps}
128
- onPress={() => Repository.nextPage()}
129
- tooltip="Next Page"
130
- />);
131
- items.push(<IconButton
132
- {...testProps('lastPageBtn')}
133
- key="lastPageBtn"
134
- reference="lastPageBtn"
135
- className="Pagination-lastPageBtn"
136
- parent={self}
137
- isDisabled={isDisabled}
138
- icon={AnglesRight}
139
- _icon={iconProps}
140
- onPress={() => Repository.setPage(totalPages)}
141
- tooltip="Last Page"
142
- />);
143
- if (!minimize) {
144
- items.push(<Text
145
- key="page"
146
- className="Pagination-page mx-1"
147
- >Page</Text>);
148
- items.push(<Input
149
- {...testProps('pageInput')}
150
- key="pageInput"
151
- reference="pageInput"
119
+ isDisabled = page === totalPages || totalPages <= 1;
120
+ items.push(<IconButton
121
+ {...testProps('nextPageBtn')}
122
+ key="nextPageBtn"
123
+ reference="nextPageBtn"
124
+ className="Pagination-nextPageBtn"
152
125
  parent={self}
153
- keyboardType="numeric"
154
- value={page?.toString()}
155
- onChangeValue={(value) => Repository.setPage(value)}
156
- maxValue={totalPages}
157
- isDisabled={totalPages === 1}
158
- className={clsx(
159
- 'Pagination-pageInput',
160
- 'min-w-[40px]',
161
- 'w-[40px]',
162
- 'text-center',
163
- 'bg-grey-100',
164
- )}
165
- textAlignIsCenter={true}
166
- tooltip="Set Page"
167
- tooltipClassName="w-[40px]"
126
+ isDisabled={isDisabled}
127
+ icon={AngleRight}
128
+ _icon={iconProps}
129
+ onPress={() => Repository.nextPage()}
130
+ tooltip="Next Page"
131
+ />);
132
+ items.push(<IconButton
133
+ {...testProps('lastPageBtn')}
134
+ key="lastPageBtn"
135
+ reference="lastPageBtn"
136
+ className="Pagination-lastPageBtn"
137
+ parent={self}
138
+ isDisabled={isDisabled}
139
+ icon={AnglesRight}
140
+ _icon={iconProps}
141
+ onPress={() => Repository.setPage(totalPages)}
142
+ tooltip="Last Page"
143
+ />);
144
+ if (!minimize) {
145
+ items.push(<Text
146
+ key="page"
147
+ className="Pagination-page mx-1"
148
+ >Page</Text>);
149
+ items.push(<Input
150
+ {...testProps('pageInput')}
151
+ key="pageInput"
152
+ reference="pageInput"
153
+ parent={self}
154
+ keyboardType="numeric"
155
+ value={page?.toString()}
156
+ onChangeValue={(value) => Repository.setPage(value)}
157
+ maxValue={totalPages}
158
+ isDisabled={totalPages === 1}
159
+ className={clsx(
160
+ 'Pagination-pageInput',
161
+ 'min-w-[40px]',
162
+ 'w-[40px]',
163
+ 'text-center',
164
+ 'bg-grey-100',
165
+ )}
166
+ textAlignIsCenter={true}
167
+ tooltip="Set Page"
168
+ tooltipClassName="w-[40px]"
169
+ />);
170
+ items.push(<Text
171
+ key="totalPages"
172
+ className={clsx(
173
+ 'Pagination-totalPages',
174
+ 'whitespace-nowrap',
175
+ 'inline-flex',
176
+ 'mx-1',
177
+ )}
178
+ >{`of ${totalPages}`}</Text>);
179
+ }
180
+ }
181
+
182
+ if (showPagination && !minimize && !disablePageSize) {
183
+ items.push(<PageSizeSelect
184
+ {...testProps('pageSize')}
185
+ key="pageSize"
186
+ reference="pageSize"
187
+ parent={self}
188
+ pageSize={pageSize}
189
+ Repository={Repository}
168
190
  />);
191
+ }
192
+ if (showPagination && !minimize) {
193
+ let pageSpan = `${pageStart} – ${pageEnd}`;
194
+ if (pageStart === pageEnd) {
195
+ pageSpan = pageStart;
196
+ }
169
197
  items.push(<Text
170
- key="totalPages"
198
+ key="pageDisplay"
171
199
  className={clsx(
172
- 'Pagination-totalPages',
200
+ 'Pagination-pageDisplay',
173
201
  'whitespace-nowrap',
174
202
  'inline-flex',
175
203
  'mx-1',
176
204
  )}
177
- >{`of ${totalPages}`}</Text>);
178
- }
179
- }
180
-
181
- if (showPagination && !minimize && !disablePageSize) {
182
- items.push(<PageSizeSelect
183
- {...testProps('pageSize')}
184
- key="pageSize"
185
- reference="pageSize"
186
- parent={self}
187
- pageSize={pageSize}
188
- Repository={Repository}
189
- />);
190
- }
191
- if (showPagination && !minimize) {
192
- let pageSpan = `${pageStart} – ${pageEnd}`;
193
- if (pageStart === pageEnd) {
194
- pageSpan = pageStart;
205
+ >{`Displaying ${pageSpan} of ${total}`}</Text>);
195
206
  }
196
- items.push(<Text
197
- key="pageDisplay"
198
- className={clsx(
199
- 'Pagination-pageDisplay',
200
- 'whitespace-nowrap',
201
- 'inline-flex',
202
- 'mx-1',
203
- )}
204
- >{`Displaying ${pageSpan} of ${total}`}</Text>);
205
207
  }
206
208
  }
207
209
  return <HStack
@@ -1,6 +1,7 @@
1
1
  import { useMemo, useEffect, } from 'react';
2
2
  import {
3
3
  Box,
4
+ BoxNative,
4
5
  HStackNative,
5
6
  Icon,
6
7
  Spinner,
@@ -185,7 +186,7 @@ export default function TreeNode(props) {
185
186
  styles.TREE_NODE_EXPAND_BTN_CLASSNAME,
186
187
  )}
187
188
  /> :
188
- <Box
189
+ <BoxNative
189
190
  {...testProps('spacer')}
190
191
  className={clsx(
191
192
  'TreeNode-spacer',
@@ -161,11 +161,8 @@ function Viewer(props) {
161
161
  viewerTypeProps.isViewOnly = true;
162
162
  viewerTypeProps.SourceRepository = Repository;
163
163
  }
164
- if (type?.match(/(GridEditor)$/)) {
165
- viewerTypeProps.disableAdd = true;
166
- viewerTypeProps.disableEdit = true;
167
- viewerTypeProps.disableDelete = true;
168
- viewerTypeProps.disableDuplicate = true;
164
+ if (type?.match(/(Grid|GridEditor)$/)) {
165
+ viewerTypeProps.canEditorViewOnly = true;
169
166
  }
170
167
  const Element = getComponentFromType(type);
171
168