@onehat/ui 0.4.61 → 0.4.62
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 +1 -1
- package/src/Components/Fab/DynamicFab.js +108 -0
- package/src/Components/Fab/FabWithTooltip.js +6 -0
- package/src/Components/Form/Field/Combo/Combo.js +3 -3
- package/src/Components/Form/Field/Date.js +1 -1
- package/src/Components/Form/Field/Number.js +4 -1
- package/src/Components/Form/Form.js +78 -24
- package/src/Components/Hoc/withData.js +16 -0
- package/src/Components/Hoc/withEditor.js +2 -1
- package/src/Components/Hoc/withPresetButtons.js +25 -4
- package/src/Components/Hoc/withTooltip.js +2 -0
- package/src/Components/Report/Report.js +10 -4
- package/src/Components/Tab/TabBar.js +11 -4
- package/src/Components/Tooltip/Tooltip.js +2 -2
- package/src/Components/Viewer/Viewer.js +77 -20
- package/src/Functions/buildAdditionalButtons.js +8 -15
- package/src/Functions/isValidDate.js +5 -0
- package/src/Functions/urlize.js +6 -0
- package/src/Components/Buttons/DynamicFab.js +0 -98
- package/src/Constants/EditorModes.js +0 -2
package/package.json
CHANGED
|
@@ -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
|
+
};
|
|
@@ -659,7 +659,7 @@ export const ComboComponent = forwardRef((props, ref) => {
|
|
|
659
659
|
placeholder={placeholder}
|
|
660
660
|
tooltip={tooltip}
|
|
661
661
|
tooltipPlacement={tooltipPlacement}
|
|
662
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1017
|
+
tooltipTriggerClassName={`
|
|
1018
1018
|
h-full
|
|
1019
1019
|
flex-1
|
|
1020
1020
|
`}
|
|
@@ -164,7 +164,10 @@ function NumberElement(props) {
|
|
|
164
164
|
isDisabled={isDisabled}
|
|
165
165
|
tooltip={tooltip}
|
|
166
166
|
tooltipPlacement={tooltipPlacement}
|
|
167
|
-
|
|
167
|
+
tooltipTriggerClassName={`
|
|
168
|
+
flex-1
|
|
169
|
+
h-full
|
|
170
|
+
`}
|
|
168
171
|
className={`
|
|
169
172
|
h-full
|
|
170
173
|
text-center
|
|
@@ -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 '../
|
|
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,15 @@ 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
|
|
79
86
|
ancillaryItems = [], // additional items which are not controllable form elements, but should appear in the form
|
|
87
|
+
showAncillaryButtons = false,
|
|
80
88
|
columnDefaults = {}, // defaults for each Column defined in items (above)
|
|
81
89
|
columnsConfig, // Which columns are shown in Grid, so the inline editor can match. Used only for EDITOR_TYPE__INLINE
|
|
82
90
|
validator, // custom validator, mainly for EDITOR_TYPE__PLAIN
|
|
@@ -140,7 +148,13 @@ function Form(props) {
|
|
|
140
148
|
} = props,
|
|
141
149
|
formRef = useRef(),
|
|
142
150
|
ancillaryItemsRef = useRef({}),
|
|
143
|
-
|
|
151
|
+
ancillaryButtons = useRef([]),
|
|
152
|
+
setAncillaryButtons = (array) => {
|
|
153
|
+
ancillaryButtons.current = array;
|
|
154
|
+
},
|
|
155
|
+
getAncillaryButtons = () => {
|
|
156
|
+
return ancillaryButtons.current;
|
|
157
|
+
},
|
|
144
158
|
styles = UiGlobals.styles,
|
|
145
159
|
record = props.record?.length === 1 ? props.record[0] : props.record;
|
|
146
160
|
|
|
@@ -158,6 +172,14 @@ function Form(props) {
|
|
|
158
172
|
forceUpdate = useForceUpdate(),
|
|
159
173
|
[previousRecord, setPreviousRecord] = useState(record),
|
|
160
174
|
[containerWidth, setContainerWidth] = useState(),
|
|
175
|
+
[isFabVisible, setIsFabVisible] = useState(false),
|
|
176
|
+
fabOpacity = useSharedValue(0),
|
|
177
|
+
fabAnimatedStyle = useAnimatedStyle(() => {
|
|
178
|
+
return {
|
|
179
|
+
opacity: withTiming(fabOpacity.value, { duration: FAB_FADE_TIME }), // Smooth fade animation
|
|
180
|
+
pointerEvents: fabOpacity.value > 0 ? 'auto' : 'none', // Disable interaction when invisible
|
|
181
|
+
};
|
|
182
|
+
}),
|
|
161
183
|
initialValues = _.merge(startingValues, (record && !record.isDestroyed ? record.submitValues : {})),
|
|
162
184
|
defaultValues = isMultiple ? getNullFieldValues(initialValues, Repository) : initialValues, // when multiple entities, set all default values to null
|
|
163
185
|
validatorToUse = validator || (isMultiple ? disableRequiredYupFields(Repository?.schema?.model?.validator) : Repository?.schema?.model?.validator) || yup.object(),
|
|
@@ -902,13 +924,15 @@ function Form(props) {
|
|
|
902
924
|
},
|
|
903
925
|
buildAncillary = () => {
|
|
904
926
|
const components = [];
|
|
905
|
-
|
|
927
|
+
setAncillaryButtons([]);
|
|
906
928
|
if (ancillaryItems.length) {
|
|
907
929
|
|
|
908
930
|
// add the "scroll to top" button
|
|
909
|
-
|
|
931
|
+
getAncillaryButtons().push({
|
|
910
932
|
icon: ArrowUp,
|
|
933
|
+
reference: 'scrollToTop',
|
|
911
934
|
onPress: () => scrollToAncillaryItem(0),
|
|
935
|
+
tooltip: 'Scroll to top',
|
|
912
936
|
});
|
|
913
937
|
|
|
914
938
|
_.each(ancillaryItems, (item, ix) => {
|
|
@@ -925,11 +949,12 @@ function Form(props) {
|
|
|
925
949
|
return;
|
|
926
950
|
}
|
|
927
951
|
if (icon) {
|
|
928
|
-
//
|
|
929
|
-
// If they don't, the ix will be wrong
|
|
930
|
-
|
|
952
|
+
// NOTE: this assumes that if one Ancillary item has an icon, they all do.
|
|
953
|
+
// If they don't, the ix will be wrong!
|
|
954
|
+
getAncillaryButtons().push({
|
|
931
955
|
icon,
|
|
932
956
|
onPress: () => scrollToAncillaryItem(ix +1), // offset for the "scroll to top" button
|
|
957
|
+
tooltip: title,
|
|
933
958
|
});
|
|
934
959
|
}
|
|
935
960
|
if (type.match(/Grid/) && !itemPropsToPass.h) {
|
|
@@ -959,7 +984,7 @@ function Form(props) {
|
|
|
959
984
|
`}
|
|
960
985
|
>{title}</Text>;
|
|
961
986
|
if (icon) {
|
|
962
|
-
title = <HStack className="items-center"><Icon as={icon}
|
|
987
|
+
title = <HStack className="items-center"><Icon as={icon} className="w-[32px] h-[32px] mr-2" />{title}</HStack>
|
|
963
988
|
}
|
|
964
989
|
}
|
|
965
990
|
if (description) {
|
|
@@ -988,12 +1013,6 @@ function Form(props) {
|
|
|
988
1013
|
}
|
|
989
1014
|
return components;
|
|
990
1015
|
},
|
|
991
|
-
scrollToAncillaryItem = (ix) => {
|
|
992
|
-
ancillaryItemsRef.current[ix]?.scrollIntoView({
|
|
993
|
-
behavior: 'smooth',
|
|
994
|
-
block: 'start',
|
|
995
|
-
});
|
|
996
|
-
},
|
|
997
1016
|
onSubmitError = (errors, e) => {
|
|
998
1017
|
if (editorType === EDITOR_TYPE__INLINE) {
|
|
999
1018
|
alert(errors.message);
|
|
@@ -1026,7 +1045,31 @@ function Form(props) {
|
|
|
1026
1045
|
}
|
|
1027
1046
|
|
|
1028
1047
|
setContainerWidth(e.nativeEvent.layout.width);
|
|
1029
|
-
}
|
|
1048
|
+
},
|
|
1049
|
+
scrollToAncillaryItem = (ix) => {
|
|
1050
|
+
ancillaryItemsRef.current[ix]?.scrollIntoView({
|
|
1051
|
+
behavior: 'smooth',
|
|
1052
|
+
block: 'start',
|
|
1053
|
+
});
|
|
1054
|
+
},
|
|
1055
|
+
onScroll = useCallback(
|
|
1056
|
+
_.debounce((e) => {
|
|
1057
|
+
if (!showAncillaryButtons) {
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
const
|
|
1061
|
+
scrollY = e.nativeEvent.contentOffset.y,
|
|
1062
|
+
isFabVisible = scrollY > 50;
|
|
1063
|
+
fabOpacity.value = isFabVisible ? 1 : 0;
|
|
1064
|
+
if (isFabVisible) {
|
|
1065
|
+
setIsFabVisible(true);
|
|
1066
|
+
} else {
|
|
1067
|
+
// delay removal from DOM until fade-out is complete
|
|
1068
|
+
setTimeout(() => setIsFabVisible(isFabVisible), FAB_FADE_TIME);
|
|
1069
|
+
}
|
|
1070
|
+
}, 100), // delay
|
|
1071
|
+
[]
|
|
1072
|
+
);
|
|
1030
1073
|
|
|
1031
1074
|
useEffect(() => {
|
|
1032
1075
|
if (skipAll) {
|
|
@@ -1111,8 +1154,9 @@ function Form(props) {
|
|
|
1111
1154
|
style.maxHeight = maxHeight;
|
|
1112
1155
|
}
|
|
1113
1156
|
|
|
1114
|
-
const formButtons = [];
|
|
1115
1157
|
let modeHeader = null,
|
|
1158
|
+
formButtons = null,
|
|
1159
|
+
scrollButtons = null,
|
|
1116
1160
|
footer = null,
|
|
1117
1161
|
footerButtons = null,
|
|
1118
1162
|
formComponents,
|
|
@@ -1188,16 +1232,19 @@ function Form(props) {
|
|
|
1188
1232
|
</Toolbar>;
|
|
1189
1233
|
}
|
|
1190
1234
|
if (getEditorMode() === EDITOR_MODE__EDIT && !_.isEmpty(additionalButtons)) {
|
|
1191
|
-
formButtons
|
|
1235
|
+
formButtons = <Toolbar className="justify-end flex-wrap gap-2">
|
|
1192
1236
|
{additionalButtons}
|
|
1193
|
-
</Toolbar
|
|
1237
|
+
</Toolbar>;
|
|
1194
1238
|
}
|
|
1195
|
-
if (!_.isEmpty(
|
|
1196
|
-
fab = <
|
|
1197
|
-
|
|
1239
|
+
if (showAncillaryButtons && !_.isEmpty(getAncillaryButtons())) {
|
|
1240
|
+
fab = <Animated.View style={fabAnimatedStyle}>
|
|
1241
|
+
<DynamicFab
|
|
1242
|
+
buttons={getAncillaryButtons()}
|
|
1198
1243
|
collapseOnPress={false}
|
|
1199
1244
|
className="bottom-[55px]"
|
|
1200
|
-
|
|
1245
|
+
tooltip="Scroll to Ancillary Item"
|
|
1246
|
+
/>
|
|
1247
|
+
</Animated.View>;
|
|
1201
1248
|
}
|
|
1202
1249
|
}
|
|
1203
1250
|
|
|
@@ -1412,6 +1459,8 @@ function Form(props) {
|
|
|
1412
1459
|
pb-1
|
|
1413
1460
|
web:min-h-[${minHeight}px]
|
|
1414
1461
|
`}
|
|
1462
|
+
onScroll={onScroll}
|
|
1463
|
+
scrollEventThrottle={16 /* ms */}
|
|
1415
1464
|
contentContainerStyle={{
|
|
1416
1465
|
// height: '100%',
|
|
1417
1466
|
}}
|
|
@@ -1420,11 +1469,16 @@ function Form(props) {
|
|
|
1420
1469
|
{modeHeader}
|
|
1421
1470
|
{formHeader}
|
|
1422
1471
|
{formButtons}
|
|
1472
|
+
{showAncillaryButtons && !_.isEmpty(getAncillaryButtons()) &&
|
|
1473
|
+
<Toolbar className="justify-start flex-wrap gap-2">
|
|
1474
|
+
<Text>Scroll:</Text>
|
|
1475
|
+
{buildAdditionalButtons(_.omitBy(getAncillaryButtons(), (btnConfig) => btnConfig.reference === 'scrollToTop'))}
|
|
1476
|
+
</Toolbar>}
|
|
1423
1477
|
{editor}
|
|
1424
1478
|
</ScrollView>}
|
|
1425
1479
|
|
|
1426
1480
|
{footer}
|
|
1427
|
-
{fab}
|
|
1481
|
+
{isFabVisible && fab}
|
|
1428
1482
|
|
|
1429
1483
|
</>}
|
|
1430
1484
|
</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
|
}
|
|
@@ -50,6 +50,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
50
50
|
newEntityDisplayValue,
|
|
51
51
|
newEntityDisplayProperty, // in case the field to set for newEntityDisplayValue is different from model
|
|
52
52
|
defaultValues,
|
|
53
|
+
initialEditorMode = EDITOR_MODE__VIEW,
|
|
53
54
|
stayInEditModeOnSelectionChange = false,
|
|
54
55
|
|
|
55
56
|
// withComponent
|
|
@@ -81,7 +82,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
81
82
|
listeners = useRef({}),
|
|
82
83
|
editorStateRef = useRef(),
|
|
83
84
|
newEntityDisplayValueRef = useRef(),
|
|
84
|
-
editorModeRef = useRef(
|
|
85
|
+
editorModeRef = useRef(initialEditorMode),
|
|
85
86
|
isIgnoreNextSelectionChangeRef = useRef(false),
|
|
86
87
|
[currentRecord, setCurrentRecord] = useState(null),
|
|
87
88
|
[isAdding, setIsAdding] = useState(false),
|
|
@@ -57,6 +57,7 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
57
57
|
canRecordBeEdited,
|
|
58
58
|
canRecordBeDeleted,
|
|
59
59
|
canRecordBeDuplicated,
|
|
60
|
+
canProceedWithCrud, // fn returns bool on if the CRUD operation can proceed
|
|
60
61
|
...propsToPass
|
|
61
62
|
} = props,
|
|
62
63
|
{
|
|
@@ -209,7 +210,12 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
209
210
|
case ADD:
|
|
210
211
|
key = 'addBtn';
|
|
211
212
|
text = 'Add';
|
|
212
|
-
handler = (parent, e) =>
|
|
213
|
+
handler = (parent, e) => {
|
|
214
|
+
if (canProceedWithCrud && !canProceedWithCrud()) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
onAdd();
|
|
218
|
+
};
|
|
213
219
|
icon = Plus;
|
|
214
220
|
if (isNoSelectorSelected() ||
|
|
215
221
|
(isTree && isEmptySelection())
|
|
@@ -220,7 +226,12 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
220
226
|
case EDIT:
|
|
221
227
|
key = 'editBtn';
|
|
222
228
|
text = 'Edit';
|
|
223
|
-
handler = (parent, e) =>
|
|
229
|
+
handler = (parent, e) => {
|
|
230
|
+
if (canProceedWithCrud && !canProceedWithCrud()) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
onEdit();
|
|
234
|
+
};
|
|
224
235
|
icon = Edit;
|
|
225
236
|
if (isNoSelectorSelected() ||
|
|
226
237
|
isEmptySelection() ||
|
|
@@ -235,7 +246,12 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
235
246
|
key = 'deleteBtn';
|
|
236
247
|
text = 'Delete';
|
|
237
248
|
handler = onDelete;
|
|
238
|
-
handler = (parent, e) =>
|
|
249
|
+
handler = (parent, e) => {
|
|
250
|
+
if (canProceedWithCrud && !canProceedWithCrud()) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
onDelete();
|
|
254
|
+
};
|
|
239
255
|
icon = Trash;
|
|
240
256
|
if (isNoSelectorSelected() ||
|
|
241
257
|
isEmptySelection() ||
|
|
@@ -280,7 +296,12 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
280
296
|
case DUPLICATE:
|
|
281
297
|
key = 'duplicateBtn';
|
|
282
298
|
text = 'Duplicate';
|
|
283
|
-
handler = (parent, e) =>
|
|
299
|
+
handler = (parent, e) => {
|
|
300
|
+
if (canProceedWithCrud && !canProceedWithCrud()) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
onDuplicate();
|
|
304
|
+
};
|
|
284
305
|
icon = Duplicate;
|
|
285
306
|
isDisabled = !selection.length || selection.length !== 1;
|
|
286
307
|
if (isNoSelectorSelected() ||
|
|
@@ -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) =>
|
|
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) =>
|
|
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
|
-
|
|
401
|
+
|
|
402
|
+
const currentTab = tabs[currentTabIx];
|
|
403
|
+
if (!currentTab.content && !currentTab.items) {
|
|
402
404
|
return null;
|
|
403
405
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
+
|
|
407
|
+
if (currentTab.content) {
|
|
408
|
+
return currentTab.content;
|
|
406
409
|
}
|
|
407
|
-
return _.map(
|
|
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.
|
|
21
|
-
triggerClassName += ' ' + props.
|
|
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 '../
|
|
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
|
-
|
|
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
|
-
|
|
328
|
+
setAncillaryButtons([]);
|
|
305
329
|
if (ancillaryItems.length) {
|
|
306
330
|
|
|
307
331
|
// add the "scroll to top" button
|
|
308
|
-
|
|
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
|
-
|
|
354
|
+
getAncillaryButtons().push({
|
|
328
355
|
icon,
|
|
329
|
-
|
|
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}
|
|
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
|
-
|
|
383
|
-
|
|
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(
|
|
411
|
-
fab = <
|
|
412
|
-
|
|
457
|
+
if (showAncillaryButtons && !_.isEmpty(getAncillaryButtons())) {
|
|
458
|
+
fab = <Animated.View style={fabAnimatedStyle}>
|
|
459
|
+
<DynamicFab
|
|
460
|
+
buttons={getAncillaryButtons()}
|
|
413
461
|
collapseOnPress={false}
|
|
414
|
-
|
|
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
|
-
{
|
|
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>;
|
|
@@ -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
|
-
|
|
19
|
-
parent: self,
|
|
20
|
-
reference: key,
|
|
21
|
-
text,
|
|
22
|
-
icon,
|
|
23
|
-
isDisabled,
|
|
24
|
-
tooltip,
|
|
25
|
-
color,
|
|
14
|
+
...configToPass,
|
|
26
15
|
};
|
|
27
|
-
|
|
28
|
-
buttonProps.
|
|
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;
|
|
@@ -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
|
-
};
|