@onehat/ui 0.3.32 → 0.3.34

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.3.32",
3
+ "version": "0.3.34",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -1,4 +1,4 @@
1
- import { useEffect, useState, isValidElement, } from 'react';
1
+ import { useEffect, useState, useRef, isValidElement, } from 'react';
2
2
  import {
3
3
  Box,
4
4
  Column,
@@ -77,6 +77,7 @@ function Form(props) {
77
77
  // sizing of outer container
78
78
  h,
79
79
  maxHeight,
80
+ minHeight = 0,
80
81
  w,
81
82
  maxWidth,
82
83
  flex,
@@ -105,6 +106,7 @@ function Form(props) {
105
106
  // withAlert
106
107
  alert,
107
108
  } = props,
109
+ formRef = useRef(),
108
110
  styles = UiGlobals.styles,
109
111
  record = props.record?.length === 1 ? props.record[0] : props.record,
110
112
  isMultiple = _.isArray(record),
@@ -564,6 +566,10 @@ function Form(props) {
564
566
  if (!_.isNil(editorStateRef)) {
565
567
  editorStateRef.current = formState; // Update state so HOC can know what's going on
566
568
  }
569
+
570
+ if (self) {
571
+ self.ref = formRef;
572
+ }
567
573
 
568
574
  const sizeProps = {};
569
575
  if (!flex && !h && !w) {
@@ -617,7 +623,7 @@ function Form(props) {
617
623
  } else {
618
624
  formComponents = buildFromItems();
619
625
  const formAncillaryComponents = buildAncillary();
620
- editor = <ScrollView _web={{ height: 1 }} width="100%" pb={1}>
626
+ editor = <ScrollView _web={{ minHeight, }} width="100%" pb={1}>
621
627
  <Column p={4}>{formComponents}</Column>
622
628
  <Column m={2} pt={4}>{formAncillaryComponents}</Column>
623
629
  </ScrollView>;
@@ -654,7 +660,7 @@ function Form(props) {
654
660
 
655
661
  const additionalButtons = buildAdditionalButtons(additionalEditButtons);
656
662
 
657
- return <Column {...sizeProps} onLayout={onLayout}>
663
+ return <Column {...sizeProps} onLayout={onLayout} ref={formRef}>
658
664
 
659
665
  <Row px={4} pt={4} alignItems="center" justifyContent="flex-end">
660
666
  {isSingle && editorMode === EDITOR_MODE__EDIT && onBack &&
@@ -774,6 +774,9 @@ function GridComponent(props) {
774
774
 
775
775
  }, [selectorId, selectorSelected]);
776
776
 
777
+ if (self) {
778
+ self.ref = gridRef;
779
+ }
777
780
 
778
781
  isAddingRef.current = isAdding;
779
782
 
@@ -10,6 +10,7 @@ export default function withComponent(WrappedComponent) {
10
10
  const {
11
11
  // self: parent,
12
12
  parent,
13
+ componentMethods,
13
14
  ...propsToPass
14
15
  } = props,
15
16
  { reference } = props,
@@ -17,6 +18,13 @@ export default function withComponent(WrappedComponent) {
17
18
  selfRef = useRef({
18
19
  parent,
19
20
  reference,
21
+ hasChild: (childRef) => {
22
+ const {
23
+ reference,
24
+ } = childRef,
25
+ found = _.find(childrenRef.current, (ref, ix) => ix === reference);
26
+ return !!found;
27
+ },
20
28
  registerChild: (childRef) => {
21
29
  const {
22
30
  reference,
@@ -38,7 +46,12 @@ export default function withComponent(WrappedComponent) {
38
46
  });
39
47
 
40
48
  useEffect(() => {
41
- if (parent && reference) {
49
+ if (componentMethods) {
50
+ _.each(componentMethods, (method, name) => {
51
+ selfRef.current[name] = method;
52
+ });
53
+ }
54
+ if (parent && reference && !parent.hasChild(selfRef.current)) {
42
55
  parent.registerChild(selfRef.current);
43
56
  }
44
57
  return () => {
@@ -28,6 +28,9 @@ export default function withData(WrappedComponent) {
28
28
  displayField = 'value',
29
29
  idIx,
30
30
  displayIx,
31
+
32
+ // withComponent
33
+ self,
31
34
  } = props,
32
35
  propsToPass = _.omit(props, ['model']), // passing 'model' would mess things up if withData gets called twice (e.g. withData(...withData(...)) ), as we'd be trying to recreate Repository twice
33
36
  localIdIx = idIx || (fields && idField ? fields.indexOf(idField) : null),
@@ -64,6 +67,9 @@ export default function withData(WrappedComponent) {
64
67
  if (setRepository) { // pass it on up to higher components
65
68
  setRepository(Repository);
66
69
  }
70
+ if (self) {
71
+ self.repository = Repository;
72
+ }
67
73
  setIsReady(true);
68
74
  })();
69
75
 
@@ -22,6 +22,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
22
22
  disableDelete = false,
23
23
  disableDuplicate = false,
24
24
  disableView = false,
25
+ useRemoteDuplicate = false, // call specific copyToNew function on server, rather than simple duplicate on client
25
26
  getRecordIdentifier = (selection) => {
26
27
  if (selection.length > 1) {
27
28
  return 'records?';
@@ -224,6 +225,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
224
225
  if (selection.length !== 1) {
225
226
  return;
226
227
  }
228
+ if (useRemoteDuplicate) {
229
+ return onRemoteDuplicate();
230
+ }
227
231
  const
228
232
  entity = selection[0],
229
233
  idProperty = Repository.getSchema().model.idProperty,
@@ -233,6 +237,40 @@ export default function withEditor(WrappedComponent, isTree = false) {
233
237
  setEditorMode(EDITOR_MODE__EDIT);
234
238
  setIsEditorShown(true);
235
239
  },
240
+ onRemoteDuplicate = async () => {
241
+
242
+ // Call /duplicate on server
243
+ const
244
+ Model = Repository.getSchema().name,
245
+ entity = selection[0],
246
+ id = entity.id;
247
+ const result = await Repository._send('POST', Model + '/duplicate', { id });
248
+ const {
249
+ root,
250
+ success,
251
+ total,
252
+ message
253
+ } = Repository._processServerResponse(result);
254
+
255
+ if (!success) {
256
+ throw Error(message);
257
+ }
258
+
259
+ const duplicateId = root.id;
260
+
261
+ // Filter the grid with only the duplicate's ID, and open it for editing.
262
+ self.filterById(duplicateId, () => { // because of the way useFilters is made, we have to use a callback, not await a Promise.
263
+
264
+ // Select the only node
265
+ const duplicateEntity = Repository.getById(duplicateId);
266
+ self.setSelection([duplicateEntity]);
267
+
268
+ // edit it
269
+ onEdit();
270
+
271
+ });
272
+
273
+ },
236
274
  onEditorSave = async (data, e) => {
237
275
  const
238
276
  what = record || selection,
@@ -343,12 +381,12 @@ export default function withEditor(WrappedComponent, isTree = false) {
343
381
  }, [selection]);
344
382
 
345
383
  if (self) {
346
- self.onAdd = onAdd;
347
- self.onEdit = onEdit;
348
- self.onDelete = onDelete;
349
- self.onMoveChildren = onMoveChildren;
350
- self.onDeleteChildren = onDeleteChildren;
351
- self.onDuplicate = onDuplicate;
384
+ self.add = onAdd;
385
+ self.edit = onEdit;
386
+ self.delete = onDelete;
387
+ self.moveChildren = onMoveChildren;
388
+ self.deleteChildren = onDeleteChildren;
389
+ self.duplicate = onDuplicate;
352
390
  }
353
391
 
354
392
  if (lastSelection !== selection) {
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useId, } from 'react';
1
+ import { useState, useEffect, useId, useRef, } from 'react';
2
2
  import {
3
3
  Column,
4
4
  Modal,
@@ -46,6 +46,9 @@ export default function withFilters(WrappedComponent) {
46
46
 
47
47
  // withData
48
48
  Repository,
49
+
50
+ // withComponent
51
+ self,
49
52
  } = props;
50
53
 
51
54
  let modal = null,
@@ -59,7 +62,7 @@ export default function withFilters(WrappedComponent) {
59
62
  defaultFilters: modelDefaultFilters,
60
63
  ancillaryFilters: modelAncillaryFilters,
61
64
  } = Repository.getSchema().model,
62
- id = props.id || useId(),
65
+ id = props.id || props.reference || useId(),
63
66
 
64
67
  // determine the starting filters
65
68
  startingFilters = !_.isEmpty(customFilters) ? customFilters : // custom filters override component filters
@@ -124,6 +127,7 @@ export default function withFilters(WrappedComponent) {
124
127
  }
125
128
 
126
129
  const
130
+ filterCallbackRef = useRef(),
127
131
  [filters, setFiltersRaw] = useState(formattedStartingFilters), // array of formatted filters
128
132
  [slots, setSlots] = useState(startingSlots), // array of field names user is currently filtering on; blank slots have a null entry in array
129
133
  [modalFilters, setModalFilters] = useState([]),
@@ -230,6 +234,21 @@ export default function withFilters(WrappedComponent) {
230
234
  }
231
235
  return inArray(filterType, ['NumberRange', 'DateRange']);
232
236
  },
237
+ filterById = (id, cb) => {
238
+ onClearFilters();
239
+ filterCallbackRef.current = cb; // store the callback, so we can call it the next time this HOC renders with new filters
240
+ const newFilters = _.clone(filters);
241
+ _.remove(newFilters, (filter) => {
242
+ return filter.field === 'q';
243
+ });
244
+ newFilters.unshift({
245
+ field: 'q',
246
+ title: 'Search all text fields',
247
+ type: 'Input',
248
+ value: 'id:' + id,
249
+ });
250
+ setFilters(newFilters, false, false);
251
+ },
233
252
  renderFilters = () => {
234
253
  const
235
254
  filterProps = {
@@ -347,7 +366,7 @@ export default function withFilters(WrappedComponent) {
347
366
  } else {
348
367
  const
349
368
  isAncillary = type === FILTER_TYPE_ANCILLARY,
350
- filterName = (isAncillary ? 'ancillary___' : '') + field;
369
+ filterName = (isAncillary ? 'ancillary___' : '') + field;
351
370
  newFilterNames.push(filterName);
352
371
  newRepoFilters.push({ name: filterName, value, });
353
372
  }
@@ -362,12 +381,17 @@ export default function withFilters(WrappedComponent) {
362
381
  setPreviousFilterNames(newFilterNames);
363
382
  }
364
383
 
365
- Repository.filter(newRepoFilters, null, false); // false so other filters remain
366
-
367
384
  if (searchAllText && Repository.searchAncillary && !Repository.hasBaseParam('searchAncillary')) {
368
385
  Repository.setBaseParam('searchAncillary', true);
369
386
  }
370
387
 
388
+ await Repository.filter(newRepoFilters, null, false); // false so other filters remain
389
+
390
+ if (filterCallbackRef.current) {
391
+ filterCallbackRef.current(); // call the callback
392
+ filterCallbackRef.current = null; // clear the callback
393
+ }
394
+
371
395
  if (!isReady) {
372
396
  setIsReady(true);
373
397
  }
@@ -378,6 +402,11 @@ export default function withFilters(WrappedComponent) {
378
402
  return null;
379
403
  }
380
404
 
405
+ if (self) {
406
+ self.filterById = filterById;
407
+ self.setFilters = setFilters;
408
+ }
409
+
381
410
  const
382
411
  renderedFilters = renderFilters(),
383
412
  hasFilters = !!renderedFilters.length;
@@ -499,6 +528,7 @@ export default function withFilters(WrappedComponent) {
499
528
  editorType={EDITOR_TYPE__PLAIN}
500
529
  flex={1}
501
530
  startingValues={formStartingValues}
531
+ minHeight={minHeight}
502
532
  items={[
503
533
  {
504
534
  type: 'Column',
@@ -158,8 +158,10 @@ export default function withPdfButton(WrappedComponent) {
158
158
  setIsModalShown(true);
159
159
  },
160
160
  };
161
- additionalEditButtons.push(button);
162
- if (additionalEditButtons !== additionalViewButtons) { // Ensure they're NOT the same object, otherwise this would be adding it twice!
161
+ if (!_.find(additionalEditButtons, btn => button.key === btn.key)) {
162
+ additionalEditButtons.push(button);
163
+ }
164
+ if (!_.find(additionalViewButtons, btn => button.key === btn.key)) {
163
165
  additionalViewButtons.push(button);
164
166
  }
165
167
 
@@ -27,6 +27,9 @@ export default function withSelection(WrappedComponent) {
27
27
  autoSelectFirstItem = false,
28
28
  fireEvent,
29
29
 
30
+ // withComponent
31
+ self,
32
+
30
33
  // withValue
31
34
  value,
32
35
  setValue,
@@ -309,6 +312,19 @@ export default function withSelection(WrappedComponent) {
309
312
 
310
313
  }, []);
311
314
 
315
+ if (self) {
316
+ self.setSelection = setSelection;
317
+ self.selectNext = selectNext;
318
+ self.selectPrev = selectPrev;
319
+ self.addToSelection = addToSelection;
320
+ self.removeFromSelection = removeFromSelection;
321
+ self.deselectAll = deselectAll;
322
+ self.selectRangeTo = selectRangeTo;
323
+ self.isInSelection = isInSelection;
324
+ self.getIdsFromLocalSelection = getIdsFromLocalSelection;
325
+ self.getDisplayValuesFromSelection = getDisplayValuesFromLocalSelection;
326
+ }
327
+
312
328
  if (usesWithValue) {
313
329
  useEffect(() => {
314
330
  if (!isReady) {
@@ -1,3 +1,4 @@
1
+ import { useRef, } from 'react';
1
2
  import {
2
3
  Column,
3
4
  Icon,
@@ -45,6 +46,7 @@ function Viewer(props) {
45
46
  selectorSelected,
46
47
 
47
48
  } = props,
49
+ scrollViewRef = useRef(),
48
50
  isMultiple = _.isArray(record),
49
51
  isSideEditor = editorType === EDITOR_TYPE__SIDE,
50
52
  styles = UiGlobals.styles,
@@ -205,13 +207,17 @@ function Viewer(props) {
205
207
  return additionalButtons;
206
208
  };
207
209
 
210
+ if (self) {
211
+ self.ref = scrollViewRef;
212
+ }
213
+
208
214
  const
209
215
  showDeleteBtn = onDelete && viewerCanDelete,
210
216
  showCloseBtn = !isSideEditor,
211
217
  additionalButtons = buildAdditionalButtons();
212
218
 
213
219
  return <Column flex={flex} {...props}>
214
- <ScrollView width="100%" _web={{ height: 1 }}>
220
+ <ScrollView width="100%" _web={{ height: 1 }} ref={scrollViewRef}>
215
221
  <Column p={4}>
216
222
  {onEditMode && <Row mb={4} justifyContent="flex-end">
217
223
  <Button