@onehat/ui 0.4.104 → 0.4.105

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.105",
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,
@@ -1262,7 +1263,7 @@ function Form(props) {
1262
1263
 
1263
1264
  if (inArray(editorType, [EDITOR_TYPE__SIDE, EDITOR_TYPE__SMART, EDITOR_TYPE__WINDOWED]) &&
1264
1265
  isSingle && getEditorMode() === EDITOR_MODE__EDIT &&
1265
- (onBack || onViewMode)) {
1266
+ (onBack || onViewMode || isEditorModeControlledByParent)) {
1266
1267
  modeHeader = <Toolbar>
1267
1268
  <HStack className="flex-1 items-center">
1268
1269
  {onBack &&
@@ -1280,7 +1281,7 @@ function Form(props) {
1280
1281
  )}
1281
1282
  text="Back"
1282
1283
  />}
1283
- <Text className="text-[20px] ml-1 text-grey-500">Edit Mode</Text>
1284
+ <Text className="text-[20px] ml-1 text-grey-500">{isEditorModeControlledByParent ? 'Edit Mode (Inherited)' : 'Edit Mode'}</Text>
1284
1285
  </HStack>
1285
1286
  {onViewMode && !disableView && (!canUser || canUser(VIEW)) &&
1286
1287
  <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
 
@@ -58,6 +59,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
58
59
  defaultValues,
59
60
  initialEditorMode = EDITOR_MODE__VIEW,
60
61
  stayInEditModeOnSelectionChange = false,
62
+ inheritParentEditorMode = true,
61
63
 
62
64
  // withComponent
63
65
  self,
@@ -84,6 +86,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
84
86
  confirm,
85
87
  hideAlert,
86
88
  } = props,
89
+ parentEditorModeContext = useContext(EditorModeContext),
87
90
  forceUpdate = useForceUpdate(),
88
91
  listeners = useRef({}),
89
92
  editorStateRef = useRef(),
@@ -170,13 +173,85 @@ export default function withEditor(WrappedComponent, isTree = false) {
170
173
  forceUpdate();
171
174
  }
172
175
  },
176
+ getParentEditorMode = () => {
177
+ return parentEditorModeContext?.effectiveEditorMode || null;
178
+ },
179
+ getInheritedEditorMode = () => {
180
+ if (!inheritParentEditorMode) {
181
+ return null;
182
+ }
183
+ const parentMode = getParentEditorMode();
184
+ if (parentMode === EDITOR_MODE__ADD) {
185
+ return EDITOR_MODE__EDIT;
186
+ }
187
+ if (parentMode === EDITOR_MODE__EDIT || parentMode === EDITOR_MODE__VIEW) {
188
+ return parentMode;
189
+ }
190
+ return null;
191
+ },
192
+ getIsParentSaveLocked = () => {
193
+ return !!parentEditorModeContext?.isAnyAncestorUnsaved;
194
+ },
195
+ getIsCurrentSelectionUnsaved = () => {
196
+ const selection = getSelection();
197
+ if (!selection || selection.length !== 1) {
198
+ return false;
199
+ }
200
+ const record = selection[0];
201
+ if (!record || record.isDestroyed) {
202
+ return false;
203
+ }
204
+ return !!(record.isPhantom || record.isRemotePhantom);
205
+ },
206
+ getIsEditorDisabledByParent = () => {
207
+ return getIsParentSaveLocked();
208
+ },
209
+ getIsEditorModeControlledByParent = () => {
210
+ return !!getInheritedEditorMode();
211
+ },
173
212
  getNewEntityDisplayValue = () => {
174
213
  return newEntityDisplayValueRef.current;
175
214
  },
176
215
  setNewEntityDisplayValue = (val) => {
177
216
  newEntityDisplayValueRef.current = val;
178
217
  },
