@onehat/ui 0.3.13 → 0.3.18

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.13",
3
+ "version": "0.3.18",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -33,9 +33,13 @@ export default function Editor(props) {
33
33
 
34
34
  // Repository?.isRemotePhantomMode && selection.length === 1 &&
35
35
  if (editorMode === EDITOR_MODE__VIEW) {
36
+ const record = selection[0];
37
+ if (record.isDestroyed) {
38
+ return null;
39
+ }
36
40
  return <Viewer
37
41
  {...props}
38
- record={selection[0]}
42
+ record={record}
39
43
  onEditMode={isEditorViewOnly ? null : onEditMode}
40
44
  onClose={onClose}
41
45
  onDelete={onDelete}
@@ -37,19 +37,23 @@ export default function Viewer(props) {
37
37
  onClose,
38
38
  onDelete,
39
39
  } = props,
40
+ isMultiple = _.isArray(record),
40
41
  isSideEditor = editorType === EDITOR_TYPE__SIDE,
41
42
  styles = UiGlobals.styles,
42
43
  flex = props.flex || 1,
43
44
  buildAncillary = () => {
44
- let components = [];
45
+ const components = [];
45
46
  if (ancillaryItems.length) {
46
- components = _.map(ancillaryItems, (item, ix) => {
47
+ _.each(ancillaryItems, (item, ix) => {
47
48
  let {
48
49
  type,
49
50
  title = null,
50
51
  selectorId = null,
51
52
  ...propsToPass
52
53
  } = item;
54
+ if (isMultiple && type !== 'Attachments') {
55
+ return;
56
+ }
53
57
  if (!propsToPass.h) {
54
58
  propsToPass.h = 400;
55
59
  }
@@ -61,6 +65,7 @@ export default function Viewer(props) {
61
65
  flex={1}
62
66
  h={350}
63
67
  canEditorViewOnly={true}
68
+ uniqueRepository={true}
64
69
  {...propsToPass}
65
70
  />;
66
71
  if (title) {
@@ -69,7 +74,7 @@ export default function Viewer(props) {
69
74
  fontWeight="bold"
70
75
  >{title}</Text>;
71
76
  }
72
- return <Column key={'ancillary-' + ix} my={5}>{title}{element}</Column>;
77
+ components.push(<Column key={'ancillary-' + ix} my={5}>{title}{element}</Column>);
73
78
  });
74
79
  }
75
80
  return components;
@@ -34,7 +34,6 @@ export function ComboComponent(props) {
34
34
  tooltip = null,
35
35
  menuMinWidth = 150,
36
36
  disableDirectEntry = false,
37
- disablePagination = false,
38
37
  hideMenuOnSelection = true,
39
38
  _input = {},
40
39
  isEditor = false,
@@ -55,7 +54,7 @@ export function ComboComponent(props) {
55
54
  selectionMode,
56
55
  selectNext,
57
56
  selectPrev,
58
- getDisplayFromSelection,
57
+ getDisplayValuesFromSelection,
59
58
 
60
59
  tooltipPlacement = 'bottom',
61
60
  } = props,
@@ -318,7 +317,7 @@ export function ComboComponent(props) {
318
317
  if (found) {
319
318
  const
320
319
  newSelection = [found],
321
- newTextValue = getDisplayFromSelection(newSelection);
320
+ newTextValue = getDisplayValuesFromSelection(newSelection);
322
321
 
323
322
  setTextValue(newTextValue);
324
323
  setSelection(newSelection);
@@ -351,7 +350,7 @@ export function ComboComponent(props) {
351
350
  }
352
351
 
353
352
  // Adjust text input to match selection
354
- let localTextValue = getDisplayFromSelection(selection);
353
+ let localTextValue = getDisplayValuesFromSelection(selection);
355
354
  if (!_.isEqual(localTextValue, textValue)) {
356
355
  setTextValue(localTextValue);
357
356
  }
@@ -510,10 +509,11 @@ export function ComboComponent(props) {
510
509
  w: '100%',
511
510
  };
512
511
  }}
512
+ allowToggleSelection={true}
513
+ disableAdjustingPageSizeToHeight={true}
513
514
  {...props}
514
515
  h={styles.FORM_COMBO_MENU_HEIGHT + 'px'}
515
516
  disablePresetButtons={!isEditor}
516
- disablePagination={disablePagination}
517
517
  setSelection={(selection) => {
518
518
  // Decorator fn to add local functionality
519
519
  // Close the menu when row is selected on grid
@@ -1,12 +1,23 @@
1
1
  import {
2
2
  SELECTION_MODE_MULTI,
3
3
  } from '../../../../Constants/Selection.js';
4
- import Combo from './Combo.js';
4
+ import Combo, { ComboEditor } from './Combo.js';
5
5
 
6
- export default function Tag(props) {
7
- return <Combo
8
- selectionMode={SELECTION_MODE_MULTI}
9
- disableDirectEntry={true}
10
- {...props}
11
- />;
6
+ function withAdditionalProps(WrappedComponent) {
7
+ return (props) => {
8
+ return <WrappedComponent
9
+ selectionMode={SELECTION_MODE_MULTI}
10
+ valueIsAlwaysArray={true}
11
+ valueAsIdAndText={true}
12
+ valueAsStringifiedJson={true}
13
+ disableDirectEntry={true}
14
+ pageSize={500}
15
+ {...props}
16
+ />;
17
+ };
12
18
  }
19
+
20
+ const Tag = withAdditionalProps(Combo);
21
+ export const TagEditor = withAdditionalProps(ComboEditor);
22
+
23
+ export default Tag;
@@ -4,7 +4,6 @@ import {
4
4
  } from 'native-base';
5
5
  import UiGlobals from '../../../UiGlobals.js';
6
6
  import withTooltip from '../../Hoc/withTooltip.js';
7
- import withValue from '../../Hoc/withValue.js';
8
7
 
9
8
  const
10
9
  TextElement = (props) => {
@@ -21,7 +20,7 @@ const
21
20
  {...props}
22
21
  >{props.value}</Text>;
23
22
  },
24
- TextField = withValue(TextElement);
23
+ TextField = TextElement; // NOT using withValue on Text element, as this element is simply for display purposes!
25
24
 
26
25
  // Tooltip needs us to forwardRef
27
26
  export default withTooltip(React.forwardRef((props, ref) => {
@@ -82,6 +82,7 @@ function Form(props) {
82
82
 
83
83
  // withEditor
84
84
  isEditorViewOnly = false,
85
+ isSaving = false,
85
86
  editorMode,
86
87
  onCancel,
87
88
  onSave,
@@ -163,6 +164,7 @@ function Form(props) {
163
164
  renderer,
164
165
  w,
165
166
  flex,
167
+ useSelectorId = false,
166
168
  } = config;
167
169
 
168
170
  if (!isEditable) {
@@ -214,6 +216,11 @@ function Form(props) {
214
216
  }
215
217
  const Element = getComponentFromType(editor);
216
218
 
219
+ if (useSelectorId) {
220
+ editorProps.selectorId = selectorId;
221
+ editorProps.selectorSelected = editorProps;
222
+ }
223
+
217
224
  let element = <Element
218
225
  name={name}
219
226
  value={value}
@@ -224,8 +231,6 @@ function Form(props) {
224
231
  }
225
232
  }}
226
233
  onBlur={onBlur}
227
- selectorId={selectorId}
228
- selectorSelected={selectorSelected}
229
234
  flex={1}
230
235
  {...editorProps}
231
236
  // {...defaults}
@@ -262,6 +267,7 @@ function Form(props) {
262
267
  label,
263
268
  items,
264
269
  onChange: onEditorChange,
270
+ useSelectorId = false,
265
271
  ...propsToPass
266
272
  } = item;
267
273
  let editorTypeProps = {};
@@ -366,6 +372,11 @@ function Form(props) {
366
372
  if (isValidElement(Element)) {
367
373
  throw new Error('Should not yet be valid React element. Did you use <Element> instead of () => <Element> when defining it?')
368
374
  }
375
+
376
+ if (useSelectorId) {
377
+ editorTypeProps.selectorId = selectorId;
378
+ editorTypeProps.selectorSelected = editorProps;
379
+ }
369
380
  let element = <Element
370
381
  name={name}
371
382
  value={value}
@@ -376,8 +387,6 @@ function Form(props) {
376
387
  }
377
388
  }}
378
389
  onBlur={onBlur}
379
- selectorId={selectorId}
380
- selectorSelected={selectorSelected}
381
390
  flex={1}
382
391
  {...defaults}
383
392
  {...propsToPass}
@@ -412,15 +421,18 @@ function Form(props) {
412
421
  />;
413
422
  },
414
423
  buildAncillary = () => {
415
- let components = [];
424
+ const components = [];
416
425
  if (ancillaryItems.length) {
417
- components = _.map(ancillaryItems, (item, ix) => {
426
+ _.each(ancillaryItems, (item, ix) => {
418
427
  let {
419
428
  type,
420
429
  title = null,
421
430
  selectorId,
422
431
  ...propsToPass
423
432
  } = item;
433
+ if (isMultiple && type !== 'Attachments') {
434
+ return;
435
+ }
424
436
  if (!propsToPass.h) {
425
437
  propsToPass.h = 400;
426
438
  }
@@ -430,6 +442,7 @@ function Form(props) {
430
442
  selectorId={selectorId}
431
443
  selectorSelected={selectorSelected || record}
432
444
  flex={1}
445
+ uniqueRepository={true}
433
446
  {...propsToPass}
434
447
  />;
435
448
  if (title) {
@@ -438,7 +451,7 @@ function Form(props) {
438
451
  fontWeight="bold"
439
452
  >{title}</Text>;
440
453
  }
441
- return <Column key={'ancillary-' + ix} mx={2} my={5}>{title}{element}</Column>;
454
+ components.push(<Column key={'ancillary-' + ix} mx={2} my={5}>{title}{element}</Column>);
442
455
  });
443
456
  }
444
457
  return components;
@@ -506,6 +519,13 @@ function Form(props) {
506
519
  sizeProps.maxHeight = maxHeight;
507
520
  }
508
521
 
522
+ const savingProps = {};
523
+ if (isSaving) {
524
+ savingProps.borderTopWidth = 2;
525
+ savingProps.borderTopColor = '#f00';
526
+ }
527
+
528
+
509
529
  let formComponents,
510
530
  editor;
511
531
  if (editorType === EDITOR_TYPE__INLINE) {
@@ -589,8 +609,8 @@ function Form(props) {
589
609
 
590
610
  {editor}
591
611
 
592
- <Footer justifyContent="flex-end" {...footerProps}>
593
- {onDelete && editorMode === EDITOR_MODE__EDIT &&
612
+ <Footer justifyContent="flex-end" {...footerProps} {...savingProps}>
613
+ {onDelete && editorMode === EDITOR_MODE__EDIT && isSingle &&
594
614
  <Row flex={1} justifyContent="flex-start">
595
615
  <Button
596
616
  key="deleteBtn"
@@ -614,7 +634,7 @@ function Form(props) {
614
634
  }}
615
635
  icon={<Rotate color="#fff" />}
616
636
  />}
617
- {!isEditorViewOnly && onCancel && <Button
637
+ {!isEditorViewOnly && isSingle && onCancel && <Button
618
638
  key="cancelBtn"
619
639
  variant="ghost"
620
640
  onPress={onCancel}
@@ -86,7 +86,7 @@ function GridComponent(props) {
86
86
  canColumnsReorder = true,
87
87
  canColumnsResize = true,
88
88
  canRowsReorder = false,
89
- allowToggleSelection = true, // i.e. single click with no shift key toggles the selection of the item clicked on
89
+ allowToggleSelection = false, // i.e. single click with no shift key toggles the selection of the item clicked on
90
90
  disableBottomToolbar = false,
91
91
  disablePagination = false,
92
92
  bottomToolbar = 'pagination',
@@ -164,6 +164,10 @@ function GridComponent(props) {
164
164
  shiftKey,
165
165
  metaKey,
166
166
  } = e;
167
+ let allowToggle = allowToggleSelection;
168
+ if (metaKey) {
169
+ allowToggle = true;
170
+ }
167
171
 
168
172
  if (selectionMode === SELECTION_MODE_MULTI) {
169
173
  if (shiftKey) {
@@ -172,28 +176,18 @@ function GridComponent(props) {
172
176
  } else {
173
177
  selectRangeTo(item);
174
178
  }
175
- } else if (metaKey) {
176
- if (isInSelection(item)) {
177
- // Already selected
178
- if (allowToggleSelection) {
179
- removeFromSelection(item);
180
- } else {
181
- // Do nothing.
182
- }
183
- } else {
184
- addToSelection(item);
185
- }
186
179
  } else {
187
- if (isInSelection(item)) {
188
- // Already selected
189
- if (allowToggleSelection) {
180
+ if (allowToggle) {
181
+ if (isInSelection(item)) {
190
182
  removeFromSelection(item);
191
183
  } else {
192
- // Do nothing.
184
+ addToSelection(item);
193
185
  }
194
186
  } else {
195
- // select just this one
196
- setSelection([item]);
187
+ if (!isInSelection(item)) {
188
+ // select just this one
189
+ setSelection([item]);
190
+ }
197
191
  }
198
192
  }
199
193
  } else {
@@ -201,7 +195,7 @@ function GridComponent(props) {
201
195
  let newSelection = selection;
202
196
  if (isInSelection(item)) {
203
197
  // Already selected
204
- if (allowToggleSelection) {
198
+ if (allowToggle) {
205
199
  // Create empty selection
206
200
  newSelection = [];
207
201
  } else {
@@ -826,7 +820,7 @@ function GridComponent(props) {
826
820
  deselectAll();
827
821
  }
828
822
  }}>
829
- {!entities.length ? <NoRecordsFound text={noneFoundText} onRefresh={onRefresh} /> :
823
+ {!entities?.length ? <NoRecordsFound text={noneFoundText} onRefresh={onRefresh} /> :
830
824
  <FlatList
831
825
  ref={gridRef}
832
826
  // ListHeaderComponent={listHeaderComponent}
@@ -285,7 +285,7 @@ export default function GridHeaderRow(props) {
285
285
 
286
286
  // These header Components should match the columns exactly
287
287
  // so we can drag/drop them to control the columns.
288
- const headerColumns = _.map(localColumnsConfig, (config, ix) => {
288
+ const headerColumns = _.map(localColumnsConfig, (config, ix, all) => {
289
289
  let {
290
290
  columnId,
291
291
  fieldName,
@@ -305,15 +305,21 @@ export default function GridHeaderRow(props) {
305
305
  borderRightColor: '#fff',
306
306
  }
307
307
 
308
- if (w) {
309
- propsToPass.w = w;
310
- } else if (flex) {
311
- propsToPass.flex = flex;
312
- propsToPass.minWidth = 100;
313
- } else if (localColumnsConfig.length === 1) {
314
- // Only one column and flex is not set
315
- propsToPass.flex = 1;
316
- if (!header) {
308
+ if (all.length === 1) {
309
+ propsToPass.w = '100%';
310
+ isReorderable = false;
311
+ isResizable = false;
312
+ } else {
313
+ if (w) {
314
+ propsToPass.w = w;
315
+ } else if (flex) {
316
+ propsToPass.flex = flex;
317
+ propsToPass.minWidth = 100;
318
+ } else if (localColumnsConfig.length === 1) {
319
+ // Only one column and flex is not set
320
+ propsToPass.flex = 1;
321
+ if (!header) {
322
+ }
317
323
  }
318
324
  }
319
325
 
@@ -32,15 +32,19 @@ export default function GridRow(props) {
32
32
  return useMemo(() => {
33
33
  const renderColumns = (item) => {
34
34
  if (_.isArray(columnsConfig)) {
35
- return _.map(columnsConfig, (config, key) => {
35
+ return _.map(columnsConfig, (config, key, all) => {
36
36
  const propsToPass = columnProps[key] || {};
37
- if (config.w) {
38
- propsToPass.w = config.w;
39
- } else if (config.flex) {
40
- propsToPass.flex = config.flex;
41
- propsToPass.minWidth = 100;
37
+ if (all.length === 1) {
38
+ propsToPass.w = '100%';
42
39
  } else {
43
- propsToPass.flex = 1;
40
+ if (config.w) {
41
+ propsToPass.w = config.w;
42
+ } else if (config.flex) {
43
+ propsToPass.flex = config.flex;
44
+ propsToPass.minWidth = 100;
45
+ } else {
46
+ propsToPass.flex = 1;
47
+ }
44
48
  }
45
49
  propsToPass.p = 1;
46
50
  propsToPass.justifyContent = 'center';
@@ -50,6 +50,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
50
50
  editorStateRef = useRef(),
51
51
  [currentRecord, setCurrentRecord] = useState(null),
52
52
  [isAdding, setIsAdding] = useState(false),
53
+ [isSaving, setIsSaving] = useState(false),
53
54
  [isEditorShown, setIsEditorShown] = useState(false),
54
55
  [isEditorViewOnly, setIsEditorViewOnly] = useState(canEditorViewOnly), // current state of whether editor is in view-only mode
55
56
  [lastSelection, setLastSelection] = useState(),
@@ -107,7 +108,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
107
108
  addValues = Repository.unmapData(addValues);
108
109
 
109
110
  setIsAdding(true);
111
+ setIsSaving(true);
110
112
  const entity = await Repository.add(addValues, false, true);
113
+ setIsSaving(false);
111
114
  setSelection([entity]);
112
115
  setIsEditorViewOnly(false);
113
116
  setEditorMode(EDITOR_MODE__ADD);
@@ -185,6 +188,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
185
188
  if (getListeners().onAfterDelete) {
186
189
  await getListeners().onAfterDelete(selection);
187
190
  }
191
+ setSelection([]);
188
192
  if (cb) {
189
193
  cb();
190
194
  }
@@ -246,10 +250,13 @@ export default function withEditor(WrappedComponent, isTree = false) {
246
250
  await getListeners().onBeforeEditSave(what);
247
251
  }
248
252
 
253
+ setIsSaving(true);
249
254
  await Repository.save();
255
+ setIsSaving(false);
250
256
 
251
257
  setIsAdding(false);
252
- setIsEditorShown(false);
258
+ setEditorMode(EDITOR_MODE__EDIT);
259
+ // setIsEditorShown(false);
253
260
 
254
261
  if (getListeners().onAfterEdit) {
255
262
  await getListeners().onAfterEdit(what);
@@ -339,6 +346,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
339
346
  isEditorShown={isEditorShown}
340
347
  isEditorViewOnly={isEditorViewOnly}
341
348
  isAdding={isAdding}
349
+ isSaving={isSaving}
342
350
  editorMode={editorMode}
343
351
  onEditMode={onEditMode}
344
352
  onViewMode={onViewMode}
@@ -59,7 +59,7 @@ export default function withFilters(WrappedComponent) {
59
59
  defaultFilters: modelDefaultFilters,
60
60
  ancillaryFilters: modelAncillaryFilters,
61
61
  } = Repository.getSchema().model,
62
- id = useId(),
62
+ id = props.id || useId(),
63
63
 
64
64
  // determine the starting filters
65
65
  startingFilters = !_.isEmpty(customFilters) ? customFilters : // custom filters override component filters
@@ -235,6 +235,9 @@ export default function withFilters(WrappedComponent) {
235
235
  const
236
236
  filterProps = {
237
237
  mx: 1,
238
+ disableAdjustingPageSizeToHeight: true,
239
+ pageSize: 20,
240
+ uniqueRepository: true,
238
241
  },
239
242
  filterElements = [];
240
243
  _.each(filters, (filter, ix) => {
@@ -38,9 +38,14 @@ export default function withSelection(WrappedComponent) {
38
38
  displayIx,
39
39
  } = props,
40
40
  usesWithValue = !!setValue,
41
- [localSelection, setLocalSelection] = useState(selection || defaultSelection || []),
41
+ initialSelection = selection || defaultSelection || [],
42
+ [localSelection, setLocalSelection] = useState(initialSelection),
42
43
  [isReady, setIsReady] = useState(selection || false), // if selection is already defined, or value is not null and we don't need to load repository, it's ready
43
44
  setSelection = (selection) => {
45
+ if (_.isEqual(selection, localSelection)) {
46
+ return;
47
+ }
48
+
44
49
  setLocalSelection(selection);
45
50
  if (onChangeSelection) {
46
51
  onChangeSelection(selection);
@@ -175,7 +180,7 @@ export default function withSelection(WrappedComponent) {
175
180
  });
176
181
  return found;
177
182
  },
178
- getIdFromSelection = () => {
183
+ getIdsFromLocalSelection = () => {
179
184
  if (!localSelection[0]) {
180
185
  return null;
181
186
  }
@@ -190,7 +195,7 @@ export default function withSelection(WrappedComponent) {
190
195
  }
191
196
  return values;
192
197
  },
193
- getDisplayFromSelection = (selection) => {
198
+ getDisplayValuesFromLocalSelection = (selection) => {
194
199
  if (!selection[0]) {
195
200
  return '';
196
201
  }
@@ -203,18 +208,17 @@ export default function withSelection(WrappedComponent) {
203
208
  })
204
209
  .join(', ');
205
210
  },
206
- conformValueToSelection = () => {
211
+ conformValueToLocalSelection = () => {
207
212
  if (!setValue) {
208
213
  return;
209
214
  }
210
- // Adjust the value to match the selection
211
- const localValue = getIdFromSelection();
215
+
216
+ const localValue = getIdsFromLocalSelection();
212
217
  if (!_.isEqual(localValue, value)) {
213
218
  setValue(localValue);
214
219
  }
215
220
  },
216
221
  conformSelectionToValue = async () => {
217
- // adjust the selection to match the value
218
222
  let newSelection = [];
219
223
  if (Repository) {
220
224
  // Get entity or entities that match value
@@ -273,7 +277,8 @@ export default function withSelection(WrappedComponent) {
273
277
 
274
278
  (async () => {
275
279
 
276
- if (Repository && usesWithValue && !Repository.isLoaded && Repository.isRemote && !Repository.isAutoLoad && !Repository.isLoading) {
280
+ if (usesWithValue && Repository?.isRemote
281
+ && !Repository.isAutoLoad && !Repository.isLoaded && !Repository.isLoading) {
277
282
  // on initialization, we can't conformSelectionToValue if the repository is not yet loaded,
278
283
  // so first load repo, then conform to value
279
284
  await Repository.load();
@@ -283,6 +288,10 @@ export default function withSelection(WrappedComponent) {
283
288
 
284
289
  await conformSelectionToValue();
285
290
 
291
+ } else if (!_.isEmpty(selection)) {
292
+
293
+ conformValueToLocalSelection();
294
+
286
295
  } else if (autoSelectFirstItem) {
287
296
  let newSelection = [];
288
297
  if (Repository) {
@@ -300,7 +309,6 @@ export default function withSelection(WrappedComponent) {
300
309
 
301
310
  }, []);
302
311
 
303
-
304
312
  if (usesWithValue) {
305
313
  useEffect(() => {
306
314
  if (!isReady) {
@@ -309,15 +317,16 @@ export default function withSelection(WrappedComponent) {
309
317
 
310
318
  conformSelectionToValue();
311
319
 
312
- }, [value, isReady]);
320
+ }, [value]);
313
321
 
314
322
  useEffect(() => {
315
323
  if (!isReady) {
316
324
  return () => {};
317
325
  }
318
326
 
319
- conformValueToSelection();
320
- }, [selection, isReady]);
327
+ conformValueToLocalSelection();
328
+
329
+ }, [selection]);
321
330
  }
322
331
 
323
332
  if (!isReady) {
@@ -336,8 +345,8 @@ export default function withSelection(WrappedComponent) {
336
345
  deselectAll={deselectAll}
337
346
  selectRangeTo={selectRangeTo}
338
347
  isInSelection={isInSelection}
339
- getIdFromSelection={getIdFromSelection}
340
- getDisplayFromSelection={getDisplayFromSelection}
348
+ getIdsFromSelection={getIdsFromLocalSelection}
349
+ getDisplayValuesFromSelection={getDisplayValuesFromLocalSelection}
341
350
  />;
342
351
  };
343
352
  }
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect, } from 'react';
2
- import isJson from '../../Functions/isJson.js';
2
+ import natsort from 'natsort';
3
3
  import _ from 'lodash';
4
4
 
5
5
  // This HOC gives the component value props, primarily for a Form Field.
@@ -19,6 +19,9 @@ export default function withValue(WrappedComponent) {
19
19
  onChangeValue,
20
20
  value,
21
21
  startingValue = null,
22
+ valueIsAlwaysArray = false,
23
+ valueAsIdAndText = false,
24
+ valueAsStringifiedJson = false,
22
25
 
23
26
  // withData
24
27
  Repository,
@@ -26,6 +29,39 @@ export default function withValue(WrappedComponent) {
26
29
  } = props,
27
30
  [localValue, setLocalValue] = useState(startingValue || value),
28
31
  setValue = (newValue) => {
32
+ if (valueIsAlwaysArray && !_.isArray(newValue)) {
33
+ newValue = _.isNil(newValue) ? [] : [newValue];
34
+ }
35
+ if (_.isArray(newValue)) {
36
+ // TODO: sort by the sortProperty, whatever that is, instead of just value
37
+ newValue.sort(natsort()); // Only sort if we're using id/text arrangement. Otherwise, keep sort order as specified in Repository.
38
+ }
39
+ if (valueAsIdAndText) {
40
+ if (_.isArray(newValue)) {
41
+ newValue = _.map(newValue, (id) => {
42
+ if (_.isNil(id)) {
43
+ return id;
44
+ }
45
+ const record = Repository.getById(id);
46
+ return {
47
+ id: record.getId(),
48
+ text: record.getDisplayValue(),
49
+ };
50
+ })
51
+ } else {
52
+ if (!_.isNil(id)) {
53
+ const record = Repository.getById(newValue);
54
+ newValue = {
55
+ id: record.getId(),
56
+ text: record.getDisplayValue(),
57
+ };
58
+ }
59
+ }
60
+ }
61
+ if (valueAsStringifiedJson) {
62
+ newValue = JSON.stringify(newValue);
63
+ }
64
+
29
65
  if (newValue === localValue) {
30
66
  return;
31
67
  }
@@ -33,10 +69,6 @@ export default function withValue(WrappedComponent) {
33
69
  setLocalValue(newValue);
34
70
 
35
71
  if (onChangeValue) {
36
- if (_.isArray(newValue)) {
37
- // convert from inner value to outer value
38
- newValue = JSON.stringify(newValue);
39
- }
40
72
  onChangeValue(newValue);
41
73
  }
42
74
  },
@@ -71,10 +103,26 @@ export default function withValue(WrappedComponent) {
71
103
  }, [value]);
72
104
 
73
105
 
106
+ // Convert localValue to normal JS primitives for field components
74
107
  let convertedValue = localValue;
75
- if (_.isString(localValue) && isJson(localValue) && !_.isNil(localValue)) {
76
- // convert from outer value to inner value
77
- convertedValue = JSON.parse(localValue);
108
+ if (_.isString(convertedValue) && valueAsStringifiedJson && !_.isNil(convertedValue)) {
109
+ convertedValue = JSON.parse(convertedValue);
110
+ }
111
+ if (valueIsAlwaysArray) {
112
+ if (_.isEmpty(convertedValue) || _.isNil(convertedValue)) {
113
+ convertedValue = null;
114
+ } else if (convertedValue.length === 1) {
115
+ convertedValue = convertedValue[0];
116
+ }
117
+ }
118
+ if (valueAsIdAndText && !_.isNil(convertedValue)) {
119
+ if (_.isArray(convertedValue)) {
120
+ convertedValue = _.map(convertedValue, (value) => {
121
+ return value?.id;
122
+ });
123
+ } else {
124
+ convertedValue = convertedValue?.id;
125
+ }
78
126
  }
79
127
 
80
128
  return <WrappedComponent
@@ -0,0 +1,14 @@
1
+ // Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.
2
+ import * as React from "react"
3
+ import Svg, { Path } from "react-native-svg"
4
+ import { Icon } from 'native-base';
5
+
6
+ function SvgComponent(props) {
7
+ return (
8
+ <Icon xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" {...props}>
9
+ <Path d="M304 240V16.6c0-9 7-16.6 16-16.6 123.7 0 224 100.3 224 224 0 9-7.6 16-16.6 16H304zM32 272c0-121.3 90.1-221.7 207-237.7 9.2-1.3 17 6.1 17 15.4V288l156.5 156.5c6.7 6.7 6.2 17.7-1.5 23.1-39.2 28-87.2 44.4-139 44.4-132.5 0-240-107.4-240-240zm526.4 16c9.3 0 16.6 7.8 15.4 17-7.7 55.9-34.6 105.6-73.9 142.3-6 5.6-15.4 5.2-21.2-.7L320 288h238.4z" />
10
+ </Icon>
11
+ )
12
+ }
13
+
14
+ export default SvgComponent
@@ -0,0 +1,14 @@
1
+ // Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.
2
+ import * as React from "react"
3
+ import Svg, { Path } from "react-native-svg"
4
+ import { Icon } from 'native-base';
5
+
6
+ function SvgComponent(props) {
7
+ return (
8
+ <Icon xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" {...props}>
9
+ <Path d="M308.5 135.3c7.1-6.3 9.9-16.2 6.2-25-2.3-5.3-4.8-10.5-7.6-15.5l-3.1-5.4c-3-5-6.3-9.9-9.8-14.6-5.7-7.6-15.7-10.1-24.7-7.1L241.3 77c-10.7-8.8-23-16-36.2-20.9l-6.1-29c-1.9-9.3-9.1-16.7-18.5-17.8-6.6-.9-13.3-1.3-20.1-1.3h-.7c-6.8 0-13.5.4-20.1 1.2-9.4 1.1-16.6 8.6-18.5 17.8L115 56.1c-13.3 5-25.5 12.1-36.2 20.9l-28.3-9.2c-9-3-19-.5-24.7 7.1-3.5 4.7-6.8 9.6-9.9 14.6l-3 5.3c-2.8 5-5.3 10.2-7.6 15.6-3.7 8.7-.9 18.6 6.2 25l22.2 19.8c-1.1 6.7-1.7 13.7-1.7 20.8s.6 14.1 1.7 20.9l-22.2 19.8c-7.1 6.3-9.9 16.2-6.2 25 2.3 5.3 4.8 10.5 7.6 15.6l3 5.2c3 5.1 6.3 9.9 9.9 14.6 5.7 7.6 15.7 10.1 24.7 7.1l28.2-9.3c10.7 8.8 23 16 36.2 20.9l6.1 29.1c1.9 9.3 9.1 16.7 18.5 17.8 6.7.8 13.5 1.2 20.4 1.2s13.7-.4 20.4-1.2c9.4-1.1 16.6-8.6 18.5-17.8l6.1-29.1c13.3-5 25.5-12.1 36.2-20.9l28.2 9.3c9 3 19 .5 24.7-7.1 3.5-4.7 6.8-9.5 9.8-14.6l3.1-5.4c2.8-5 5.3-10.2 7.6-15.5 3.7-8.7.9-18.6-6.2-25l-22.2-19.8c1.1-6.8 1.7-13.8 1.7-20.9s-.6-14.1-1.7-20.9l22.2-19.8zM112 176a48 48 0 1196 0 48 48 0 11-96 0zm392.7 324.5c6.3 7.1 16.2 9.9 25 6.2 5.3-2.3 10.5-4.8 15.5-7.6l5.4-3.1c5-3 9.9-6.3 14.6-9.8 7.6-5.7 10.1-15.7 7.1-24.7l-9.3-28.2c8.8-10.7 16-23 20.9-36.2L613 391c9.3-1.9 16.7-9.1 17.8-18.5.8-6.7 1.2-13.5 1.2-20.4s-.4-13.7-1.2-20.4c-1.1-9.4-8.6-16.6-17.8-18.5l-29.1-6.2c-5-13.3-12.1-25.5-20.9-36.2l9.3-28.2c3-9 .5-19-7.1-24.7-4.7-3.5-9.6-6.8-14.6-9.9l-5.3-3c-5-2.8-10.2-5.3-15.6-7.6-8.7-3.7-18.6-.9-25 6.2l-19.8 22.2c-6.8-1.1-13.8-1.7-20.9-1.7s-14.1.6-20.9 1.7l-19.8-22.2c-6.3-7.1-16.2-9.9-25-6.2-5.3 2.3-10.5 4.8-15.6 7.6l-5.2 3c-5.1 3-9.9 6.3-14.6 9.9-7.6 5.7-10.1 15.7-7.1 24.7l9.3 28.2c-8.8 10.7-16 23-20.9 36.2l-29.1 6c-9.3 1.9-16.7 9.1-17.8 18.5-.8 6.7-1.2 13.5-1.2 20.4s.4 13.7 1.2 20.4c1.1 9.4 8.6 16.6 17.8 18.5l29.1 6.1c5 13.3 12.1 25.5 20.9 36.2l-9.3 28.2c-3 9-.5 19 7.1 24.7 4.7 3.5 9.5 6.8 14.6 9.8l5.4 3.1c5 2.8 10.2 5.3 15.5 7.6 8.7 3.7 18.6.9 25-6.2l19.8-22.2c6.8 1.1 13.8 1.7 20.9 1.7s14.1-.6 20.9-1.7l19.8 22.2zM464 304a48 48 0 110 96 48 48 0 110-96z" />
10
+ </Icon>
11
+ )
12
+ }
13
+
14
+ export default SvgComponent
@@ -14,6 +14,7 @@ import Header from './Header.js';
14
14
  import Mask from './Mask.js';
15
15
  import withCollapsible from '../Hoc/withCollapsible.js';
16
16
  import emptyFn from '../../Functions/emptyFn.js';
17
+ import UiGlobals from '../../UiGlobals.js';
17
18
  import _ from 'lodash';
18
19
 
19
20
  // Note on collapseDirections:
@@ -34,7 +35,7 @@ function Panel(props) {
34
35
  onLayout = null,
35
36
 
36
37
  // Header
37
- title = Inflector.humanize(Inflector.underscore(props.model)),
38
+ title = UiGlobals.customInflect(Inflector.camel2words(Inflector.underscore(props.model))),
38
39
  showHeader = true,
39
40
  header = null,
40
41
  isClosable = false,
@@ -0,0 +1,6 @@
1
+ import UiGlobals from '../UiGlobals.js';
2
+ import _ from 'lodash';
3
+
4
+ export default function setCustomInflector(customInflector) {
5
+ UiGlobals.customInflect = customInflector;
6
+ }
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, } from 'react';
1
+ import { useState, useEffect, useRef, } from 'react';
2
2
  import {
3
3
  Box,
4
4
  Button,
@@ -49,8 +49,9 @@ function AttachmentsElement(props) {
49
49
 
50
50
  } = props,
51
51
  styles = UiGlobals.styles,
52
- model = selectorSelected?.repository?.name,
53
- modelid = selectorSelected?.id,
52
+ model = _.isArray(selectorSelected) && selectorSelected[0] ? selectorSelected[0].repository?.name : selectorSelected?.repository?.name,
53
+ modelidCalc = _.isArray(selectorSelected) ? _.map(selectorSelected, (entity) => entity.id) : selectorSelected?.id,
54
+ modelid = useRef(modelidCalc),
54
55
  [isReady, setIsReady] = useState(false),
55
56
  [isUploading, setIsUploading] = useState(false),
56
57
  [showAll, setShowAll] = useState(false),
@@ -78,6 +79,9 @@ function AttachmentsElement(props) {
78
79
  });
79
80
  setFiles(files);
80
81
  },
82
+ clearFiles = () => {
83
+ setFiles([]);
84
+ },
81
85
  toggleShowAll = () => {
82
86
  setShowAll(!showAll);
83
87
  },
@@ -86,7 +90,7 @@ function AttachmentsElement(props) {
86
90
  _.each(files, (file) => {
87
91
  file.extraUploadData = {
88
92
  model,
89
- modelid,
93
+ modelid: modelid.current,
90
94
  };
91
95
  });
92
96
  },
@@ -112,6 +116,10 @@ function AttachmentsElement(props) {
112
116
  Repository.deleteById(id);
113
117
  };
114
118
 
119
+ if (!_.isEqual(modelidCalc, modelid.current)) {
120
+ modelid.current = modelidCalc;
121
+ }
122
+
115
123
  useEffect(() => {
116
124
 
117
125
  if (!model) {
@@ -120,37 +128,43 @@ function AttachmentsElement(props) {
120
128
 
121
129
  (async () => {
122
130
 
123
- // Load Repository
124
- const filters = [
125
- {
126
- name: 'model',
127
- value: model,
128
- },
129
- {
130
- name: 'modelid',
131
- value: modelid,
132
- },
133
- ];
134
- if (accept) {
135
- let name,
136
- mimetypes;
137
- if (_.isString(accept)) {
138
- name = 'mimetype LIKE';
139
- mimetypes = accept.replace('*', '%');
140
- } else if (_.isArray(accept)) {
141
- name = 'mimetype IN';
142
- mimetypes = accept;
131
+ if (!_.isArray(modelid.current)) {
132
+
133
+ // Load Repository
134
+ const filters = [
135
+ {
136
+ name: 'model',
137
+ value: model,
138
+ },
139
+ {
140
+ name: 'modelid',
141
+ value: modelid.current,
142
+ },
143
+ ];
144
+ if (accept) {
145
+ let name,
146
+ mimetypes;
147
+ if (_.isString(accept)) {
148
+ name = 'mimetype LIKE';
149
+ mimetypes = accept.replace('*', '%');
150
+ } else if (_.isArray(accept)) {
151
+ name = 'mimetype IN';
152
+ mimetypes = accept;
153
+ }
154
+ filters.push({
155
+ name,
156
+ value: mimetypes,
157
+ });
143
158
  }
144
- filters.push({
145
- name,
146
- value: mimetypes,
147
- });
159
+ Repository.filter(filters);
160
+ Repository.setPageSize(showAll ? EXPANDED_MAX : COLLAPSED_MAX);
161
+ await Repository.load();
162
+
163
+ buildFiles();
164
+ } else {
165
+ clearFiles();
148
166
  }
149
- Repository.filter(filters);
150
- Repository.setPageSize(showAll ? EXPANDED_MAX : COLLAPSED_MAX);
151
- await Repository.load();
152
167
 
153
- buildFiles();
154
168
 
155
169
  if (!isReady) {
156
170
  setIsReady(true);
@@ -162,7 +176,7 @@ function AttachmentsElement(props) {
162
176
  return () => {
163
177
  Repository.off('load', buildFiles);
164
178
  };
165
- }, [model, modelid, showAll]);
179
+ }, [model, modelid.current, showAll]);
166
180
 
167
181
  if (!isReady) {
168
182
  return null;
package/src/UiGlobals.js CHANGED
@@ -4,6 +4,7 @@ import _ from 'lodash';
4
4
 
5
5
  const Globals = {
6
6
  mode: CURRENT_MODE,
7
+ customInflect: (str) => str,
7
8
  };
8
9
 
9
10
  export default Globals;
@@ -1,11 +0,0 @@
1
- import {
2
- SELECTION_MODE_MULTI,
3
- } from '../../../Constants/Selection.js';
4
- import Combo from './Combo/Combo.js';
5
-
6
- export default function Tag(props) {
7
- return <Combo
8
- selectionMode={SELECTION_MODE_MULTI}
9
- {...props}
10
- />;
11
- }