@onehat/ui 0.4.104 → 0.4.106

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.104",
3
+ "version": "0.4.106",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -139,6 +139,7 @@ function Form(props) {
139
139
  onDelete,
140
140
  editorStateRef,
141
141
  disableView,
142
+ isEditorModeControlledByParent = false,
142
143
 
143
144
  // parent container
144
145
  selectorId,
@@ -968,6 +969,14 @@ function Form(props) {
968
969
  buildAncillary = () => {
969
970
  const
970
971
  validAncillaryItems = _.filter(ancillaryItems, (item) => !!item), // filter out any null/undefined items
972
+ parentEditorModeRaw = getEditorMode?.() || props.editorMode || null,
973
+ parentEditorMode = parentEditorModeRaw === EDITOR_MODE__ADD
974
+ ? EDITOR_MODE__EDIT
975
+ : parentEditorModeRaw,
976
+ normalizedParentEditorMode =
977
+ parentEditorMode === EDITOR_MODE__EDIT || parentEditorMode === EDITOR_MODE__VIEW
978
+ ? parentEditorMode
979
+ : null,
971
980
  components = [];
972
981
  setAncillaryButtons([]);
973
982
  if (validAncillaryItems.length) {
@@ -1010,6 +1019,8 @@ function Form(props) {
1010
1019
  }
1011
1020
 
1012
1021
  const
1022
+ ancillaryEditorMode = itemPropsToPass.editorMode ?? normalizedParentEditorMode,
1023
+ ancillaryInitialEditorMode = itemPropsToPass.initialEditorMode ?? ancillaryEditorMode ?? undefined,
1013
1024
  Element = getComponentFromType(type),
1014
1025
  element = <Element
1015
1026
  {...testProps('ancillary-' + type)}
@@ -1019,6 +1030,8 @@ function Form(props) {
1019
1030
  uniqueRepository={true}
1020
1031
  parent={self}
1021
1032
  {...itemPropsToPass}
1033
+ editorMode={ancillaryEditorMode}
1034
+ initialEditorMode={ancillaryInitialEditorMode}
1022
1035
  />;
1023
1036
  if (title) {
1024
1037
  if (record?.displayValue) {
@@ -1262,7 +1275,7 @@ function Form(props) {
1262
1275
 
1263
1276
  if (inArray(editorType, [EDITOR_TYPE__SIDE, EDITOR_TYPE__SMART, EDITOR_TYPE__WINDOWED]) &&
1264
1277
  isSingle && getEditorMode() === EDITOR_MODE__EDIT &&
1265
- (onBack || onViewMode)) {
1278
+ (onBack || onViewMode || isEditorModeControlledByParent)) {
1266
1279
  modeHeader = <Toolbar>
1267
1280
  <HStack className="flex-1 items-center">
1268
1281
  {onBack &&
@@ -1280,7 +1293,7 @@ function Form(props) {
1280
1293
  )}
1281
1294
  text="Back"
1282
1295
  />}
1283
- <Text className="text-[20px] ml-1 text-grey-500">Edit Mode</Text>
1296
+ <Text className="text-[20px] ml-1 text-grey-500">{isEditorModeControlledByParent ? 'Edit Mode (Inherited)' : 'Edit Mode'}</Text>
1284
1297
  </HStack>
1285
1298
  {onViewMode && !disableView && (!canUser || canUser(VIEW)) &&
1286
1299
  <Button
@@ -1,4 +1,4 @@
1
- import { forwardRef, useEffect, useState, useRef, } from 'react';
1
+ import { forwardRef, useContext, useEffect, useState, useRef, } from 'react';
2
2
  import {
3
3
  ADD,
4
4
  EDIT,
@@ -15,6 +15,7 @@ import {
15
15
  } from '../../Constants/Editor.js';
16
16
  import useForceUpdate from '../../Hooks/useForceUpdate.js'
17
17
  import Button from '../Buttons/Button.js';
18
+ import EditorModeContext from '../../Contexts/EditorModeContext.js';
18
19
  import UiGlobals from '../../UiGlobals.js';
19
20
  import _ from 'lodash';
20
21
 
@@ -56,8 +57,10 @@ export default function withEditor(WrappedComponent, isTree = false) {
56
57
  newEntityDisplayValue,
57
58
  newEntityDisplayProperty, // in case the field to set for newEntityDisplayValue is different from model
58
59
  defaultValues,
60
+ editorMode: parentEditorModeProp,
59
61
  initialEditorMode = EDITOR_MODE__VIEW,
60
62
  stayInEditModeOnSelectionChange = false,
63
+ inheritParentEditorMode = true,
61
64
 
62
65
  // withComponent
63
66
  self,
@@ -84,6 +87,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
84
87
  confirm,
85
88
  hideAlert,
86
89
  } = props,
90
+ parentEditorModeContext = useContext(EditorModeContext),
87
91
  forceUpdate = useForceUpdate(),
88
92
  listeners = useRef({}),
89
93
  editorStateRef = useRef(),
@@ -170,13 +174,99 @@ export default function withEditor(WrappedComponent, isTree = false) {
170
174
  forceUpdate();
171
175
  }
172
176
  },
177
+ getParentEditorMode = () => {
178
+ const contextMode = parentEditorModeContext?.effectiveEditorMode || null;
179
+ if (contextMode) {
180
+ return contextMode;
181
+ }
182
+
183
+ // Some modal implementations break React context boundaries. Fall back to
184
+ // an explicitly-passed parent mode so nested ancillary editors still inherit.
185
+ if (parentEditorModeProp === EDITOR_MODE__ADD) {
186
+ return EDITOR_MODE__EDIT;
187
+ }
188
+ if (parentEditorModeProp === EDITOR_MODE__EDIT || parentEditorModeProp === EDITOR_MODE__VIEW) {
189
+ return parentEditorModeProp;
190
+ }
191
+
192
+ return null;
193
+ },
194
+ getInheritedEditorMode = () => {
195
+ if (!inheritParentEditorMode) {
196
+ return null;
197
+ }
198
+ const parentMode = getParentEditorMode();
199
+ if (parentMode === EDITOR_MODE__ADD) {
200
+ return EDITOR_MODE__EDIT;
201
+ }
202
+ if (parentMode === EDITOR_MODE__EDIT || parentMode === EDITOR_MODE__VIEW) {
203
+ return parentMode;
204
+ }
205
+ return null;
206
+ },
207
+ getIsParentSaveLocked = () => {
208
+ return !!parentEditorModeContext?.isAnyAncestorUnsaved;
209
+ },
210
+ getIsCurrentSelectionUnsaved = () => {
211
+ const selection = getSelection();
212
+ if (!selection || selection.length !== 1) {
213
+ return false;
214
+ }
215
+ const record = selection[0];
216
+ if (!record || record.isDestroyed) {
217
+ return false;
218
+ }
219
+ return !!(record.isPhantom || record.isRemotePhantom);
220
+ },
221
+ getIsEditorDisabledByParent = () => {
222
+ return getIsParentSaveLocked();
223
+ },
224
+ getIsEditorModeControlledByParent = () => {
225
+ return !!getInheritedEditorMode();
226
+ },
173
227
  getNewEntityDisplayValue = () => {
174
228
  return newEntityDisplayValueRef.current;
175
229
  },
176
230
  setNewEntityDisplayValue = (val) => {
177
231
  newEntityDisplayValueRef.current = val;
178
232
  },
233
+ showViewFallback = async () => {
234
+ // helper for doEdit
235
+ // If the editor is forced into EDIT mode by parent inheritance,
236
+ // but the child editor cannot actually be in edit mode due to permissions or configuration,
237
+ // switch the mode to VIEW.
238
+
239
+ if (!userCanView) {
240
+ return;
241
+ }
242
+ if (canUser && !canUser(VIEW)) {
243
+ showPermissionsError(VIEW);
244
+ return;
245
+ }
246
+ if (canProceedWithCrud && !canProceedWithCrud()) {
247
+ return;
248
+ }
249
+ if (editorType === EDITOR_TYPE__INLINE) {
250
+ return;
251
+ }
252
+ const selection = getSelection();
253
+ if (selection.length !== 1) {
254
+ return;
255
+ }
256
+ setIsEditorViewOnly(true);
257
+ setEditorMode(EDITOR_MODE__VIEW);
258
+ setIsEditorShown(true);
259
+ if (getListeners().onAfterView) {
260
+ await getListeners().onAfterView();
261
+ }
262
+ },
179
263
  doAdd = async (e, values) => {
264
+ if (getIsEditorDisabledByParent()) {
265
+ return;
266
+ }
267
+ if (getInheritedEditorMode() === EDITOR_MODE__VIEW) {
268
+ return;
269
+ }
180
270
  if (canUser && !canUser(ADD)) {
181
271
  showPermissionsError(ADD);
182
272
  return;
@@ -295,7 +385,23 @@ export default function withEditor(WrappedComponent, isTree = false) {
295
385
  setIsEditorShown(true);
296
386
  },
297
387
  doEdit = async () => {
388
+ if (getIsEditorDisabledByParent()) {
389
+ return;
390
+ }
391
+ const inheritedEditorMode = getInheritedEditorMode();
392
+ if (inheritedEditorMode === EDITOR_MODE__VIEW) {
393
+ await doView(false);
394
+ return;
395
+ }
396
+ if (inheritedEditorMode === EDITOR_MODE__EDIT && (!userCanEdit || disableEdit || canEditorViewOnly)) {
397
+ await showViewFallback();
398
+ return;
399
+ }
298
400
  if (canUser && !canUser(EDIT)) {
401
+ if (inheritedEditorMode === EDITOR_MODE__EDIT) {
402
+ await showViewFallback();
403
+ return;
404
+ }
299
405
  showPermissionsError(EDIT);
300
406
  return;
301
407
  }
@@ -317,6 +423,12 @@ export default function withEditor(WrappedComponent, isTree = false) {
317
423
  setIsEditorShown(true);
318
424
  },
319
425
  doDelete = async (args) => {
426
+ if (getIsEditorDisabledByParent()) {
427
+ return;
428
+ }
429
+ if (getInheritedEditorMode() === EDITOR_MODE__VIEW) {
430
+ return;
431
+ }
320
432
  if (canUser && !canUser(DELETE)) {
321
433
  showPermissionsError(DELETE);
322
434
  return;
@@ -430,6 +542,17 @@ export default function withEditor(WrappedComponent, isTree = false) {
430
542
  }
431
543
  },
432
544
  doView = async (allowEditing = false) => {
545
+ if (getIsEditorDisabledByParent()) {
546
+ return;
547
+ }
548
+ const inheritedEditorMode = getInheritedEditorMode();
549
+ if (inheritedEditorMode === EDITOR_MODE__EDIT) {
550
+ await doEdit();
551
+ return;
552
+ }
553
+ if (inheritedEditorMode === EDITOR_MODE__VIEW) {
554
+ allowEditing = false;
555
+ }
433
556
  if (!userCanView) {
434
557
  return;
435
558
  }
@@ -460,6 +583,12 @@ export default function withEditor(WrappedComponent, isTree = false) {
460
583
  }
461
584
  },
462
585
  doDuplicate = async () => {
586
+ if (getIsEditorDisabledByParent()) {
587
+ return;
588
+ }
589
+ if (getInheritedEditorMode() === EDITOR_MODE__VIEW) {
590
+ return;
591
+ }
463
592
  if (!userCanEdit || disableDuplicate) {
464
593
  return;
465
594
  }
@@ -533,6 +662,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
533
662
  }
534
663
  },
535
664
  doEditorSave = async (data, e) => {
665
+ if (getIsEditorDisabledByParent()) {
666
+ return false;
667
+ }
536
668
  let mode = getEditorMode() === EDITOR_MODE__ADD ? ADD : EDIT;
537
669
  if (canUser && !canUser(mode)) {
538
670
  showPermissionsError(mode);
@@ -651,6 +783,12 @@ export default function withEditor(WrappedComponent, isTree = false) {
651
783
  setIsEditorShown(false);
652
784
  },
653
785
  doEditorDelete = async () => {
786
+ if (getIsEditorDisabledByParent()) {
787
+ return;
788
+ }
789
+ if (getInheritedEditorMode() === EDITOR_MODE__VIEW) {
790
+ return;
791
+ }
654
792
  if (canUser && !canUser(DELETE)) {
655
793
  showPermissionsError(DELETE);
656
794
  return;
@@ -662,6 +800,46 @@ export default function withEditor(WrappedComponent, isTree = false) {
662
800
  });
663
801
  },
664
802
  calculateEditorMode = () => {
803
+ // Calculate the editor's effective mode based on parent inheritance, permissions, and local selection state.
804
+ // Priority order:
805
+ // 1. If parent is save-locked (unsaved ancestor), force VIEW mode
806
+ // 2. If parent forces VIEW mode via inheritance, return VIEW (child cannot edit if parent is view-only)
807
+ // 3. If parent forces EDIT mode via inheritance, check child permissions:
808
+ // a. If parent disabled, child disabled, or child cannot edit, return VIEW
809
+ // b. If single phantom record, return ADD (new record being created)
810
+ // c. Otherwise return EDIT or VIEW based on selection count
811
+ // 4. Fall back to local selection heuristics (multiple→EDIT, single→VIEW, stays in previous mode if configured)
812
+ const
813
+ selection = getSelection(),
814
+ inheritedEditorMode = getInheritedEditorMode();
815
+
816
+ if (getIsEditorDisabledByParent()) {
817
+ return EDITOR_MODE__VIEW;
818
+ }
819
+
820
+ if (inheritedEditorMode === EDITOR_MODE__VIEW) {
821
+ return EDITOR_MODE__VIEW;
822
+ }
823
+
824
+ if (inheritedEditorMode === EDITOR_MODE__EDIT) {
825
+ if (!getCanEditorBeInEditMode()) {
826
+ return EDITOR_MODE__VIEW;
827
+ }
828
+ if (canEditorViewOnly || !userCanEdit || disableEdit) {
829
+ return EDITOR_MODE__VIEW;
830
+ }
831
+ if (canUser && !canUser(EDIT)) {
832
+ return EDITOR_MODE__VIEW;
833
+ }
834
+ if (canRecordBeEdited && canRecordBeEdited(selection) === false) {
835
+ return EDITOR_MODE__VIEW;
836
+ }
837
+ if (selection.length === 1 && !selection[0].isDestroyed && (selection[0].isPhantom || selection[0].isRemotePhantom) && !disableAdd) {
838
+ return EDITOR_MODE__ADD;
839
+ }
840
+ return selection.length ? EDITOR_MODE__EDIT : EDITOR_MODE__VIEW;
841
+ }
842
+
665
843
  if (!getCanEditorBeInEditMode()) { // this is a result of canRecordBeEdited returning false
666
844
  return EDITOR_MODE__VIEW;
667
845
  }
@@ -677,7 +855,6 @@ export default function withEditor(WrappedComponent, isTree = false) {
677
855
  }
678
856
 
679
857
  // calculateEditorMode gets called only on selection changes
680
- const selection = getSelection();
681
858
  let mode;
682
859
  if (editorType === EDITOR_TYPE__SIDE && !_.isNil(UiGlobals.isSideEditorAlwaysEditMode) && UiGlobals.isSideEditorAlwaysEditMode) {
683
860
  // special case: side editor is always edit mode
@@ -705,6 +882,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
705
882
  return mode;
706
883
  },
707
884
  setEditMode = () => {
885
+ if (getIsEditorDisabledByParent() || getIsEditorModeControlledByParent()) {
886
+ return;
887
+ }
708
888
  if (canUser && !canUser(EDIT)) {
709
889
  showPermissionsError(EDIT);
710
890
  return;
@@ -713,6 +893,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
713
893
  setEditorMode(EDITOR_MODE__EDIT);
714
894
  },
715
895
  setViewMode = () => {
896
+ if (getIsEditorDisabledByParent() || getIsEditorModeControlledByParent()) {
897
+ return;
898
+ }
716
899
  if (canUser && !canUser(VIEW)) {
717
900
  showPermissionsError(VIEW);
718
901
  return;
@@ -729,10 +912,20 @@ export default function withEditor(WrappedComponent, isTree = false) {
729
912
  }
730
913
  };
731
914
 
915
+ const
916
+ inheritedEditorMode = getInheritedEditorMode(),
917
+ isEditorDisabledByParent = getIsEditorDisabledByParent(),
918
+ isEditorModeControlledByParent = getIsEditorModeControlledByParent(),
919
+ isCurrentSelectionUnsaved = getIsCurrentSelectionUnsaved(),
920
+ isAnyAncestorUnsaved = getIsParentSaveLocked() || isCurrentSelectionUnsaved,
921
+ isCrudBlockedByInheritedView = inheritedEditorMode === EDITOR_MODE__VIEW;
922
+
732
923
  useEffect(() => {
733
924
 
734
925
  if (editorType === EDITOR_TYPE__SIDE) {
735
- if (selection?.length) { // || isAdding
926
+ if (isEditorDisabledByParent) {
927
+ setIsEditorShown(false);
928
+ } else if (selection?.length) { // || isAdding
736
929
  // there is a selection, so show the editor
737
930
  setIsEditorShown(true);
738
931
  } else {
@@ -746,6 +939,19 @@ export default function withEditor(WrappedComponent, isTree = false) {
746
939
  } else {
747
940
  setCanEditorBeInEditMode(true);
748
941
  }
942
+
943
+ if (isEditorDisabledByParent || inheritedEditorMode === EDITOR_MODE__VIEW) {
944
+ setIsEditorViewOnly(true);
945
+ } else if (inheritedEditorMode === EDITOR_MODE__EDIT) {
946
+ const canEditInInheritedMode =
947
+ !canEditorViewOnly &&
948
+ userCanEdit &&
949
+ !disableEdit &&
950
+ (!canUser || canUser(EDIT)) &&
951
+ (!canRecordBeEdited || canRecordBeEdited(selection));
952
+ setIsEditorViewOnly(!canEditInInheritedMode);
953
+ }
954
+
749
955
  setEditorMode(calculateEditorMode());
750
956
  setLastSelection(selection);
751
957
 
@@ -755,7 +961,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
755
961
  Promise.resolve().then(() => {
756
962
  setIsIgnoreNextSelectionChange(false);
757
963
  });
758
- }, [selection]);
964
+ }, [selection, inheritedEditorMode, isEditorDisabledByParent]);
759
965
 
760
966
  if (self) {
761
967
  self.add = doAdd;
@@ -776,7 +982,13 @@ export default function withEditor(WrappedComponent, isTree = false) {
776
982
  setEditorMode(calculateEditorMode());
777
983
  }
778
984
 
779
- return <WrappedComponent
985
+ const editorModeContextValue = {
986
+ effectiveEditorMode: getEditorMode(),
987
+ isAnyAncestorUnsaved,
988
+ };
989
+
990
+ return <EditorModeContext.Provider value={editorModeContextValue}>
991
+ <WrappedComponent
780
992
  {...props}
781
993
  ref={ref}
782
994
  disableWithEditor={false}
@@ -786,35 +998,38 @@ export default function withEditor(WrappedComponent, isTree = false) {
786
998
  isEditorShown={getIsEditorShown()}
787
999
  getIsEditorShown={getIsEditorShown}
788
1000
  isEditorViewOnly={isEditorViewOnly}
1001
+ isEditorModeControlledByParent={isEditorModeControlledByParent}
1002
+ isEditorDisabledByParent={isEditorDisabledByParent}
789
1003
  isAdding={isAdding}
790
1004
  isSaving={isSaving}
791
1005
  editorMode={getEditorMode()}
792
1006
  getEditorMode={getEditorMode}
793
- onEditMode={setEditMode}
794
- onViewMode={setViewMode}
1007
+ onEditMode={(isEditorModeControlledByParent || isEditorDisabledByParent) ? null : setEditMode}
1008
+ onViewMode={(isEditorModeControlledByParent || isEditorDisabledByParent) ? null : setViewMode}
795
1009
  editorStateRef={editorStateRef}
796
1010
  setIsEditorShown={setIsEditorShown}
797
1011
  setIsIgnoreNextSelectionChange={setIsIgnoreNextSelectionChange}
798
- onAdd={(!userCanEdit || disableAdd) ? null : doAdd}
799
- onEdit={(!userCanEdit || disableEdit || (canRecordBeEdited && !canRecordBeEdited(selection))) ? null : doEdit}
800
- onDelete={(!userCanEdit || disableDelete || (canRecordBeDeleted && !canRecordBeDeleted(selection))) ? null : doDelete}
801
- onView={doView}
802
- onDuplicate={doDuplicate}
1012
+ onAdd={(isEditorDisabledByParent || isCrudBlockedByInheritedView || !userCanEdit || disableAdd) ? null : doAdd}
1013
+ onEdit={(isEditorDisabledByParent || isCrudBlockedByInheritedView || !userCanEdit || disableEdit || (canRecordBeEdited && !canRecordBeEdited(selection))) ? null : doEdit}
1014
+ onDelete={(isEditorDisabledByParent || isCrudBlockedByInheritedView || !userCanEdit || disableDelete || (canRecordBeDeleted && !canRecordBeDeleted(selection))) ? null : doDelete}
1015
+ onView={isEditorDisabledByParent ? null : doView}
1016
+ onDuplicate={(isEditorDisabledByParent || isCrudBlockedByInheritedView) ? null : doDuplicate}
803
1017
  onEditorSave={doEditorSave}
804
1018
  onEditorCancel={doEditorCancel}
805
- onEditorDelete={(!userCanEdit || disableDelete) ? null : doEditorDelete}
1019
+ onEditorDelete={(isEditorDisabledByParent || isCrudBlockedByInheritedView || !userCanEdit || disableDelete) ? null : doEditorDelete}
806
1020
  onEditorClose={doEditorClose}
807
1021
  setWithEditListeners={setListeners}
808
1022
  isEditor={true}
809
1023
  userCanEdit={userCanEdit}
810
1024
  userCanView={userCanView}
811
- disableAdd={disableAdd}
812
- disableEdit={disableEdit}
813
- disableDelete={disableDelete}
814
- disableDuplicate={disableDuplicate}
815
- disableView ={disableView}
1025
+ disableAdd={disableAdd || isEditorDisabledByParent || isCrudBlockedByInheritedView}
1026
+ disableEdit={disableEdit || isEditorDisabledByParent || isCrudBlockedByInheritedView}
1027
+ disableDelete={disableDelete || isEditorDisabledByParent || isCrudBlockedByInheritedView}
1028
+ disableDuplicate={disableDuplicate || isEditorDisabledByParent || isCrudBlockedByInheritedView}
1029
+ disableView ={disableView || isEditorDisabledByParent}
816
1030
  setSelection={setSelectionDecorated}
817
1031
  isTree={isTree}
818
- />;
1032
+ />
1033
+ </EditorModeContext.Provider>;
819
1034
  });
820
1035
  }
@@ -126,6 +126,11 @@ export default function MetersEditor(props) {
126
126
  isEditable: false,
127
127
  isEditingEnabledInPlainEditor: true,
128
128
  },
129
+ {
130
+ name: 'meters__latest_meter_reading_date',
131
+ isEditable: false,
132
+ isEditingEnabledInPlainEditor: true,
133
+ },
129
134
  ...(includeExtendedCalculatedFields ? [
130
135
  {
131
136
  name: 'meters__latest_inspection_date',
@@ -18,6 +18,9 @@ import {
18
18
  EDIT,
19
19
  } from '../../Constants/Commands.js';
20
20
  import {
21
+ EDITOR_MODE__ADD,
22
+ EDITOR_MODE__EDIT,
23
+ EDITOR_MODE__VIEW,
21
24
  EDITOR_TYPE__SIDE,
22
25
  EDITOR_TYPE__SMART,
23
26
  } from '../../Constants/Editor.js';
@@ -69,9 +72,12 @@ function Viewer(props) {
69
72
 
70
73
  // withEditor
71
74
  editorType,
75
+ getEditorMode,
76
+ editorMode,
72
77
  onEditMode,
73
78
  onClose,
74
79
  onDelete,
80
+ isEditorModeControlledByParent = false,
75
81
 
76
82
  // parent container
77
83
  selectorId,
@@ -352,6 +358,14 @@ function Viewer(props) {
352
358
  buildAncillary = () => {
353
359
  const
354
360
  validAncillaryItems = _.filter(ancillaryItems, (item) => !!item), // filter out any null/undefined items
361
+ parentEditorModeRaw = (getEditorMode && getEditorMode()) || editorMode || null,
362
+ parentEditorMode = parentEditorModeRaw === EDITOR_MODE__ADD
363
+ ? EDITOR_MODE__EDIT
364
+ : parentEditorModeRaw,
365
+ normalizedParentEditorMode =
366
+ parentEditorMode === EDITOR_MODE__EDIT || parentEditorMode === EDITOR_MODE__VIEW
367
+ ? parentEditorMode
368
+ : null,
355
369
  components = [];
356
370
  setAncillaryButtons([]);
357
371
  if (validAncillaryItems.length) {
@@ -395,6 +409,8 @@ function Viewer(props) {
395
409
  }
396
410
 
397
411
  const
412
+ ancillaryEditorMode = itemPropsToPass.editorMode ?? normalizedParentEditorMode,
413
+ ancillaryInitialEditorMode = itemPropsToPass.initialEditorMode ?? ancillaryEditorMode ?? undefined,
398
414
  Element = getComponentFromType(type),
399
415
  element = <Element
400
416
  {...testProps('ancillary-' + type)}
@@ -406,6 +422,8 @@ function Viewer(props) {
406
422
  uniqueRepository={true}
407
423
  parent={self}
408
424
  {...itemPropsToPass}
425
+ editorMode={ancillaryEditorMode}
426
+ initialEditorMode={ancillaryInitialEditorMode}
409
427
  className={className}
410
428
  canRowsReorder={false}
411
429
  />;
@@ -563,7 +581,7 @@ function Viewer(props) {
563
581
 
564
582
  <Toolbar className="justify-end">
565
583
  <HStack className="flex-1 items-center">
566
- <Text className="text-[20px] ml-1 text-grey-500">View Mode</Text>
584
+ <Text className="text-[20px] ml-1 text-grey-500">{isEditorModeControlledByParent ? 'View Mode (Inherited)' : 'View Mode'}</Text>
567
585
  </HStack>
568
586
  {onEditMode && (!canUser || canUser(EDIT)) &&
569
587
  <Button
@@ -0,0 +1,5 @@
1
+ import { createContext } from 'react';
2
+
3
+ const EditorModeContext = createContext(null);
4
+
5
+ export default EditorModeContext;
@@ -388,6 +388,19 @@ function AttachmentsElement(props) {
388
388
  downloadInBackground(url, {}, Attachments.headers);
389
389
  }
390
390
  },
391
+ onDownloadAll = () => {
392
+ if (!model || _.isNil(modelid.current) || _.isArray(modelid.current)) {
393
+ alert('Cannot download all attachments without a single selected model and model id.');
394
+ return;
395
+ }
396
+
397
+ const url = Attachments.api.baseURL + 'Attachments/downloadAll/' + encodeURIComponent(model) + '/' + encodeURIComponent(modelid.current);
398
+ if (isPwa) {
399
+ alert('Files cannot be downloaded and viewed within an iOS PWA. Please use the Safari browser instead.');
400
+ } else {
401
+ downloadInBackground(url, {}, Attachments.headers);
402
+ }
403
+ },
391
404
 
392
405
  // dropzone
393
406
  onDropzoneChange = async (files) => {
@@ -1087,6 +1100,19 @@ function AttachmentsElement(props) {
1087
1100
  )}
1088
1101
  tooltip="List View"
1089
1102
  />
1103
+ <Box className="spacer flex-1" />
1104
+ <IconButton
1105
+ onPress={onDownloadAll}
1106
+ icon={Download}
1107
+ className={clsx(
1108
+ 'w-[25px]',
1109
+ 'h-[25px]',
1110
+ 'px-[2px]',
1111
+ 'py-[2px]',
1112
+ )}
1113
+ isDisabled={!model || _.isNil(modelid.current) || _.isArray(modelid.current) || files.length === 0}
1114
+ tooltip="Download All"
1115
+ />
1090
1116
  </HStack>
1091
1117
 
1092
1118
  {content}
@@ -1,2 +0,0 @@
1
- export const EDITOR_MODE_ADD = 'EDITOR_MODE_ADD';
2
- export const EDITOR_MODE_EDIT = 'EDITOR_MODE_EDIT';