@onehat/ui 0.4.61 → 0.4.64

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.61",
3
+ "version": "0.4.64",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -0,0 +1,108 @@
1
+ import { useCallback } from 'react';
2
+ import {
3
+ Fab, FabIcon, FabLabel,
4
+ VStack,
5
+ } from '@project-components/Gluestack';
6
+ import Animated, {
7
+ useSharedValue,
8
+ useAnimatedStyle,
9
+ withTiming,
10
+ } from 'react-native-reanimated';
11
+ import IconButton from '../Buttons/IconButton.js';
12
+ import FabWithTooltip from './FabWithTooltip.js';
13
+ import EllipsisVertical from '../Icons/EllipsisVertical.js';
14
+ import Xmark from '../Icons/Xmark.js';
15
+
16
+ // This component creates a floating action button (FAB)
17
+ // that can expand and collapse to show multiple buttons beneath it.
18
+
19
+ export default function DynamicFab(props) {
20
+ const {
21
+ icon,
22
+ buttons, // to show when expanded
23
+ label,
24
+ tooltip,
25
+ tooltipPlacement = 'left',
26
+ tooltipClassName,
27
+ tooltipTriggerClassName,
28
+ collapseOnPress = true,
29
+ } = props,
30
+ isExpanded = useSharedValue(0),
31
+ toggleFab = useCallback(() => {
32
+ isExpanded.value = isExpanded.value ? 0 : 1;
33
+ }, []),
34
+ buttonSpacing = 45,
35
+ verticalOffset = 50; // to shift the entire expanded group up
36
+
37
+ let className = `
38
+ DynamicFab
39
+ fixed
40
+ pb-[20px]
41
+ bottom-4
42
+ right-4
43
+ `;
44
+ if (props.className) {
45
+ className += ` ${props.className}`;
46
+ }
47
+
48
+ return <VStack className={className}>
49
+ {buttons
50
+ .slice() // clone, so we don't mutate the original array
51
+ .reverse() // so buttons appear in the correct order
52
+ .map((btnConfig, ix) => {
53
+ const {
54
+ tooltipPlacement = 'left',
55
+ onPress,
56
+ key,
57
+ ...btnConfigToPass
58
+ } = btnConfig,
59
+ animatedStyle = useAnimatedStyle(() => {
60
+ return {
61
+ opacity: withTiming(isExpanded.value, { duration: 200 }),
62
+ pointerEvents: isExpanded.value ? 'auto' : 'none', // Disable interaction when collapsed
63
+ };
64
+ });
65
+
66
+ return <Animated.View
67
+ key={ix}
68
+ style={[
69
+ animatedStyle,
70
+ {
71
+ position: 'absolute',
72
+ bottom: buttonSpacing * (ix + 1) + verticalOffset, // Static vertical positioning
73
+ right: 0,
74
+ },
75
+ ]}
76
+ >
77
+ <IconButton
78
+ className={`
79
+ bg-primary-600
80
+ text-white
81
+ hover:bg-primary-700
82
+ active:bg-primary-800
83
+ `}
84
+ tooltipPlacement={tooltipPlacement}
85
+ onPress={() => {
86
+ onPress();
87
+ if (collapseOnPress) {
88
+ isExpanded.value = 0;
89
+ }
90
+ }}
91
+ {...btnConfigToPass}
92
+ />
93
+ </Animated.View>;
94
+ })}
95
+ <FabWithTooltip
96
+ size="lg"
97
+ onPress={toggleFab}
98
+ className="z-100 bg-primary-600"
99
+ tooltip={tooltip}
100
+ tooltipPlacement={tooltipPlacement}
101
+ tooltipClassName={tooltipClassName}
102
+ tooltipTriggerClassName={tooltipTriggerClassName}
103
+ >
104
+ <FabIcon as={isExpanded.value ? Xmark : icon || EllipsisVertical} />
105
+ {label ? <FabLabel>{label}</FabLabel> : null}
106
+ </FabWithTooltip>
107
+ </VStack>;
108
+ };
@@ -0,0 +1,6 @@
1
+ import {
2
+ Fab,
3
+ } from '@project-components/Gluestack';
4
+ import withTooltip from '../Hoc/withTooltip.js';
5
+
6
+ export default withTooltip(Fab);
@@ -659,7 +659,7 @@ export const ComboComponent = forwardRef((props, ref) => {
659
659
  placeholder={placeholder}
660
660
  tooltip={tooltip}
661
661
  tooltipPlacement={tooltipPlacement}
662
- tooltipClassName={`
662
+ tooltipTriggerClassName={`
663
663
  grow
664
664
  h-auto
665
665
  self-stretch
@@ -901,7 +901,7 @@ export const ComboComponent = forwardRef((props, ref) => {
901
901
  placeholder={placeholder}
902
902
  tooltip={tooltip}
903
903
  tooltipPlacement={tooltipPlacement}
904
- tooltipClassName={`
904
+ tooltipTriggerClassName={`
905
905
  grow
906
906
  h-full
907
907
  flex-1
@@ -1014,7 +1014,7 @@ export const ComboComponent = forwardRef((props, ref) => {
1014
1014
  placeholder={placeholder}
1015
1015
  tooltip={tooltip}
1016
1016
  tooltipPlacement={tooltipPlacement}
1017
- tooltipClassName={`
1017
+ tooltipTriggerClassName={`
1018
1018
  h-full
1019
1019
  flex-1
1020
1020
  `}
@@ -366,7 +366,7 @@ export const DateElement = forwardRef((props, ref) => {
366
366
  isDisabled={isDisabled}
367
367
  tooltip={tooltip}
368
368
  tooltipPlacement={tooltipPlacement}
369
- tooltipClassName={`
369
+ tooltipTriggerClassName={`
370
370
  flex-1
371
371
  h-full
372
372
  `}
@@ -164,7 +164,10 @@ function NumberElement(props) {
164
164
  isDisabled={isDisabled}
165
165
  tooltip={tooltip}
166
166
  tooltipPlacement={tooltipPlacement}
167
- tooltipClassName="flex-1"
167
+ tooltipTriggerClassName={`
168
+ flex-1
169
+ h-full
170
+ `}
168
171
  className={`
169
172
  h-full
170
173
  text-center
@@ -61,8 +61,8 @@ function TagComponent(props) {
61
61
  await repository.waitUntilDoneLoading();
62
62
  }
63
63
  let record = repository.getById(id); // first try to get from entities in memory
64
- if (!record && repository.getSingleEntityFromServer) {
65
- record = await repository.getSingleEntityFromServer(id);
64
+ if (!record && repository.loadOneAdditionalEntity) {
65
+ record = await repository.loadOneAdditionalEntity(id);
66
66
  }
67
67
 
68
68
  if (!record) {
@@ -1,4 +1,4 @@
1
- import { useEffect, useState, useRef, isValidElement, } from 'react';
1
+ import { useEffect, useCallback, useState, useRef, isValidElement, } from 'react';
2
2
  import {
3
3
  Box,
4
4
  HStack,
@@ -8,6 +8,11 @@ import {
8
8
  VStack,
9
9
  VStackNative,
10
10
  } from '@project-components/Gluestack';
11
+ import Animated, {
12
+ useSharedValue,
13
+ useAnimatedStyle,
14
+ withTiming,
15
+ } from 'react-native-reanimated';
11
16
  import {
12
17
  VIEW,
13
18
  } from '../../Constants/Commands.js';
@@ -40,7 +45,7 @@ import testProps from '../../Functions/testProps.js';
40
45
  import Toolbar from '../Toolbar/Toolbar.js';
41
46
  import Button from '../Buttons/Button.js';
42
47
  import IconButton from '../Buttons/IconButton.js';
43
- import DynamicFab from '../Buttons/DynamicFab.js';
48
+ import DynamicFab from '../Fab/DynamicFab.js';
44
49
  import AngleLeft from '../Icons/AngleLeft.js';
45
50
  import Eye from '../Icons/Eye.js';
46
51
  import Rotate from '../Icons/Rotate.js';
@@ -71,12 +76,16 @@ import _ from 'lodash';
71
76
  // EDITOR_TYPE__PLAIN
72
77
  // Form is embedded on screen in some other way. Mainly use startingValues, items, validator
73
78
 
79
+ const FAB_FADE_TIME = 300; // ms
80
+
74
81
  function Form(props) {
75
82
  const {
76
83
  editorType = EDITOR_TYPE__WINDOWED, // EDITOR_TYPE__INLINE | EDITOR_TYPE__WINDOWED | EDITOR_TYPE__SIDE | EDITOR_TYPE__SMART | EDITOR_TYPE__PLAIN
77
84
  startingValues = {},
78
85
  items = [], // Columns, FieldSets, Fields, etc to define the form
86
+ isItemsCustomLayout = false,
79
87
  ancillaryItems = [], // additional items which are not controllable form elements, but should appear in the form
88
+ showAncillaryButtons = false,
80
89
  columnDefaults = {}, // defaults for each Column defined in items (above)
81
90
  columnsConfig, // Which columns are shown in Grid, so the inline editor can match. Used only for EDITOR_TYPE__INLINE
82
91
  validator, // custom validator, mainly for EDITOR_TYPE__PLAIN
@@ -140,7 +149,13 @@ function Form(props) {
140
149
  } = props,
141
150
  formRef = useRef(),
142
151
  ancillaryItemsRef = useRef({}),
143
- ancillaryFabs = useRef([]),
152
+ ancillaryButtons = useRef([]),
153
+ setAncillaryButtons = (array) => {
154
+ ancillaryButtons.current = array;
155
+ },
156
+ getAncillaryButtons = () => {
157
+ return ancillaryButtons.current;
158
+ },
144
159
  styles = UiGlobals.styles,
145
160
  record = props.record?.length === 1 ? props.record[0] : props.record;
146
161
 
@@ -158,6 +173,14 @@ function Form(props) {
158
173
  forceUpdate = useForceUpdate(),
159
174
  [previousRecord, setPreviousRecord] = useState(record),
160
175
  [containerWidth, setContainerWidth] = useState(),
176
+ [isFabVisible, setIsFabVisible] = useState(false),
177
+ fabOpacity = useSharedValue(0),
178
+ fabAnimatedStyle = useAnimatedStyle(() => {
179
+ return {
180
+ opacity: withTiming(fabOpacity.value, { duration: FAB_FADE_TIME }), // Smooth fade animation
181
+ pointerEvents: fabOpacity.value > 0 ? 'auto' : 'none', // Disable interaction when invisible
182
+ };
183
+ }),
161
184
  initialValues = _.merge(startingValues, (record && !record.isDestroyed ? record.submitValues : {})),
162
185
  defaultValues = isMultiple ? getNullFieldValues(initialValues, Repository) : initialValues, // when multiple entities, set all default values to null
163
186
  validatorToUse = validator || (isMultiple ? disableRequiredYupFields(Repository?.schema?.model?.validator) : Repository?.schema?.model?.validator) || yup.object(),
@@ -902,13 +925,15 @@ function Form(props) {
902
925
  },
903
926
  buildAncillary = () => {
904
927
  const components = [];
905
- ancillaryFabs.current = [];
928
+ setAncillaryButtons([]);
906
929
  if (ancillaryItems.length) {
907
930
 
908
931
  // add the "scroll to top" button
909
- ancillaryFabs.current.push({
932
+ getAncillaryButtons().push({
910
933
  icon: ArrowUp,
934
+ reference: 'scrollToTop',
911
935
  onPress: () => scrollToAncillaryItem(0),
936
+ tooltip: 'Scroll to top',
912
937
  });
913
938
 
914
939
  _.each(ancillaryItems, (item, ix) => {
@@ -925,11 +950,12 @@ function Form(props) {
925
950
  return;
926
951
  }
927
952
  if (icon) {
928
- // TODO: this assumes that if one Ancillary item has an icon, they all do.
929
- // If they don't, the ix will be wrong.
930
- ancillaryFabs.current.push({
953
+ // NOTE: this assumes that if one Ancillary item has an icon, they all do.
954
+ // If they don't, the ix will be wrong!
955
+ getAncillaryButtons().push({
931
956
  icon,
932
957
  onPress: () => scrollToAncillaryItem(ix +1), // offset for the "scroll to top" button
958
+ tooltip: title,
933
959
  });
934
960
  }
935
961
  if (type.match(/Grid/) && !itemPropsToPass.h) {
@@ -959,7 +985,7 @@ function Form(props) {
959
985
  `}
960
986
  >{title}</Text>;
961
987
  if (icon) {
962
- title = <HStack className="items-center"><Icon as={icon} size="lg" className="mr-2" />{title}</HStack>
988
+ title = <HStack className="items-center"><Icon as={icon} className="w-[32px] h-[32px] mr-2" />{title}</HStack>
963
989
  }
964
990
  }
965
991
  if (description) {
@@ -988,12 +1014,6 @@ function Form(props) {
988
1014
  }
989
1015
  return components;
990
1016
  },
991
- scrollToAncillaryItem = (ix) => {
992
- ancillaryItemsRef.current[ix]?.scrollIntoView({
993
- behavior: 'smooth',
994
- block: 'start',
995
- });
996
- },
997
1017
  onSubmitError = (errors, e) => {
998
1018
  if (editorType === EDITOR_TYPE__INLINE) {
999
1019
  alert(errors.message);
@@ -1026,7 +1046,31 @@ function Form(props) {
1026
1046
  }
1027
1047
 
1028
1048
  setContainerWidth(e.nativeEvent.layout.width);
1029
- };
1049
+ },
1050
+ scrollToAncillaryItem = (ix) => {
1051
+ ancillaryItemsRef.current[ix]?.scrollIntoView({
1052
+ behavior: 'smooth',
1053
+ block: 'start',
1054
+ });
1055
+ },
1056
+ onScroll = useCallback(
1057
+ _.debounce((e) => {
1058
+ if (!showAncillaryButtons) {
1059
+ return;
1060
+ }
1061
+ const
1062
+ scrollY = e.nativeEvent.contentOffset.y,
1063
+ isFabVisible = scrollY > 50;
1064
+ fabOpacity.value = isFabVisible ? 1 : 0;
1065
+ if (isFabVisible) {
1066
+ setIsFabVisible(true);
1067
+ } else {
1068
+ // delay removal from DOM until fade-out is complete
1069
+ setTimeout(() => setIsFabVisible(isFabVisible), FAB_FADE_TIME);
1070
+ }
1071
+ }, 100), // delay
1072
+ []
1073
+ );
1030
1074
 
1031
1075
  useEffect(() => {
1032
1076
  if (skipAll) {
@@ -1111,8 +1155,9 @@ function Form(props) {
1111
1155
  style.maxHeight = maxHeight;
1112
1156
  }
1113
1157
 
1114
- const formButtons = [];
1115
1158
  let modeHeader = null,
1159
+ formButtons = null,
1160
+ scrollButtons = null,
1116
1161
  footer = null,
1117
1162
  footerButtons = null,
1118
1163
  formComponents,
@@ -1143,8 +1188,8 @@ function Form(props) {
1143
1188
  formComponents = buildFromItems();
1144
1189
  const formAncillaryComponents = buildAncillary();
1145
1190
  editor = <>
1146
- {containerWidth >= styles.FORM_ONE_COLUMN_THRESHOLD ? <HStack className="Form-formComponents-HStack p-4 gap-4 justify-center">{formComponents}</HStack> : null}
1147
- {containerWidth < styles.FORM_ONE_COLUMN_THRESHOLD ? <VStack className="Form-formComponents-VStack p-4">{formComponents}</VStack> : null}
1191
+ {containerWidth >= styles.FORM_ONE_COLUMN_THRESHOLD && !isItemsCustomLayout ? <HStack className="Form-formComponents-HStack p-4 gap-4 justify-center">{formComponents}</HStack> : null}
1192
+ {containerWidth < styles.FORM_ONE_COLUMN_THRESHOLD || isItemsCustomLayout ? <VStack className="Form-formComponents-VStack p-4">{formComponents}</VStack> : null}
1148
1193
  {formAncillaryComponents.length ? <VStack className="Form-AncillaryComponents m-2 pt-4 px-2">{formAncillaryComponents}</VStack> : null}
1149
1194
  </>;
1150
1195
 
@@ -1188,16 +1233,19 @@ function Form(props) {
1188
1233
  </Toolbar>;
1189
1234
  }
1190
1235
  if (getEditorMode() === EDITOR_MODE__EDIT && !_.isEmpty(additionalButtons)) {
1191
- formButtons.push(<Toolbar key="additionalButtonsToolbar" className="justify-end flex-wrap gap-2">
1236
+ formButtons = <Toolbar className="justify-end flex-wrap gap-2">
1192
1237
  {additionalButtons}
1193
- </Toolbar>)
1238
+ </Toolbar>;
1194
1239
  }
1195
- if (!_.isEmpty(ancillaryFabs.current)) {
1196
- fab = <DynamicFab
1197
- fabs={ancillaryFabs.current}
1240
+ if (showAncillaryButtons && !_.isEmpty(getAncillaryButtons())) {
1241
+ fab = <Animated.View style={fabAnimatedStyle}>
1242
+ <DynamicFab
1243
+ buttons={getAncillaryButtons()}
1198
1244
  collapseOnPress={false}
1199
1245
  className="bottom-[55px]"
1200
- />;
1246
+ tooltip="Scroll to Ancillary Item"
1247
+ />
1248
+ </Animated.View>;
1201
1249
  }
1202
1250
  }
1203
1251
 
@@ -1412,6 +1460,8 @@ function Form(props) {
1412
1460
  pb-1
1413
1461
  web:min-h-[${minHeight}px]
1414
1462
  `}
1463
+ onScroll={onScroll}
1464
+ scrollEventThrottle={16 /* ms */}
1415
1465
  contentContainerStyle={{
1416
1466
  // height: '100%',
1417
1467
  }}
@@ -1420,11 +1470,16 @@ function Form(props) {
1420
1470
  {modeHeader}
1421
1471
  {formHeader}
1422
1472
  {formButtons}
1473
+ {showAncillaryButtons && !_.isEmpty(getAncillaryButtons()) &&
1474
+ <Toolbar className="justify-start flex-wrap gap-2">
1475
+ <Text>Scroll:</Text>
1476
+ {buildAdditionalButtons(_.omitBy(getAncillaryButtons(), (btnConfig) => btnConfig.reference === 'scrollToTop'))}
1477
+ </Toolbar>}
1423
1478
  {editor}
1424
1479
  </ScrollView>}
1425
1480
 
1426
1481
  {footer}
1427
- {fab}
1482
+ {isFabVisible && fab}
1428
1483
 
1429
1484
  </>}
1430
1485
  </VStackNative>;
@@ -99,6 +99,22 @@ export default function withData(WrappedComponent) {
99
99
 
100
100
  }, []);
101
101
 
102
+ useEffect(() => {
103
+ if (!baseParams || !LocalRepository) {
104
+ return;
105
+ }
106
+
107
+ // If baseParams changes, re-load the Repository
108
+ if (LocalRepository.isLoaded && !_.isEqual(LocalRepository.getBaseParams(), baseParams)) {
109
+ LocalRepository.setBaseParams(baseParams);
110
+
111
+ if (LocalRepository.isRemote && !LocalRepository.isLoading) {
112
+ LocalRepository.load();
113
+ }
114
+ }
115
+
116
+ }, [baseParams, LocalRepository]);
117
+
102
118
  if (!isReady) {
103
119
  return null;
104
120
  }
@@ -29,6 +29,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
29
29
  userCanEdit = true, // not permissions, but capability
30
30
  userCanView = true,
31
31
  canEditorViewOnly = false, // whether the editor can *ever* change state out of 'View' mode
32
+ canProceedWithCrud, // fn returns bool on if the CRUD operation can proceed
32
33
  disableAdd = false,
33
34
  disableEdit = false,
34
35
  disableDelete = false,
@@ -50,6 +51,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
50
51
  newEntityDisplayValue,
51
52
  newEntityDisplayProperty, // in case the field to set for newEntityDisplayValue is different from model
52
53
  defaultValues,
54
+ initialEditorMode = EDITOR_MODE__VIEW,
53
55
  stayInEditModeOnSelectionChange = false,
54
56
 
55
57
  // withComponent
@@ -81,7 +83,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
81
83
  listeners = useRef({}),
82
84
  editorStateRef = useRef(),
83
85
  newEntityDisplayValueRef = useRef(),
84
- editorModeRef = useRef(EDITOR_MODE__VIEW),
86
+ editorModeRef = useRef(initialEditorMode),
85
87
  isIgnoreNextSelectionChangeRef = useRef(false),
86
88
  [currentRecord, setCurrentRecord] = useState(null),
87
89
  [isAdding, setIsAdding] = useState(false),
@@ -152,6 +154,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
152
154
  showPermissionsError(ADD);
153
155
  return;
154
156
  }
157
+ if (canProceedWithCrud && !canProceedWithCrud()) {
158
+ return;
159
+ }
155
160
 
156
161
  const selection = getSelection();
157
162
  let addValues = values;
@@ -251,6 +256,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
251
256
  showPermissionsError(EDIT);
252
257
  return;
253
258
  }
259
+ if (canProceedWithCrud && !canProceedWithCrud()) {
260
+ return;
261
+ }
254
262
  const selection = getSelection();
255
263
  if (_.isEmpty(selection) || (_.isArray(selection) && (selection.length > 1 || selection[0]?.isDestroyed))) {
256
264
  return;
@@ -270,6 +278,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
270
278
  showPermissionsError(DELETE);
271
279
  return;
272
280
  }
281
+ if (canProceedWithCrud && !canProceedWithCrud()) {
282
+ return;
283
+ }
273
284
  let cb = null;
274
285
  if (_.isFunction(args)) {
275
286
  cb = args;
@@ -367,6 +378,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
367
378
  showPermissionsError(VIEW);
368
379
  return;
369
380
  }
381
+ if (canProceedWithCrud && !canProceedWithCrud()) {
382
+ return;
383
+ }
370
384
  if (editorType === EDITOR_TYPE__INLINE) {
371
385
  alert('Cannot view in inline editor.');
372
386
  return; // inline editor doesn't have a view mode
@@ -394,6 +408,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
394
408
  showPermissionsError(DUPLICATE);
395
409
  return;
396
410
  }
411
+ if (canProceedWithCrud && !canProceedWithCrud()) {
412
+ return;
413
+ }
397
414
 
398
415
  const selection = getSelection();
399
416
  if (selection.length !== 1) {
@@ -8,6 +8,7 @@ import {
8
8
  DUPLICATE,
9
9
  PRINT,
10
10
  UPLOAD_DOWNLOAD,
11
+ DOWNLOAD,
11
12
  } from '../../Constants/Commands.js';
12
13
  import Clipboard from '../Icons/Clipboard.js';
13
14
  import Duplicate from '../Icons/Duplicate.js';
@@ -17,6 +18,7 @@ import Trash from '../Icons/Trash.js';
17
18
  import Plus from '../Icons/Plus.js';
18
19
  import Print from '../Icons/Print.js';
19
20
  import UploadDownload from '../Icons/UploadDownload.js';
21
+ import Download from '../Icons/Download.js';
20
22
  import inArray from '../../Functions/inArray.js';
21
23
  import UploadsDownloadsWindow from '../Window/UploadsDownloadsWindow.js';
22
24
  import _ from 'lodash';
@@ -33,6 +35,7 @@ const presetButtons = [
33
35
  DUPLICATE,
34
36
  // PRINT,
35
37
  UPLOAD_DOWNLOAD,
38
+ DOWNLOAD,
36
39
  ];
37
40
 
38
41
  export default function withPresetButtons(WrappedComponent, isGrid = false) {
@@ -48,6 +51,7 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
48
51
  contextMenuItems = [],
49
52
  additionalToolbarButtons = [],
50
53
  useUploadDownload = false,
54
+ useDownload = false,
51
55
  uploadHeaders,
52
56
  uploadParams,
53
57
  onUpload,
@@ -176,6 +180,13 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
176
180
  isDisabled = true;
177
181
  }
178
182
  break;
183
+ case DOWNLOAD:
184
+ if (!useDownload) {
185
+ isDisabled = true;
186
+ } else if (canUser && !(canUser(DOWNLOAD) || canUser(UPLOAD_DOWNLOAD))) { // check Permissions
187
+ isDisabled = true;
188
+ }
189
+ break;
179
190
  default:
180
191
  }
181
192
  return isDisabled;
@@ -209,7 +220,9 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
209
220
  case ADD:
210
221
  key = 'addBtn';
211
222
  text = 'Add';
212
- handler = (parent, e) => onAdd();
223
+ handler = (parent, e) => {
224
+ onAdd();
225
+ };
213
226
  icon = Plus;
214
227
  if (isNoSelectorSelected() ||
215
228
  (isTree && isEmptySelection())
@@ -220,7 +233,9 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
220
233
  case EDIT:
221
234
  key = 'editBtn';
222
235
  text = 'Edit';
223
- handler = (parent, e) => onEdit();
236
+ handler = (parent, e) => {
237
+ onEdit();
238
+ };
224
239
  icon = Edit;
225
240
  if (isNoSelectorSelected() ||
226
241
  isEmptySelection() ||
@@ -235,7 +250,9 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
235
250
  key = 'deleteBtn';
236
251
  text = 'Delete';
237
252
  handler = onDelete;
238
- handler = (parent, e) => onDelete();
253
+ handler = (parent, e) => {
254
+ onDelete();
255
+ };
239
256
  icon = Trash;
240
257
  if (isNoSelectorSelected() ||
241
258
  isEmptySelection() ||
@@ -280,7 +297,9 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
280
297
  case DUPLICATE:
281
298
  key = 'duplicateBtn';
282
299
  text = 'Duplicate';
283
- handler = (parent, e) => onDuplicate();
300
+ handler = (parent, e) => {
301
+ onDuplicate();
302
+ };
284
303
  icon = Duplicate;
285
304
  isDisabled = !selection.length || selection.length !== 1;
286
305
  if (isNoSelectorSelected() ||
@@ -302,6 +321,12 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
302
321
  handler = (parent, e) => onUploadDownload();
303
322
  icon = UploadDownload;
304
323
  break;
324
+ case DOWNLOAD:
325
+ key = 'downloadBtn';
326
+ text = 'Download';
327
+ handler = (parent, e) => onDownload();
328
+ icon = Download;
329
+ break;
305
330
  default:
306
331
  }
307
332
  return {
@@ -376,6 +401,20 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
376
401
  />,
377
402
  onCancel: hideModal,
378
403
  });
404
+ },
405
+ onDownload = () => {
406
+ showModal({
407
+ body: <UploadsDownloadsWindow
408
+ reference="downloads"
409
+ onClose={hideModal}
410
+ isDownloadOnly={true}
411
+ Repository={Repository}
412
+ columnsConfig={props.columnsConfig}
413
+ downloadHeaders={downloadHeaders}
414
+ downloadParams={downloadParams}
415
+ />,
416
+ onCancel: hideModal,
417
+ });
379
418
  };
380
419
  // onPrint = () => {
381
420
  // debugger;
@@ -11,6 +11,7 @@ export default function withTooltip(WrappedComponent) {
11
11
  tooltip,
12
12
  tooltipPlacement = 'bottom',
13
13
  tooltipClassName,
14
+ tooltipTriggerClassName,
14
15
  _tooltip = {},
15
16
  ...propsToPass
16
17
  } = props;
@@ -22,6 +23,7 @@ export default function withTooltip(WrappedComponent) {
22
23
  label={tooltip}
23
24
  placement={tooltipPlacement}
24
25
  className={tooltipClassName}
26
+ triggerClassName={tooltipTriggerClassName}
25
27
  {..._tooltip}
26
28
  >
27
29
  {component}
@@ -18,6 +18,7 @@ import {
18
18
  } from '../../Constants/ReportTypes.js';
19
19
  import Form from '../Form/Form.js';
20
20
  import withComponent from '../Hoc/withComponent.js';
21
+ import withAlert from '../Hoc/withAlert.js';
21
22
  import testProps from '../../Functions/testProps.js';
22
23
  import ChartLine from '../Icons/ChartLine.js';
23
24
  import Pdf from '../Icons/Pdf.js';
@@ -36,8 +37,13 @@ function Report(props) {
36
37
  disablePdf = false,
37
38
  disableExcel = false,
38
39
  showReportHeaders = true,
40
+ alert,
39
41
  } = props,
40
- buttons = [];
42
+ buttons = [],
43
+ downloadReport = (args) => {
44
+ getReport(args);
45
+ alert('Download started');
46
+ };
41
47
 
42
48
  const propsIcon = props._icon || {};
43
49
  propsIcon.className = 'w-full h-full text-primary-500';
@@ -59,7 +65,7 @@ function Report(props) {
59
65
  key: 'excelBtn',
60
66
  text: 'Download Excel',
61
67
  icon: Excel,
62
- onPress: (data) => getReport({
68
+ onPress: (data) => downloadReport({
63
69
  reportId,
64
70
  data,
65
71
  reportType: REPORT_TYPES__EXCEL,
@@ -74,7 +80,7 @@ function Report(props) {
74
80
  key: 'pdfBtn',
75
81
  text: 'Download PDF',
76
82
  icon: Pdf,
77
- onPress: (data) => getReport({
83
+ onPress: (data) => downloadReport({
78
84
  reportId,
79
85
  data,
80
86
  reportType: REPORT_TYPES__PDF,
@@ -113,4 +119,4 @@ function Report(props) {
113
119
  </VStackNative>;
114
120
  }
115
121
 
116
- export default withComponent(Report);
122
+ export default withComponent(withAlert(Report));
@@ -398,13 +398,16 @@ function TabBar(props) {
398
398
  if (!tabs[currentTabIx]) {
399
399
  return null;
400
400
  }
401
- if (!tabs[currentTabIx].content && !tabs[currentTabIx].items) {
401
+
402
+ const currentTab = tabs[currentTabIx];
403
+ if (!currentTab.content && !currentTab.items) {
402
404
  return null;
403
405
  }
404
- if (tabs[currentTabIx].content) {
405
- return tabs[currentTabIx].content;
406
+
407
+ if (currentTab.content) {
408
+ return currentTab.content;
406
409
  }
407
- return _.map(tabs[currentTabIx].items, (item, ix) => {
410
+ return _.map(currentTab.items, (item, ix) => {
408
411
  return cloneElement(item, { key: ix });
409
412
  });
410
413
  };
@@ -462,6 +465,8 @@ function TabBar(props) {
462
465
  items-center
463
466
  justify-start
464
467
  py-2
468
+ overflow-x-hidden
469
+ overflow-y-auto
465
470
  ${styles.TAB_BAR_CLASSNAME}
466
471
  `}
467
472
  >
@@ -487,6 +492,8 @@ function TabBar(props) {
487
492
  ${'h-[' + tabHeight + 'px]'}
488
493
  items-center
489
494
  justify-start
495
+ overflow-x-auto
496
+ overflow-y-hidden
490
497
  p-1
491
498
  pb-0
492
499
  ${styles.TAB_BAR_CLASSNAME}
@@ -17,8 +17,8 @@ const TooltipElement = forwardRef((props, ref) => {
17
17
  }
18
18
 
19
19
  let triggerClassName = 'Tooltip-trigger';
20
- if (props.className) {
21
- triggerClassName += ' ' + props.className;
20
+ if (props.triggerClassName) {
21
+ triggerClassName += ' ' + props.triggerClassName;
22
22
  }
23
23
 
24
24
  return <Tooltip
@@ -1,4 +1,4 @@
1
- import { useEffect, useRef, useState, isValidElement, } from 'react';
1
+ import { useEffect, useCallback, useRef, useState, isValidElement, } from 'react';
2
2
  import {
3
3
  Box,
4
4
  HStack,
@@ -8,11 +8,17 @@ import {
8
8
  VStack,
9
9
  VStackNative,
10
10
  } from '@project-components/Gluestack';
11
+ import Animated, {
12
+ useSharedValue,
13
+ useAnimatedStyle,
14
+ withTiming,
15
+ } from 'react-native-reanimated';
11
16
  import {
12
17
  EDIT,
13
18
  } from '../../Constants/Commands.js';
14
19
  import {
15
20
  EDITOR_TYPE__SIDE,
21
+ EDITOR_TYPE__SMART,
16
22
  } from '../../Constants/Editor.js';
17
23
  import {
18
24
  extractCssPropertyFromClassName,
@@ -26,7 +32,7 @@ import inArray from '../../Functions/inArray.js';
26
32
  import getComponentFromType from '../../Functions/getComponentFromType.js';
27
33
  import buildAdditionalButtons from '../../Functions/buildAdditionalButtons.js';
28
34
  import testProps from '../../Functions/testProps.js';
29
- import DynamicFab from '../Buttons/DynamicFab.js';
35
+ import DynamicFab from '../Fab/DynamicFab.js';
30
36
  import Toolbar from '../Toolbar/Toolbar.js';
31
37
  import ArrowUp from '../Icons/ArrowUp.js';
32
38
  import Button from '../Buttons/Button.js';
@@ -35,11 +41,14 @@ import Pencil from '../Icons/Pencil.js';
35
41
  import Footer from '../Layout/Footer.js';
36
42
  import _ from 'lodash';
37
43
 
44
+ const FAB_FADE_TIME = 300; // ms
45
+
38
46
  function Viewer(props) {
39
47
  const {
40
48
  viewerCanDelete = false,
41
49
  items = [], // Columns, FieldSets, Fields, etc to define the form
42
50
  ancillaryItems = [], // additional items which are not controllable form elements, but should appear in the form
51
+ showAncillaryButtons = false,
43
52
  columnDefaults = {}, // defaults for each Column defined in items (above)
44
53
  record,
45
54
  additionalViewButtons,
@@ -71,10 +80,25 @@ function Viewer(props) {
71
80
  } = props,
72
81
  scrollViewRef = useRef(),
73
82
  ancillaryItemsRef = useRef({}),
74
- ancillaryFabs = useRef([]),
83
+ ancillaryButtons = useRef([]),
84
+ setAncillaryButtons = (array) => {
85
+ ancillaryButtons.current = array;
86
+ },
87
+ getAncillaryButtons = () => {
88
+ return ancillaryButtons.current;
89
+ },
75
90
  isMultiple = _.isArray(record),
76
91
  [containerWidth, setContainerWidth] = useState(),
92
+ [isFabVisible, setIsFabVisible] = useState(false),
93
+ fabOpacity = useSharedValue(0),
94
+ fabAnimatedStyle = useAnimatedStyle(() => {
95
+ return {
96
+ opacity: withTiming(fabOpacity.value, { duration: FAB_FADE_TIME }), // Smooth fade animation
97
+ pointerEvents: fabOpacity.value > 0 ? 'auto' : 'none', // Disable interaction when invisible
98
+ };
99
+ }),
77
100
  isSideEditor = editorType === EDITOR_TYPE__SIDE,
101
+ isSmartEditor = editorType === EDITOR_TYPE__SMART,
78
102
  styles = UiGlobals.styles,
79
103
  flex = props.flex || 1,
80
104
  buildFromItems = () => {
@@ -301,13 +325,16 @@ function Viewer(props) {
301
325
  },
302
326
  buildAncillary = () => {
303
327
  const components = [];
304
- ancillaryFabs.current = [];
328
+ setAncillaryButtons([]);
305
329
  if (ancillaryItems.length) {
306
330
 
307
331
  // add the "scroll to top" button
308
- ancillaryFabs.current.push({
332
+ getAncillaryButtons().push({
309
333
  icon: ArrowUp,
334
+ key: 'scrollToTop',
335
+ reference: 'scrollToTop',
310
336
  onPress: () => scrollToAncillaryItem(0),
337
+ tooltip: 'Scroll to top',
311
338
  });
312
339
 
313
340
  _.each(ancillaryItems, (item, ix) => {
@@ -324,9 +351,11 @@ function Viewer(props) {
324
351
  if (icon) {
325
352
  // NOTE: this assumes that if one Ancillary item has an icon, they all do.
326
353
  // If they don't, the ix will be wrong.
327
- ancillaryFabs.current.push({
354
+ getAncillaryButtons().push({
328
355
  icon,
329
- onPress: () => scrollToAncillaryItem(ix +1), // offset for the "scroll to top" button
356
+ key: 'ancillary-' + ix,
357
+ onPress: () => { scrollToAncillaryItem(ix +1)}, // offset for the "scroll to top" button
358
+ tooltip: title,
330
359
  });
331
360
  }
332
361
  if (type.match(/Grid/) && !itemPropsToPass.h) {
@@ -358,7 +387,7 @@ function Viewer(props) {
358
387
  }
359
388
  title = <Text className={`${styles.VIEWER_ANCILLARY_FONTSIZE} font-bold`}>{title}</Text>;
360
389
  if (icon) {
361
- title = <HStack className="items-center"><Icon as={icon} size="lg" className="mr-2" />{title}</HStack>
390
+ title = <HStack className="items-center"><Icon as={icon} className="w-[32px] h-[32px] mr-2" />{title}</HStack>
362
391
  }
363
392
  }
364
393
  components.push(<VStack
@@ -373,15 +402,33 @@ function Viewer(props) {
373
402
  }
374
403
  return components;
375
404
  },
405
+ onLayout = (e) => {
406
+ setContainerWidth(e.nativeEvent.layout.width);
407
+ },
376
408
  scrollToAncillaryItem = (ix) => {
377
409
  ancillaryItemsRef.current[ix]?.scrollIntoView({
378
410
  behavior: 'smooth',
379
411
  block: 'start',
380
412
  });
381
413
  },
382
- onLayout = (e) => {
383
- setContainerWidth(e.nativeEvent.layout.width);
384
- };
414
+ onScroll = useCallback(
415
+ _.debounce((e) => {
416
+ if (!showAncillaryButtons) {
417
+ return;
418
+ }
419
+ const
420
+ scrollY = e.nativeEvent.contentOffset.y,
421
+ isFabVisible = scrollY > 50;
422
+ fabOpacity.value = isFabVisible ? 1 : 0;
423
+ if (isFabVisible) {
424
+ setIsFabVisible(true);
425
+ } else {
426
+ // delay removal from DOM until fade-out is complete
427
+ setTimeout(() => setIsFabVisible(isFabVisible), FAB_FADE_TIME);
428
+ }
429
+ }, 100), // delay
430
+ []
431
+ );
385
432
 
386
433
  useEffect(() => {
387
434
  if (viewerSetup && record?.getSubmitValues) {
@@ -395,7 +442,7 @@ function Viewer(props) {
395
442
 
396
443
  const
397
444
  showDeleteBtn = onDelete && viewerCanDelete,
398
- showCloseBtn = !isSideEditor,
445
+ showCloseBtn = !isSideEditor && !isSmartEditor && onClose,
399
446
  showFooter = (showDeleteBtn || showCloseBtn);
400
447
  let additionalButtons = null,
401
448
  viewerComponents = null,
@@ -407,12 +454,14 @@ function Viewer(props) {
407
454
  viewerComponents = buildFromItems();
408
455
  ancillaryComponents = buildAncillary();
409
456
 
410
- if (!_.isEmpty(ancillaryFabs.current)) {
411
- fab = <DynamicFab
412
- fabs={ancillaryFabs.current}
457
+ if (showAncillaryButtons && !_.isEmpty(getAncillaryButtons())) {
458
+ fab = <Animated.View style={fabAnimatedStyle}>
459
+ <DynamicFab
460
+ buttons={getAncillaryButtons()}
413
461
  collapseOnPress={false}
414
- verticalOffset={showFooter ? 15 : 0}
415
- />;
462
+ tooltip="Scroll to Ancillary Item"
463
+ />
464
+ </Animated.View>;
416
465
  }
417
466
  }
418
467
 
@@ -450,7 +499,7 @@ function Viewer(props) {
450
499
  text="Delete"
451
500
  />
452
501
  </HStack>}
453
- {onClose && showCloseBtn &&
502
+ {showCloseBtn &&
454
503
  <Button
455
504
  {...testProps('closeBtn')}
456
505
  key="closeBtn"
@@ -472,6 +521,8 @@ function Viewer(props) {
472
521
  <ScrollView
473
522
  _web={{ height: 1 }}
474
523
  ref={scrollViewRef}
524
+ onScroll={onScroll}
525
+ scrollEventThrottle={16 /* ms */}
475
526
  className={`
476
527
  Viewer-ScrollView
477
528
  w-full
@@ -502,17 +553,23 @@ function Viewer(props) {
502
553
  </Toolbar>}
503
554
 
504
555
  {!_.isEmpty(additionalButtons) &&
505
- <Toolbar className="justify-end flex-wrap">
556
+ <Toolbar className="justify-end flex-wrap gap-2">
506
557
  {additionalButtons}
507
558
  </Toolbar>}
508
559
 
560
+ {showAncillaryButtons && !_.isEmpty(getAncillaryButtons()) &&
561
+ <Toolbar className="justify-start flex-wrap gap-2">
562
+ <Text>Scroll:</Text>
563
+ {buildAdditionalButtons(_.omitBy(getAncillaryButtons(), (btnConfig) => btnConfig.reference === 'scrollToTop'))}
564
+ </Toolbar>}
565
+
509
566
  {containerWidth >= styles.FORM_ONE_COLUMN_THRESHOLD ? <HStack className="Viewer-formComponents-HStack p-4 gap-4 justify-center">{viewerComponents}</HStack> : null}
510
567
  {containerWidth < styles.FORM_ONE_COLUMN_THRESHOLD ? <VStack className="Viewer-formComponents-VStack p-4">{viewerComponents}</VStack> : null}
511
568
  <VStack className="Viewer-AncillaryComponents m-2 pt-4 px-2">{ancillaryComponents}</VStack>
512
569
  </ScrollView>
513
570
 
514
571
  {footer}
515
- {fab}
572
+ {isFabVisible && fab}
516
573
 
517
574
  </>}
518
575
  </VStackNative>;
@@ -25,6 +25,7 @@ function UploadsDownloadsWindow(props) {
25
25
  downloadHeaders,
26
26
  uploadParams = {},
27
27
  downloadParams = {},
28
+ isDownloadOnly = false,
28
29
  onUpload,
29
30
 
30
31
  // withComponent
@@ -124,6 +125,61 @@ function UploadsDownloadsWindow(props) {
124
125
  }
125
126
  }
126
127
  };
128
+
129
+ const items = [
130
+ {
131
+ type: 'DisplayField',
132
+ text: 'Download an Excel file of the current grid contents.',
133
+ },
134
+ {
135
+ type: 'Button',
136
+ text: 'Download',
137
+ isEditable: false,
138
+ icon: Excel,
139
+ _icon: {
140
+ size: 'md',
141
+ },
142
+ onPress: () => onDownload(),
143
+ className: 'mb-5',
144
+ },
145
+ ];
146
+ if (!isDownloadOnly) {
147
+ items.push({
148
+ type: 'DisplayField',
149
+ text: 'Upload an Excel file to the current grid.',
150
+ });
151
+ items.push({
152
+ type: 'File',
153
+ name: 'file',
154
+ onChangeValue: setImportFile,
155
+ accept: '.xlsx',
156
+ });
157
+ items.push({
158
+ type: 'Row',
159
+ className: 'mt-2',
160
+ items: [
161
+ {
162
+ type: 'Button',
163
+ text: 'Upload',
164
+ isEditable: false,
165
+ icon: Upload,
166
+ _icon: {
167
+ size: 'md',
168
+ },
169
+ isDisabled: !importFile,
170
+ onPress: onUploadLocal,
171
+ },
172
+ {
173
+ type: 'Button',
174
+ text: 'Get Template',
175
+ icon: Download,
176
+ isEditable: false,
177
+ onPress: onDownloadTemplate,
178
+ },
179
+
180
+ ],
181
+ });
182
+ }
127
183
 
128
184
  return <Panel
129
185
  {...props}
@@ -151,58 +207,7 @@ function UploadsDownloadsWindow(props) {
151
207
  "type": "Column",
152
208
  "flex": 1,
153
209
  "defaults": {},
154
- "items": [
155
- {
156
- type: 'DisplayField',
157
- text: 'Download an Excel file of the current grid contents.',
158
- },
159
- {
160
- type: 'Button',
161
- text: 'Download',
162
- isEditable: false,
163
- icon: Excel,
164
- _icon: {
165
- size: 'md',
166
- },
167
- onPress: () => onDownload(),
168
- className: 'mb-5',
169
- },
170
- {
171
- type: 'DisplayField',
172
- text: 'Upload an Excel file to the current grid.',
173
- },
174
- {
175
- type: 'File',
176
- name: 'file',
177
- onChangeValue: setImportFile,
178
- accept: '.xlsx',
179
- },
180
- {
181
- type: 'Row',
182
- className: 'mt-2',
183
- items: [
184
- {
185
- type: 'Button',
186
- text: 'Upload',
187
- isEditable: false,
188
- icon: Upload,
189
- _icon: {
190
- size: 'md',
191
- },
192
- isDisabled: !importFile,
193
- onPress: onUploadLocal,
194
- },
195
- {
196
- type: 'Button',
197
- text: 'Get Template',
198
- icon: Download,
199
- isEditable: false,
200
- onPress: onDownloadTemplate,
201
- },
202
-
203
- ],
204
- },
205
- ]
210
+ "items": items,
206
211
  },
207
212
  ]}
208
213
  // record={selection}
@@ -6,3 +6,4 @@ export const COPY = 'copy';
6
6
  export const DUPLICATE = 'duplicate';
7
7
  export const PRINT = 'print';
8
8
  export const UPLOAD_DOWNLOAD = 'uploadDownload';
9
+ export const DOWNLOAD = 'download';
@@ -7,30 +7,23 @@ export default function buildAdditionalButtons(configs, self, handlerArgs = {})
7
7
  _.each(configs, (config) => {
8
8
  const {
9
9
  key,
10
- text,
11
- handler,
12
- icon,
13
- isDisabled,
14
- tooltip,
15
10
  color = '#fff',
11
+ ...configToPass
16
12
  } = config,
17
13
  buttonProps = {
18
- key,
19
- parent: self,
20
- reference: key,
21
- text,
22
- icon,
23
- isDisabled,
24
- tooltip,
25
- color,
14
+ ...configToPass,
26
15
  };
27
- if (handler) {
28
- buttonProps.onPress = () => handler(handlerArgs);
16
+ buttonProps.parent = config.self;
17
+ buttonProps.color = color;
18
+
19
+ if (!config.onPress && config.handler) {
20
+ buttonProps.onPress = () => config.handler(handlerArgs);
29
21
  }
30
22
 
31
23
  additionalButtons.push(<Button
32
24
  {...testProps(key)}
33
25
  {...buttonProps}
26
+ key={key}
34
27
  />);
35
28
  });
36
29
  return additionalButtons;
@@ -0,0 +1,5 @@
1
+ export default function(value, allowNull = true) {
2
+ if (allowNull && !value) return true; // Allow null or empty values
3
+ const date = new Date(value);
4
+ return !isNaN(date.getTime()); // Check if the date is valid
5
+ }
@@ -0,0 +1,6 @@
1
+ import UiGlobals from '../UiGlobals';
2
+ import Inflector from 'inflector-js';
3
+
4
+ export default function urlize(str) {
5
+ return '/' + UiGlobals.urlPrefix + Inflector.dasherize(Inflector.underscore(str));
6
+ }
@@ -1,98 +0,0 @@
1
- import { useCallback } from 'react';
2
- import {
3
- Fab, FabIcon, FabLabel,
4
- ScrollView,
5
- VStack,
6
- } from '@project-components/Gluestack';
7
- import Animated, {
8
- useSharedValue,
9
- useAnimatedStyle,
10
- withTiming,
11
- interpolate,
12
- } from 'react-native-reanimated';
13
- import EllipsisVertical from '../Icons/EllipsisVertical.js';
14
- import Xmark from '../Icons/Xmark.js';
15
-
16
- // This component creates a floating action button (FAB)
17
- // that can expand and collapse to show multiple FABs beneath it.
18
-
19
- export default function DynamicFab(props) {
20
- const {
21
- fabs,
22
- icon = null,
23
- label = null,
24
- collapseOnPress = true,
25
- } = props,
26
- isExpanded = useSharedValue(0),
27
- toggleFab = useCallback(() => {
28
- isExpanded.value = isExpanded.value ? 0 : 1;
29
- }, []),
30
- fabSpacing = 50,
31
- verticalOffset = 15; // to shift the entire expanded group up
32
-
33
- let className = `
34
- DynamicFab
35
- fixed
36
- pb-[20px]
37
- bottom-4
38
- right-4
39
- `;
40
- if (props.className) {
41
- className += ` ${props.className}`;
42
- }
43
-
44
- return <VStack className={className}>
45
- {fabs
46
- .slice() // clone, so we don't mutate the original array
47
- .reverse() // so fabs appear in the correct order
48
- .map((fab, index) => {
49
- const {
50
- icon,
51
- label,
52
- onPress,
53
- } = fab,
54
- animatedStyle = useAnimatedStyle(() => {
55
- const translateY = interpolate(
56
- isExpanded.value,
57
- [0, 1],
58
- [0, -(fabSpacing * (index + 1)) - verticalOffset]
59
- );
60
- return {
61
- transform: [{ translateY }],
62
- opacity: withTiming(isExpanded.value, { duration: 200 }),
63
- };
64
- });
65
-
66
- return <Animated.View
67
- key={index}
68
- style={animatedStyle}
69
- className="absolute bottom-0 right-0"
70
- >
71
- <Fab
72
- size="md"
73
- className="bg-primary-600"
74
- onPress={() => {
75
- onPress();
76
- if (collapseOnPress) {
77
- isExpanded.value = 0;
78
- }
79
- }}
80
- style={{
81
- shadowColor: 'transparent', // otherwise, on collapse a bunch of shadows build up for a moment!
82
- }}
83
- >
84
- <FabIcon as={icon} />
85
- {label && <FabLabel>{label}</FabLabel>}
86
- </Fab>
87
- </Animated.View>;
88
- })}
89
- <Fab
90
- size="lg"
91
- onPress={toggleFab}
92
- className="z-100 bg-primary-600"
93
- >
94
- <FabIcon as={isExpanded.value ? Xmark : icon || EllipsisVertical} />
95
- {label && <FabLabel>{label}</FabLabel>}
96
- </Fab>
97
- </VStack>;
98
- };
@@ -1,2 +0,0 @@
1
- export const EDITOR_MODE_ADD = 'EDITOR_MODE_ADD';
2
- export const EDITOR_MODE_EDIT = 'EDITOR_MODE_EDIT';