@onehat/ui 0.4.59 → 0.4.61

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.59",
3
+ "version": "0.4.61",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -0,0 +1,98 @@
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
+ };
@@ -40,10 +40,17 @@ import testProps from '../../Functions/testProps.js';
40
40
  import Toolbar from '../Toolbar/Toolbar.js';
41
41
  import Button from '../Buttons/Button.js';
42
42
  import IconButton from '../Buttons/IconButton.js';
43
+ import DynamicFab from '../Buttons/DynamicFab.js';
43
44
  import AngleLeft from '../Icons/AngleLeft.js';
44
45
  import Eye from '../Icons/Eye.js';
45
46
  import Rotate from '../Icons/Rotate.js';
46
47
  import Pencil from '../Icons/Pencil.js';
48
+ import Plus from '../Icons/Plus.js';
49
+ import FloppyDiskRegular from '../Icons/FloppyDiskRegular.js';
50
+ import Trash from '../Icons/Trash.js';
51
+ import ArrowUp from '../Icons/ArrowUp.js';
52
+ import Xmark from '../Icons/Xmark.js';
53
+ import Check from '../Icons/Check.js';
47
54
  import Footer from '../Layout/Footer.js';
48
55
  import Label from '../Form/Label.js';
49
56
  import _ from 'lodash';
@@ -132,6 +139,8 @@ function Form(props) {
132
139
 
133
140
  } = props,
134
141
  formRef = useRef(),
142
+ ancillaryItemsRef = useRef({}),
143
+ ancillaryFabs = useRef([]),
135
144
  styles = UiGlobals.styles,
136
145
  record = props.record?.length === 1 ? props.record[0] : props.record;
137
146
 
@@ -893,12 +902,21 @@ function Form(props) {
893
902
  },