218
+ showViewFallback = async () => {
219
+ // helper for doEdit
220
+ // If the editor is forced into EDIT mode by parent inheritance,
221
+ // but the child editor cannot actually be in edit mode due to permissions or configuration,
222
+ // switch the mode to VIEW.
223
+
224
+ if (!userCanView) {
225
+ return;
226
+ }
227
+ if (canUser && !canUser(VIEW)) {
228
+ showPermissionsError(VIEW);
229
+ return;
230
+ }
231
+ if (canProceedWithCrud && !canProceedWithCrud()) {
232
+ return;
233
+ }
234
+ if (editorType === EDITOR_TYPE__INLINE) {
235
+ return;
236
+ }
237
+ const selection = getSelection();
238
+ if (selection.length !== 1) {
239
+ return;
240
+ }
241
+ setIsEditorViewOnly(true);
242
+ setEditorMode(EDITOR_MODE__VIEW);
243
+ setIsEditorShown(true);
244
+ if (getListeners().onAfterView) {
245
+ await getListeners().onAfterView();
246
+ }
247
+ },
179
248
  doAdd = async (e, values) => {
249
+ if (getIsEditorDisabledByParent()) {
250
+ return;
251
+ }
252
+ if (getInheritedEditorMode() === EDITOR_MODE__VIEW) {
253
+ return;
254
+ }
180
255
  if (canUser && !canUser(ADD)) {
181
256
  showPermissionsError(ADD);
182
257
  return;
@@ -295,7 +370,23 @@ export default function withEditor(WrappedComponent, isTree = false) {
295
370
  setIsEditorShown(true);
296
371
  },
297
372
  doEdit = async () => {
373
+ if (getIsEditorDisabledByParent()) {
374
+ return;
375
+ }
376
+ const inheritedEditorMode = getInheritedEditorMode();
377
+ if (inheritedEditorMode === EDITOR_MODE__VIEW) {
378
+ await doView(false);
379
+ return;
380
+ }
381
+ if (inheritedEditorMode === EDITOR_MODE__EDIT && (!userCanEdit || disableEdit || canEditorViewOnly)) {
382
+ await showViewFallback();
383
+ return;
384
+ }
298
385
  if (canUser && !canUser(EDIT)) {
386
+ if (inheritedEditorMode === EDITOR_MODE__EDIT) {
387
+ await showViewFallback();
388
+ return;
389
+ }
299
390
  showPermissionsError(EDIT);
300
391
  return;
301
392
  }
@@ -317,6 +408,12 @@ export default function withEditor(WrappedComponent, isTree = false) {
317
408
  setIsEditorShown(true);
318
409
  },
319
410
  doDelete = async (args) => {
411
+ if (getIsEditorDisabledByParent()) {
412
+ return;
413
+ }
414
+ if (getInheritedEditorMode() === EDITOR_MODE__VIEW) {
415
+ return;
416
+ }
320
417
  if (canUser && !canUser(DELETE)) {
321
418
  showPermissionsError(DELETE);
322
419
  return;
@@ -430,6 +527,17 @@ export default function withEditor(WrappedComponent, isTree = false) {
430
527
  }
431
528
  },
432
529
  doView = async (allowEditing = false) => {
530
+ if (getIsEditorDisabledByParent()) {
531
+ return;
532
+ }
533
+ const inheritedEditorMode = getInheritedEditorMode();
534
+ if (inheritedEditorMode === EDITOR_MODE__EDIT) {
535
+ await doEdit();
536
+ return;
537
+ }
538
+ if (inheritedEditorMode === EDITOR_MODE__VIEW) {
539
+ allowEditing = false;
540
+ }
433
541
  if (!userCanView) {
434
542
  return;
435
543
  }
@@ -460,6 +568,12 @@ export default function withEditor(WrappedComponent, isTree = false) {
460
568
  }
461
569
  },
462
570
  doDuplicate = async () => {
571
+ if (getIsEditorDisabledByParent()) {
572
+ return;
573
+ }
574
+ if (getInheritedEditorMode() === EDITOR_MODE__VIEW) {
575
+ return;
576
+ }
463
577
  if (!userCanEdit || disableDuplicate) {
464
578
  return;
465
579
  }
@@ -533,6 +647,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
533
647
  }
534
648
  },
535
649
  doEditorSave = async (data, e) => {
650
+ if (getIsEditorDisabledByParent()) {
651
+ return false;
652
+ }
536
653
  let mode = getEditorMode() === EDITOR_MODE__ADD ? ADD : EDIT;
537
654
  if (canUser && !canUser(mode)) {
538
655
  showPermissionsError(mode);
@@ -651,6 +768,12 @@ export default function withEditor(WrappedComponent, isTree = false) {
651
768
  setIsEditorShown(false);
652
769
  },
653
770
  doEditorDelete = async () => {
771
+ if (getIsEditorDisabledByParent()) {
772
+ return;
773
+ }
774
+ if (getInheritedEditorMode() === EDITOR_MODE__VIEW) {
775
+ return;
776
+ }
654
777
  if (canUser && !canUser(DELETE)) {
655
778
  showPermissionsError(DELETE);
656
779
  return;
@@ -662,6 +785,46 @@ export default function withEditor(WrappedComponent, isTree = false) {
662
785
  });
663
786
  },
664
787
  calculateEditorMode = () => {
788
+ // Calculate the editor's effective mode based on parent inheritance, permissions, and local selection state.
789
+ // Priority order:
790
+ // 1. If parent is save-locked (unsaved ancestor), force VIEW mode
791
+ // 2. If parent forces VIEW mode via inheritance, return VIEW (child cannot edit if parent is view-only)
792
+ // 3. If parent forces EDIT mode via inheritance, check child permissions:
793
+ // a. If parent disabled, child disabled, or child cannot edit, return VIEW
794
+ // b. If single phantom record, return ADD (new record being created)
795
+ // c. Otherwise return EDIT or VIEW based on selection count
796
+ // 4. Fall back to local selection heuristics (multiple→EDIT, single→VIEW, stays in previous mode if configured)
797
+ const
798
+ selection = getSelection(),
799
+ inheritedEditorMode = getInheritedEditorMode();
800
+
801
+ if (getIsEditorDisabledByParent()) {
802
+ return EDITOR_MODE__VIEW;
803
+ }
804
+
805
+ if (inheritedEditorMode === EDITOR_MODE__VIEW) {
806
+ return EDITOR_MODE__VIEW;
807
+ }
808
+
809
+ if (inheritedEditorMode === EDITOR_MODE__EDIT) {
810
+ if (!getCanEditorBeInEditMode()) {
811
+ return EDITOR_MODE__VIEW;
812
+ }
813
+ if (canEditorViewOnly || !userCanEdit || disableEdit) {
814
+ return EDITOR_MODE__VIEW;
815
+ }
816
+ if (canUser && !canUser(EDIT)) {
817
+ return EDITOR_MODE__VIEW;
818
+ }
819
+ if (canRecordBeEdited && canRecordBeEdited(selection) === false) {
820
+ return EDITOR_MODE__VIEW;
821
+ }
822
+ if (selection.length === 1 && !selection[0].isDestroyed && (selection[0].isPhantom || selection[0].isRemotePhantom) && !disableAdd) {
823
+ return EDITOR_MODE__ADD;
824
+ }
825
+ return selection.length ? EDITOR_MODE__EDIT : EDITOR_MODE__VIEW;
826
+ }
827
+
665
828
  if (!getCanEditorBeInEditMode()) { // this is a result of canRecordBeEdited returning false
666
829
  return EDITOR_MODE__VIEW;
667
830
  }
@@ -677,7 +840,6 @@ export default function withEditor(WrappedComponent, isTree = false) {
677
840
  }
678
841
 
679
842
  // calculateEditorMode gets called only on selection changes
680
- const selection = getSelection();
681
843
  let mode;
682
844
  if (editorType === EDITOR_TYPE__SIDE && !_.isNil(UiGlobals.isSideEditorAlwaysEditMode) && UiGlobals.isSideEditorAlwaysEditMode) {
683
845
  // special case: side editor is always edit mode
@@ -705,6 +867,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
705
867
  return mode;
706
868
  },
707
869
  setEditMode = () => {
870
+ if (getIsEditorDisabledByParent() || getIsEditorModeControlledByParent()) {
871
+ return;
872
+ }
708
873
  if (canUser && !canUser(EDIT)) {
709
874
  showPermissionsError(EDIT);
710
875
  return;
@@ -713,6 +878,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
713
878
  setEditorMode(EDITOR_MODE__EDIT);
714
879
  },
715
880
  setViewMode = () => {
881
+ if (getIsEditorDisabledByParent() || getIsEditorModeControlledByParent()) {
882
+ return;
883
+ }
716
884
  if (canUser && !canUser(VIEW)) {
717
885
  showPermissionsError(VIEW);
718
886
  return;
@@ -729,10 +897,20 @@ export default function withEditor(WrappedComponent, isTree = false) {
729
897
  }
730
898
  };
731
899
 
900
+ const
901
+ inheritedEditorMode = getInheritedEditorMode(),
902
+ isEditorDisabledByParent = getIsEditorDisabledByParent(),
903
+ isEditorModeControlledByParent = getIsEditorModeControlledByParent(),
904
+ isCurrentSelectionUnsaved = getIsCurrentSelectionUnsaved(),
905
+ isAnyAncestorUnsaved = getIsParentSaveLocked() || isCurrentSelectionUnsaved,
906
+ isCrudBlockedByInheritedView = inheritedEditorMode === EDITOR_MODE__VIEW;
907
+
732
908
  useEffect(() => {
733
909
 
734
910
  if (editorType === EDITOR_TYPE__SIDE) {
735
- if (selection?.length) { // || isAdding
911
+ if (isEditorDisabledByParent) {
912
+ setIsEditorShown(false);
913
+ } else if (selection?.length) { // || isAdding
736
914
  // there is a selection, so show the editor
737
915
  setIsEditorShown(true);
738
916
  } else {
@@ -746,6 +924,19 @@ export default function withEditor(WrappedComponent, isTree = false) {
746
924
  } else {
747
925
  setCanEditorBeInEditMode(true);
748
926
  }
927
+
928
+ if (isEditorDisabledByParent || inheritedEditorMode === EDITOR_MODE__VIEW) {
929
+ setIsEditorViewOnly(true);
930
+ } else if (inheritedEditorMode === EDITOR_MODE__EDIT) {
931
+ const canEditInInheritedMode =
932
+ !canEditorViewOnly &&
933
+ userCanEdit &&
934
+ !disableEdit &&
935
+ (!canUser || canUser(EDIT)) &&
936
+ (!canRecordBeEdited || canRecordBeEdited(selection));
937
+ setIsEditorViewOnly(!canEditInInheritedMode);
938
+ }
939
+
749
940
  setEditorMode(calculateEditorMode());
750
941
  setLastSelection(selection);
751
942
 
@@ -755,7 +946,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
755
946
  Promise.resolve().then(() => {
756
947
  setIsIgnoreNextSelectionChange(false);
757
948
  });
758
- }, [selection]);
949
+ }, [selection, inheritedEditorMode, isEditorDisabledByParent]);
759
950
 
760
951
  if (self) {
761
952
  self.add = doAdd;
@@ -776,7 +967,13 @@ export default function withEditor(WrappedComponent, isTree = false) {
776
967
  setEditorMode(calculateEditorMode());
777
968
  }
778
969
 
779
- return <WrappedComponent
970
+ const editorModeContextValue = {
971
+ effectiveEditorMode: getEditorMode(),
972
+ isAnyAncestorUnsaved,
973
+ };
974
+
975
+ return <EditorModeContext.Provider value={editorModeContextValue}>
976
+ <WrappedComponent
780
977
  {...props}
781
978
  ref={ref}
782
979
  disableWithEditor={false}
@@ -786,35 +983,38 @@ export default function withEditor(WrappedComponent, isTree = false) {
786
983
  isEditorShown={getIsEditorShown()}
787
984
  getIsEditorShown={getIsEditorShown}
788
985
  isEditorViewOnly={isEditorViewOnly}
986
+ isEditorModeControlledByParent={isEditorModeControlledByParent}
987
+ isEditorDisabledByParent={isEditorDisabledByParent}
789
988
  isAdding={isAdding}
790
989
  isSaving={isSaving}
791
990
  editorMode={getEditorMode()}
792
991
  getEditorMode={getEditorMode}
793
- onEditMode={setEditMode}
794
- onViewMode={setViewMode}
992
+ onEditMode={(isEditorModeControlledByParent || isEditorDisabledByParent) ? null : setEditMode}
993
+ onViewMode={(isEditorModeControlledByParent || isEditorDisabledByParent) ? null : setViewMode}
795
994
  editorStateRef={editorStateRef}
796
995
  setIsEditorShown={setIsEditorShown}
797
996
  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}
997
+ onAdd={(isEditorDisabledByParent || isCrudBlockedByInheritedView || !userCanEdit || disableAdd) ? null : doAdd}
998
+ onEdit={(isEditorDisabledByParent || isCrudBlockedByInheritedView || !userCanEdit || disableEdit || (canRecordBeEdited && !canRecordBeEdited(selection))) ? null : doEdit}
999
+ onDelete={(isEditorDisabledByParent || isCrudBlockedByInheritedView || !userCanEdit || disableDelete || (canRecordBeDeleted && !canRecordBeDeleted(selection))) ? null : doDelete}
1000
+ onView={isEditorDisabledByParent ? null : doView}
1001
+ onDuplicate={(isEditorDisabledByParent || isCrudBlockedByInheritedView) ? null : doDuplicate}
803
1002
  onEditorSave={doEditorSave}
804
1003
  onEditorCancel={doEditorCancel}
805
- onEditorDelete={(!userCanEdit || disableDelete) ? null : doEditorDelete}
1004
+ onEditorDelete={(isEditorDisabledByParent || isCrudBlockedByInheritedView || !userCanEdit || disableDelete) ? null : doEditorDelete}
806
1005
  onEditorClose={doEditorClose}
807
1006
  setWithEditListeners={setListeners}
808
1007
  isEditor={true}
809
1008
  userCanEdit={userCanEdit}
810
1009
  userCanView={userCanView}
811
- disableAdd={disableAdd}
812
- disableEdit={disableEdit}
813
- disableDelete={disableDelete}
814
- disableDuplicate={disableDuplicate}
815
- disableView ={disableView}
1010
+ disableAdd={disableAdd || isEditorDisabledByParent || isCrudBlockedByInheritedView}
1011
+ disableEdit={disableEdit || isEditorDisabledByParent || isCrudBlockedByInheritedView}
1012
+ disableDelete={disableDelete || isEditorDisabledByParent || isCrudBlockedByInheritedView}
1013
+ disableDuplicate={disableDuplicate || isEditorDisabledByParent || isCrudBlockedByInheritedView}
1014
+ disableView ={disableView || isEditorDisabledByParent}
816
1015
  setSelection={setSelectionDecorated}
817
1016
  isTree={isTree}
818
- />;
1017
+ />
1018
+ </EditorModeContext.Provider>;
819
1019
  });
820
1020
  }
@@ -72,6 +72,7 @@ function Viewer(props) {
72
72
  onEditMode,
73
73
  onClose,
74
74
  onDelete,
75
+ isEditorModeControlledByParent = false,
75
76
 
76
77
  // parent container
77
78
  selectorId,
@@ -563,7 +564,7 @@ function Viewer(props) {
563
564
 
564
565
  <Toolbar className="justify-end">
565
566
  <HStack className="flex-1 items-center">
566
- <Text className="text-[20px] ml-1 text-grey-500">View Mode</Text>
567
+ <Text className="text-[20px] ml-1 text-grey-500">{isEditorModeControlledByParent ? 'View Mode (Inherited)' : 'View Mode'}</Text>
567
568
  </HStack>
568
569
  {onEditMode && (!canUser || canUser(EDIT)) &&
569
570
  <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';