894
903
  buildAncillary = () => {
895
904
  const components = [];
905
+ ancillaryFabs.current = [];
896
906
  if (ancillaryItems.length) {
907
+
908
+ // add the "scroll to top" button
909
+ ancillaryFabs.current.push({
910
+ icon: ArrowUp,
911
+ onPress: () => scrollToAncillaryItem(0),
912
+ });
913
+
897
914
  _.each(ancillaryItems, (item, ix) => {
898
915
  let {
899
916
  type,
900
917
  title = null,
901
918
  description = null,
919
+ icon,
902
920
  selectorId,
903
921
  selectorSelectedField,
904
922
  ...itemPropsToPass
@@ -906,6 +924,14 @@ function Form(props) {
906
924
  if (isMultiple && type !== 'Attachments') {
907
925
  return;
908
926
  }
927
+ 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({
931
+ icon,
932
+ onPress: () => scrollToAncillaryItem(ix +1), // offset for the "scroll to top" button
933
+ });
934
+ }
909
935
  if (type.match(/Grid/) && !itemPropsToPass.h) {
910
936
  itemPropsToPass.h = 400;
911
937
  }
@@ -932,6 +958,9 @@ function Form(props) {
932
958
  ${styles.FORM_ANCILLARY_TITLE_CLASSNAME}
933
959
  `}
934
960
  >{title}</Text>;
961
+ if (icon) {
962
+ title = <HStack className="items-center"><Icon as={icon} size="lg" className="mr-2" />{title}</HStack>
963
+ }
935
964
  }
936
965
  if (description) {
937
966
  description = <Text
@@ -943,6 +972,7 @@ function Form(props) {
943
972
  >{description}</Text>;
944
973
  }
945
974
  components.push(<VStack
975
+ ref={(el) => (ancillaryItemsRef.current[ix +1 /* offset for "scroll to top" */] = el)}
946
976
  key={'ancillary-' + ix}
947
977
  className={`
948
978
  Form-VStack12
@@ -958,6 +988,12 @@ function Form(props) {
958
988
  }
959
989
  return components;
960
990
  },
991
+ scrollToAncillaryItem = (ix) => {
992
+ ancillaryItemsRef.current[ix]?.scrollIntoView({
993
+ behavior: 'smooth',
994
+ block: 'start',
995
+ });
996
+ },
961
997
  onSubmitError = (errors, e) => {
962
998
  if (editorType === EDITOR_TYPE__INLINE) {
963
999
  alert(errors.message);
@@ -1082,6 +1118,7 @@ function Form(props) {
1082
1118
  formComponents,
1083
1119
  editor,
1084
1120
  additionalButtons,
1121
+ fab = null,
1085
1122
  isSaveDisabled = false,
1086
1123
  isSubmitDisabled = false,
1087
1124
  showDeleteBtn = false,
@@ -1095,13 +1132,13 @@ function Form(props) {
1095
1132
  // create editor
1096
1133
  if (editorType === EDITOR_TYPE__INLINE) {
1097
1134
  editor = buildFromColumnsConfig();
1098
- // } else if (editorType === EDITOR_TYPE__PLAIN) {
1099
- // formComponents = buildFromItems();
1100
- // const formAncillaryComponents = buildAncillary();
1101
- // editor = <>
1102
- // <VStack className="p-4">{formComponents}</VStack>
1103
- // <VStack className="pt-4">{formAncillaryComponents}</VStack>
1104
- // </>;
1135
+ // } else if (editorType === EDITOR_TYPE__PLAIN) {
1136
+ // formComponents = buildFromItems();
1137
+ // const formAncillaryComponents = buildAncillary();
1138
+ // editor = <>
1139
+ // <VStack className="p-4">{formComponents}</VStack>
1140
+ // <VStack className="pt-4">{formAncillaryComponents}</VStack>
1141
+ // </>;
1105
1142
  } else {
1106
1143
  formComponents = buildFromItems();
1107
1144
  const formAncillaryComponents = buildAncillary();
@@ -1155,10 +1192,15 @@ function Form(props) {
1155
1192
  {additionalButtons}
1156
1193
  </Toolbar>)
1157
1194
  }
1195
+ if (!_.isEmpty(ancillaryFabs.current)) {
1196
+ fab = <DynamicFab
1197
+ fabs={ancillaryFabs.current}
1198
+ collapseOnPress={false}
1199
+ className="bottom-[55px]"
1200
+ />;
1201
+ }
1158
1202
  }
1159
1203
 
1160
-
1161
-
1162
1204
  // create footer
1163
1205
  if (!formState.isValid) {
1164
1206
  isSaveDisabled = true;
@@ -1216,6 +1258,7 @@ function Form(props) {
1216
1258
  {...testProps('deleteBtn')}
1217
1259
  key="deleteBtn"
1218
1260
  onPress={onDelete}
1261
+ icon={Trash}
1219
1262
  className={`
1220
1263
  bg-warning-500
1221
1264
  hover:bg-warning-700
@@ -1239,6 +1282,7 @@ function Form(props) {
1239
1282
  {...testProps('cancelBtn')}
1240
1283
  key="cancelBtn"
1241
1284
  variant={editorType === EDITOR_TYPE__INLINE ? 'solid' : 'outline'}
1285
+ icon={Xmark}
1242
1286
  onPress={onCancel}
1243
1287
  className="text-white"
1244
1288
  text="Cancel"
@@ -1249,6 +1293,7 @@ function Form(props) {
1249
1293
  {...testProps('closeBtn')}
1250
1294
  key="closeBtn"
1251
1295
  variant={editorType === EDITOR_TYPE__INLINE ? 'solid' : 'outline'}
1296
+ icon={Xmark}
1252
1297
  onPress={onClose}
1253
1298
  className="text-white"
1254
1299
  text="Close"
@@ -1259,6 +1304,7 @@ function Form(props) {
1259
1304
  {...testProps('saveBtn')}
1260
1305
  key="saveBtn"
1261
1306
  onPress={(e) => handleSubmit(onSaveDecorated, onSubmitError)(e)}
1307
+ icon={getEditorMode() === EDITOR_MODE__ADD ? Plus : FloppyDiskRegular}
1262
1308
  isDisabled={isSaveDisabled}
1263
1309
  className="text-white"
1264
1310
  text={getEditorMode() === EDITOR_MODE__ADD ? 'Add' : 'Save'}
@@ -1268,6 +1314,7 @@ function Form(props) {
1268
1314
  <Button
1269
1315
  {...testProps('submitBtn')}
1270
1316
  key="submitBtn"
1317
+ icon={Check}
1271
1318
  onPress={(e) => handleSubmit(onSubmitDecorated, onSubmitError)(e)}
1272
1319
  isDisabled={isSubmitDisabled}
1273
1320
  className="text-white"
@@ -1283,6 +1330,7 @@ function Form(props) {
1283
1330
  {...testProps('additionalFooterBtn-' + props.key)}
1284
1331
  {...props}
1285
1332
  onPress={(e) => handleSubmit(props.onPress, onSubmitError)(e)}
1333
+ icon={props.icon || null}
1286
1334
  text={props.text}
1287
1335
  isDisabled={isDisabled}
1288
1336
  />;
@@ -1344,6 +1392,7 @@ function Form(props) {
1344
1392
 
1345
1393
  let className = props.className || '';
1346
1394
  className += ' Form-VStackNative';
1395
+ const scrollToTopAnchor = <Box ref={(el) => (ancillaryItemsRef.current[0] = el)} className="h-0" />;
1347
1396
  return <VStackNative
1348
1397
  ref={formRef}
1349
1398
  {...testProps(self)}
@@ -1367,6 +1416,7 @@ function Form(props) {
1367
1416
  // height: '100%',
1368
1417
  }}
1369
1418
  >
1419
+ {scrollToTopAnchor}
1370
1420
  {modeHeader}
1371
1421
  {formHeader}
1372
1422
  {formButtons}
@@ -1374,6 +1424,7 @@ function Form(props) {
1374
1424
  </ScrollView>}
1375
1425
 
1376
1426
  {footer}
1427
+ {fab}
1377
1428
 
1378
1429
  </>}
1379
1430
  </VStackNative>;
@@ -424,9 +424,9 @@ function GridComponent(props) {
424
424
  } else {
425
425
  let canDoEdit = false,
426
426
  canDoView = false;
427
- if (onEdit && canUser && canUser(EDIT) && canRecordBeEdited && canRecordBeEdited(selection)) {
427
+ if (onEdit && canUser && canUser(EDIT) && (!canRecordBeEdited || canRecordBeEdited(selection))) {
428
428
  canDoEdit = true;
429
- }
429
+ } else
430
430
  if (onView && canUser && canUser(VIEW)) {
431
431
  canDoView = true;
432
432
  }
@@ -0,0 +1,11 @@
1
+ import { createIcon } from "../Gluestack/icon";
2
+ // Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.
3
+ import { Path, Svg } from 'react-native-svg';
4
+
5
+ const SvgComponent = createIcon({
6
+ Root: Svg,
7
+ viewBox: '0 0 384 512',
8
+ path: <Path d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.3l105.4 105.3c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z" />,
9
+ });
10
+
11
+ export default SvgComponent
@@ -1,6 +1,8 @@
1
1
  import { useEffect, useRef, useState, isValidElement, } from 'react';
2
2
  import {
3
+ Box,
3
4
  HStack,
5
+ Icon,
4
6
  ScrollView,
5
7
  Text,
6
8
  VStack,
@@ -24,7 +26,9 @@ import inArray from '../../Functions/inArray.js';
24
26
  import getComponentFromType from '../../Functions/getComponentFromType.js';
25
27
  import buildAdditionalButtons from '../../Functions/buildAdditionalButtons.js';
26
28
  import testProps from '../../Functions/testProps.js';
29
+ import DynamicFab from '../Buttons/DynamicFab.js';
27
30
  import Toolbar from '../Toolbar/Toolbar.js';
31
+ import ArrowUp from '../Icons/ArrowUp.js';
28
32
  import Button from '../Buttons/Button.js';
29
33
  import Label from '../Form/Label.js';
30
34
  import Pencil from '../Icons/Pencil.js';
@@ -66,6 +70,8 @@ function Viewer(props) {
66
70
 
67
71
  } = props,
68
72
  scrollViewRef = useRef(),
73
+ ancillaryItemsRef = useRef({}),
74
+ ancillaryFabs = useRef([]),
69
75
  isMultiple = _.isArray(record),
70
76
  [containerWidth, setContainerWidth] = useState(),
71
77
  isSideEditor = editorType === EDITOR_TYPE__SIDE,
@@ -295,17 +301,34 @@ function Viewer(props) {
295
301
  },
296
302
  buildAncillary = () => {
297
303
  const components = [];
304
+ ancillaryFabs.current = [];
298
305
  if (ancillaryItems.length) {
306
+
307
+ // add the "scroll to top" button
308
+ ancillaryFabs.current.push({
309
+ icon: ArrowUp,
310
+ onPress: () => scrollToAncillaryItem(0),
311
+ });
312
+
299
313
  _.each(ancillaryItems, (item, ix) => {
300
314
  let {
301
315
  type,
302
316
  title = null,
317
+ icon,
303
318
  selectorId = null,
304
319
  ...itemPropsToPass
305
320
  } = item;
306
321
  if (isMultiple && type !== 'Attachments') {
307
322
  return;
308
323
  }
324
+ if (icon) {
325
+ // NOTE: this assumes that if one Ancillary item has an icon, they all do.
326
+ // If they don't, the ix will be wrong.
327
+ ancillaryFabs.current.push({
328
+ icon,
329
+ onPress: () => scrollToAncillaryItem(ix +1), // offset for the "scroll to top" button
330
+ });
331
+ }
309
332
  if (type.match(/Grid/) && !itemPropsToPass.h) {
310
333
  itemPropsToPass.h = 400;
311
334
  }
@@ -334,12 +357,28 @@ function Viewer(props) {
334
357
  title += ' for ' + record.displayValue;
335
358
  }
336
359
  title = <Text className={`${styles.VIEWER_ANCILLARY_FONTSIZE} font-bold`}>{title}</Text>;
360
+ if (icon) {
361
+ title = <HStack className="items-center"><Icon as={icon} size="lg" className="mr-2" />{title}</HStack>
362
+ }
337
363
  }
338
- components.push(<VStack key={'ancillary-' + ix} className="my-3">{title}{element}</VStack>);
364
+ components.push(<VStack
365
+ ref={(el) => (ancillaryItemsRef.current[ix +1 /* offset for "scroll to top" */] = el)}
366
+ key={'ancillary-' + ix}
367
+ className="my-3"
368
+ >
369
+ {title}
370
+ {element}
371
+ </VStack>);
339
372
  });
340
373
  }
341
374
  return components;
342
375
  },
376
+ scrollToAncillaryItem = (ix) => {
377
+ ancillaryItemsRef.current[ix]?.scrollIntoView({
378
+ behavior: 'smooth',
379
+ block: 'start',
380
+ });
381
+ },
343
382
  onLayout = (e) => {
344
383
  setContainerWidth(e.nativeEvent.layout.width);
345
384
  };
@@ -356,15 +395,25 @@ function Viewer(props) {
356
395
 
357
396
  const
358
397
  showDeleteBtn = onDelete && viewerCanDelete,
359
- showCloseBtn = !isSideEditor;
398
+ showCloseBtn = !isSideEditor,
399
+ showFooter = (showDeleteBtn || showCloseBtn);
360
400
  let additionalButtons = null,
361
401
  viewerComponents = null,
362
- ancillaryComponents = null;
402
+ ancillaryComponents = null,
403
+ fab = null;
363
404
 
364
405
  if (containerWidth) { // we need to render this component twice in order to get the container width. Skip this on first render
365
406
  additionalButtons = buildAdditionalButtons(additionalViewButtons);
366
407
  viewerComponents = buildFromItems();
367
408
  ancillaryComponents = buildAncillary();
409
+
410
+ if (!_.isEmpty(ancillaryFabs.current)) {
411
+ fab = <DynamicFab
412
+ fabs={ancillaryFabs.current}
413
+ collapseOnPress={false}
414
+ verticalOffset={showFooter ? 15 : 0}
415
+ />;
416
+ }
368
417
  }
369
418
 
370
419
  let canEdit = true;
@@ -385,6 +434,33 @@ function Viewer(props) {
385
434
  className += ' ' + props.className;
386
435
  }
387
436
 
437
+ const footer = showFooter ?
438
+ <Footer className="justify-end">
439
+ {showDeleteBtn &&
440
+ <HStack className="flex-1 justify-start">
441
+ <Button
442
+ {...testProps('deleteBtn')}
443
+ key="deleteBtn"
444
+ onPress={onDelete}
445
+ className={`
446
+ text-white
447
+ bg-warning-500
448
+ hover:bg-warning-600
449
+ `}
450
+ text="Delete"
451
+ />
452
+ </HStack>}
453
+ {onClose && showCloseBtn &&
454
+ <Button
455
+ {...testProps('closeBtn')}
456
+ key="closeBtn"
457
+ onPress={onClose}
458
+ className="text-white"
459
+ text="Close"
460
+ />}
461
+ </Footer> : null;
462
+
463
+ const scrollToTopAnchor = <Box ref={(el) => (ancillaryItemsRef.current[0] = el)} className="h-0" />;
388
464
  return <VStackNative
389
465
  {...testProps(self)}
390
466
  style={style}
@@ -403,6 +479,7 @@ function Viewer(props) {
403
479
  flex-1
404
480
  `}
405
481
  >
482
+ {scrollToTopAnchor}
406
483
  {canEdit && onEditMode &&
407
484
  <Toolbar className="justify-end">
408
485
  <HStack className="flex-1 items-center">
@@ -434,31 +511,8 @@ function Viewer(props) {
434
511
  <VStack className="Viewer-AncillaryComponents m-2 pt-4 px-2">{ancillaryComponents}</VStack>
435
512
  </ScrollView>
436
513
 
437
- {(showDeleteBtn || showCloseBtn) &&
438
- <Footer className="justify-end">
439
- {showDeleteBtn &&
440
- <HStack className="flex-1 justify-start">
441
- <Button
442
- {...testProps('deleteBtn')}
443
- key="deleteBtn"
444
- onPress={onDelete}
445
- className={`
446
- text-white
447
- bg-warning-500
448
- hover:bg-warning-600
449
- `}
450
- text="Delete"
451
- />
452
- </HStack>}
453
- {onClose && showCloseBtn &&
454
- <Button
455
- {...testProps('closeBtn')}
456
- key="closeBtn"
457
- onPress={onClose}
458
- className="text-white"
459
- text="Close"
460
- />}
461
- </Footer>}
514
+ {footer}
515
+ {fab}
462
516
 
463
517
  </>}
464
518
  </VStackNative>;
@@ -9,6 +9,7 @@ import AngleRight from './Icons/AngleRight.js';
9
9
  import AnglesLeft from './Icons/AnglesLeft.js';
10
10
  import AnglesRight from './Icons/AnglesRight.js';
11
11
  import Asterisk from './Icons/Asterisk.js';
12
+ import ArrowUp from './Icons/ArrowUp.js';
12
13
  import Ban from './Icons/Ban.js';
13
14
  import Bars from './Icons/Bars.js';
14
15
  import BarsStaggered from './Icons/BarsStaggered.js';
@@ -253,6 +254,7 @@ const components = {
253
254
  AnglesLeft,
254
255
  AnglesRight,
255
256
  Asterisk,
257
+ ArrowUp,
256
258
  Ban,
257
259
  Bars,
258
260
  BarsStaggered,