@onehat/ui 0.4.60 → 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 +108 -15
- 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/Icons/ArrowUp.js +11 -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 +142 -31
- package/src/Components/index.js +2 -0
- package/src/Functions/buildAdditionalButtons.js +8 -15
- package/src/Functions/isValidDate.js +5 -0
- package/src/Functions/urlize.js +6 -0
- 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,6 +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';
|
|
48
|
+
import DynamicFab from '../Fab/DynamicFab.js';
|
|
43
49
|
import AngleLeft from '../Icons/AngleLeft.js';
|
|
44
50
|
import Eye from '../Icons/Eye.js';
|
|
45
51
|
import Rotate from '../Icons/Rotate.js';
|
|
@@ -47,9 +53,9 @@ import Pencil from '../Icons/Pencil.js';
|
|
|
47
53
|
import Plus from '../Icons/Plus.js';
|
|
48
54
|
import FloppyDiskRegular from '../Icons/FloppyDiskRegular.js';
|
|
49
55
|
import Trash from '../Icons/Trash.js';
|
|
56
|
+
import ArrowUp from '../Icons/ArrowUp.js';
|
|
50
57
|
import Xmark from '../Icons/Xmark.js';
|
|
51
58
|
import Check from '../Icons/Check.js';
|
|
52
|
-
|
|
53
59
|
import Footer from '../Layout/Footer.js';
|
|
54
60
|
import Label from '../Form/Label.js';
|
|
55
61
|
import _ from 'lodash';
|
|
@@ -70,12 +76,15 @@ import _ from 'lodash';
|
|
|
70
76
|
// EDITOR_TYPE__PLAIN
|
|
71
77
|
// Form is embedded on screen in some other way. Mainly use startingValues, items, validator
|
|
72
78
|
|
|
79
|
+
const FAB_FADE_TIME = 300; // ms
|
|
80
|
+
|
|
73
81
|
function Form(props) {
|
|
74
82
|
const {
|
|
75
83
|
editorType = EDITOR_TYPE__WINDOWED, // EDITOR_TYPE__INLINE | EDITOR_TYPE__WINDOWED | EDITOR_TYPE__SIDE | EDITOR_TYPE__SMART | EDITOR_TYPE__PLAIN
|
|
76
84
|
startingValues = {},
|
|
77
85
|
items = [], // Columns, FieldSets, Fields, etc to define the form
|
|
78
86
|
ancillaryItems = [], // additional items which are not controllable form elements, but should appear in the form
|
|
87
|
+
showAncillaryButtons = false,
|
|
79
88
|
columnDefaults = {}, // defaults for each Column defined in items (above)
|
|
80
89
|
columnsConfig, // Which columns are shown in Grid, so the inline editor can match. Used only for EDITOR_TYPE__INLINE
|
|
81
90
|
validator, // custom validator, mainly for EDITOR_TYPE__PLAIN
|
|
@@ -138,6 +147,14 @@ function Form(props) {
|
|
|
138
147
|
|
|
139
148
|
} = props,
|
|
140
149
|
formRef = useRef(),
|
|
150
|
+
ancillaryItemsRef = useRef({}),
|
|
151
|
+
ancillaryButtons = useRef([]),
|
|
152
|
+
setAncillaryButtons = (array) => {
|
|
153
|
+
ancillaryButtons.current = array;
|
|
154
|
+
},
|
|
155
|
+
getAncillaryButtons = () => {
|
|
156
|
+
return ancillaryButtons.current;
|
|
157
|
+
},
|
|
141
158
|
styles = UiGlobals.styles,
|
|
142
159
|
record = props.record?.length === 1 ? props.record[0] : props.record;
|
|
143
160
|
|
|
@@ -155,6 +172,14 @@ function Form(props) {
|
|
|
155
172
|
forceUpdate = useForceUpdate(),
|
|
156
173
|
[previousRecord, setPreviousRecord] = useState(record),
|
|
157
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
|
+
}),
|
|
158
183
|
initialValues = _.merge(startingValues, (record && !record.isDestroyed ? record.submitValues : {})),
|
|
159
184
|
defaultValues = isMultiple ? getNullFieldValues(initialValues, Repository) : initialValues, // when multiple entities, set all default values to null
|
|
160
185
|
validatorToUse = validator || (isMultiple ? disableRequiredYupFields(Repository?.schema?.model?.validator) : Repository?.schema?.model?.validator) || yup.object(),
|
|
@@ -899,12 +924,23 @@ function Form(props) {
|
|
|
899
924
|
},
|
|
900
925
|
buildAncillary = () => {
|
|
901
926
|
const components = [];
|
|
927
|
+
setAncillaryButtons([]);
|
|
902
928
|
if (ancillaryItems.length) {
|
|
929
|
+
|
|
930
|
+
// add the "scroll to top" button
|
|
931
|
+
getAncillaryButtons().push({
|
|
932
|
+
icon: ArrowUp,
|
|
933
|
+
reference: 'scrollToTop',
|
|
934
|
+
onPress: () => scrollToAncillaryItem(0),
|
|
935
|
+
tooltip: 'Scroll to top',
|
|
936
|
+
});
|
|
937
|
+
|
|
903
938
|
_.each(ancillaryItems, (item, ix) => {
|
|
904
939
|
let {
|
|
905
940
|
type,
|
|
906
941
|
title = null,
|
|
907
942
|
description = null,
|
|
943
|
+
icon,
|
|
908
944
|
selectorId,
|
|
909
945
|
selectorSelectedField,
|
|
910
946
|
...itemPropsToPass
|
|
@@ -912,6 +948,15 @@ function Form(props) {
|
|
|
912
948
|
if (isMultiple && type !== 'Attachments') {
|
|
913
949
|
return;
|
|
914
950
|
}
|
|
951
|
+
if (icon) {
|
|
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({
|
|
955
|
+
icon,
|
|
956
|
+
onPress: () => scrollToAncillaryItem(ix +1), // offset for the "scroll to top" button
|
|
957
|
+
tooltip: title,
|
|
958
|
+
});
|
|
959
|
+
}
|
|
915
960
|
if (type.match(/Grid/) && !itemPropsToPass.h) {
|
|
916
961
|
itemPropsToPass.h = 400;
|
|
917
962
|
}
|
|
@@ -938,6 +983,9 @@ function Form(props) {
|
|
|
938
983
|
${styles.FORM_ANCILLARY_TITLE_CLASSNAME}
|
|
939
984
|
`}
|
|
940
985
|
>{title}</Text>;
|
|
986
|
+
if (icon) {
|
|
987
|
+
title = <HStack className="items-center"><Icon as={icon} className="w-[32px] h-[32px] mr-2" />{title}</HStack>
|
|
988
|
+
}
|
|
941
989
|
}
|
|
942
990
|
if (description) {
|
|
943
991
|
description = <Text
|
|
@@ -949,6 +997,7 @@ function Form(props) {
|
|
|
949
997
|
>{description}</Text>;
|
|
950
998
|
}
|
|
951
999
|
components.push(<VStack
|
|
1000
|
+
ref={(el) => (ancillaryItemsRef.current[ix +1 /* offset for "scroll to top" */] = el)}
|
|
952
1001
|
key={'ancillary-' + ix}
|
|
953
1002
|
className={`
|
|
954
1003
|
Form-VStack12
|
|
@@ -996,7 +1045,31 @@ function Form(props) {
|
|
|
996
1045
|
}
|
|
997
1046
|
|
|
998
1047
|
setContainerWidth(e.nativeEvent.layout.width);
|
|
999
|
-
}
|
|
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
|
+
);
|
|
1000
1073
|
|
|
1001
1074
|
useEffect(() => {
|
|
1002
1075
|
if (skipAll) {
|
|
@@ -1081,13 +1154,15 @@ function Form(props) {
|
|
|
1081
1154
|
style.maxHeight = maxHeight;
|
|
1082
1155
|
}
|
|
1083
1156
|
|
|
1084
|
-
const formButtons = [];
|
|
1085
1157
|
let modeHeader = null,
|
|
1158
|
+
formButtons = null,
|
|
1159
|
+
scrollButtons = null,
|
|
1086
1160
|
footer = null,
|
|
1087
1161
|
footerButtons = null,
|
|
1088
1162
|
formComponents,
|
|
1089
1163
|
editor,
|
|
1090
1164
|
additionalButtons,
|
|
1165
|
+
fab = null,
|
|
1091
1166
|
isSaveDisabled = false,
|
|
1092
1167
|
isSubmitDisabled = false,
|
|
1093
1168
|
showDeleteBtn = false,
|
|
@@ -1101,13 +1176,13 @@ function Form(props) {
|
|
|
1101
1176
|
// create editor
|
|
1102
1177
|
if (editorType === EDITOR_TYPE__INLINE) {
|
|
1103
1178
|
editor = buildFromColumnsConfig();
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1179
|
+
// } else if (editorType === EDITOR_TYPE__PLAIN) {
|
|
1180
|
+
// formComponents = buildFromItems();
|
|
1181
|
+
// const formAncillaryComponents = buildAncillary();
|
|
1182
|
+
// editor = <>
|
|
1183
|
+
// <VStack className="p-4">{formComponents}</VStack>
|
|
1184
|
+
// <VStack className="pt-4">{formAncillaryComponents}</VStack>
|
|
1185
|
+
// </>;
|
|
1111
1186
|
} else {
|
|
1112
1187
|
formComponents = buildFromItems();
|
|
1113
1188
|
const formAncillaryComponents = buildAncillary();
|
|
@@ -1157,14 +1232,22 @@ function Form(props) {
|
|
|
1157
1232
|
</Toolbar>;
|
|
1158
1233
|
}
|
|
1159
1234
|
if (getEditorMode() === EDITOR_MODE__EDIT && !_.isEmpty(additionalButtons)) {
|
|
1160
|
-
formButtons
|
|
1235
|
+
formButtons = <Toolbar className="justify-end flex-wrap gap-2">
|
|
1161
1236
|
{additionalButtons}
|
|
1162
|
-
</Toolbar
|
|
1237
|
+
</Toolbar>;
|
|
1238
|
+
}
|
|
1239
|
+
if (showAncillaryButtons && !_.isEmpty(getAncillaryButtons())) {
|
|
1240
|
+
fab = <Animated.View style={fabAnimatedStyle}>
|
|
1241
|
+
<DynamicFab
|
|
1242
|
+
buttons={getAncillaryButtons()}
|
|
1243
|
+
collapseOnPress={false}
|
|
1244
|
+
className="bottom-[55px]"
|
|
1245
|
+
tooltip="Scroll to Ancillary Item"
|
|
1246
|
+
/>
|
|
1247
|
+
</Animated.View>;
|
|
1163
1248
|
}
|
|
1164
1249
|
}
|
|
1165
1250
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
1251
|
// create footer
|
|
1169
1252
|
if (!formState.isValid) {
|
|
1170
1253
|
isSaveDisabled = true;
|
|
@@ -1356,6 +1439,7 @@ function Form(props) {
|
|
|
1356
1439
|
|
|
1357
1440
|
let className = props.className || '';
|
|
1358
1441
|
className += ' Form-VStackNative';
|
|
1442
|
+
const scrollToTopAnchor = <Box ref={(el) => (ancillaryItemsRef.current[0] = el)} className="h-0" />;
|
|
1359
1443
|
return <VStackNative
|
|
1360
1444
|
ref={formRef}
|
|
1361
1445
|
{...testProps(self)}
|
|
@@ -1375,17 +1459,26 @@ function Form(props) {
|
|
|
1375
1459
|
pb-1
|
|
1376
1460
|
web:min-h-[${minHeight}px]
|
|
1377
1461
|
`}
|
|
1462
|
+
onScroll={onScroll}
|
|
1463
|
+
scrollEventThrottle={16 /* ms */}
|
|
1378
1464
|
contentContainerStyle={{
|
|
1379
1465
|
// height: '100%',
|
|
1380
1466
|
}}
|
|
1381
1467
|
>
|
|
1468
|
+
{scrollToTopAnchor}
|
|
1382
1469
|
{modeHeader}
|
|
1383
1470
|
{formHeader}
|
|
1384
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>}
|
|
1385
1477
|
{editor}
|
|
1386
1478
|
</ScrollView>}
|
|
1387
1479
|
|
|
1388
1480
|
{footer}
|
|
1481
|
+
{isFabVisible && fab}
|
|
1389
1482
|
|
|
1390
1483
|
</>}
|
|
1391
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}
|
|
@@ -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
|
|
@@ -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,16 +1,24 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, isValidElement, } from 'react';
|
|
1
|
+
import { useEffect, useCallback, useRef, useState, isValidElement, } from 'react';
|
|
2
2
|
import {
|
|
3
|
+
Box,
|
|
3
4
|
HStack,
|
|
5
|
+
Icon,
|
|
4
6
|
ScrollView,
|
|
5
7
|
Text,
|
|
6
8
|
VStack,
|
|
7
9
|
VStackNative,
|
|
8
10
|
} from '@project-components/Gluestack';
|
|
11
|
+
import Animated, {
|
|
12
|
+
useSharedValue,
|
|
13
|
+
useAnimatedStyle,
|
|
14
|
+
withTiming,
|
|
15
|
+
} from 'react-native-reanimated';
|
|
9
16
|
import {
|
|
10
17
|
EDIT,
|
|
11
18
|
} from '../../Constants/Commands.js';
|
|
12
19
|
import {
|
|
13
20
|
EDITOR_TYPE__SIDE,
|
|
21
|
+
EDITOR_TYPE__SMART,
|
|
14
22
|
} from '../../Constants/Editor.js';
|
|
15
23
|
import {
|
|
16
24
|
extractCssPropertyFromClassName,
|
|
@@ -24,18 +32,23 @@ import inArray from '../../Functions/inArray.js';
|
|
|
24
32
|
import getComponentFromType from '../../Functions/getComponentFromType.js';
|
|
25
33
|
import buildAdditionalButtons from '../../Functions/buildAdditionalButtons.js';
|
|
26
34
|
import testProps from '../../Functions/testProps.js';
|
|
35
|
+
import DynamicFab from '../Fab/DynamicFab.js';
|
|
27
36
|
import Toolbar from '../Toolbar/Toolbar.js';
|
|
37
|
+
import ArrowUp from '../Icons/ArrowUp.js';
|
|
28
38
|
import Button from '../Buttons/Button.js';
|
|
29
39
|
import Label from '../Form/Label.js';
|
|
30
40
|
import Pencil from '../Icons/Pencil.js';
|
|
31
41
|
import Footer from '../Layout/Footer.js';
|
|
32
42
|
import _ from 'lodash';
|
|
33
43
|
|
|
44
|
+
const FAB_FADE_TIME = 300; // ms
|
|
45
|
+
|
|
34
46
|
function Viewer(props) {
|
|
35
47
|
const {
|
|
36
48
|
viewerCanDelete = false,
|
|
37
49
|
items = [], // Columns, FieldSets, Fields, etc to define the form
|
|
38
50
|
ancillaryItems = [], // additional items which are not controllable form elements, but should appear in the form
|
|
51
|
+
showAncillaryButtons = false,
|
|
39
52
|
columnDefaults = {}, // defaults for each Column defined in items (above)
|
|
40
53
|
record,
|
|
41
54
|
additionalViewButtons,
|
|
@@ -66,9 +79,26 @@ function Viewer(props) {
|
|
|
66
79
|
|
|
67
80
|
} = props,
|
|
68
81
|
scrollViewRef = useRef(),
|
|
82
|
+
ancillaryItemsRef = useRef({}),
|
|
83
|
+
ancillaryButtons = useRef([]),
|
|
84
|
+
setAncillaryButtons = (array) => {
|
|
85
|
+
ancillaryButtons.current = array;
|
|
86
|
+
},
|
|
87
|
+
getAncillaryButtons = () => {
|
|
88
|
+
return ancillaryButtons.current;
|
|
89
|
+
},
|
|
69
90
|
isMultiple = _.isArray(record),
|
|
70
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
|
+
}),
|
|
71
100
|
isSideEditor = editorType === EDITOR_TYPE__SIDE,
|
|
101
|
+
isSmartEditor = editorType === EDITOR_TYPE__SMART,
|
|
72
102
|
styles = UiGlobals.styles,
|
|
73
103
|
flex = props.flex || 1,
|
|
74
104
|
buildFromItems = () => {
|
|
@@ -295,17 +325,39 @@ function Viewer(props) {
|
|
|
295
325
|
},
|
|
296
326
|
buildAncillary = () => {
|
|
297
327
|
const components = [];
|
|
328
|
+
setAncillaryButtons([]);
|
|
298
329
|
if (ancillaryItems.length) {
|
|
330
|
+
|
|
331
|
+
// add the "scroll to top" button
|
|
332
|
+
getAncillaryButtons().push({
|
|
333
|
+
icon: ArrowUp,
|
|
334
|
+
key: 'scrollToTop',
|
|
335
|
+
reference: 'scrollToTop',
|
|
336
|
+
onPress: () => scrollToAncillaryItem(0),
|
|
337
|
+
tooltip: 'Scroll to top',
|
|
338
|
+
});
|
|
339
|
+
|
|
299
340
|
_.each(ancillaryItems, (item, ix) => {
|
|
300
341
|
let {
|
|
301
342
|
type,
|
|
302
343
|
title = null,
|
|
344
|
+
icon,
|
|
303
345
|
selectorId = null,
|
|
304
346
|
...itemPropsToPass
|
|
305
347
|
} = item;
|
|
306
348
|
if (isMultiple && type !== 'Attachments') {
|
|
307
349
|
return;
|
|
308
350
|
}
|
|
351
|
+
if (icon) {
|
|
352
|
+
// NOTE: this assumes that if one Ancillary item has an icon, they all do.
|
|
353
|
+
// If they don't, the ix will be wrong.
|
|
354
|
+
getAncillaryButtons().push({
|
|
355
|
+
icon,
|
|
356
|
+
key: 'ancillary-' + ix,
|
|
357
|
+
onPress: () => { scrollToAncillaryItem(ix +1)}, // offset for the "scroll to top" button
|
|
358
|
+
tooltip: title,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
309
361
|
if (type.match(/Grid/) && !itemPropsToPass.h) {
|
|
310
362
|
itemPropsToPass.h = 400;
|
|
311
363
|
}
|
|
@@ -334,15 +386,49 @@ function Viewer(props) {
|
|
|
334
386
|
title += ' for ' + record.displayValue;
|
|
335
387
|
}
|
|
336
388
|
title = <Text className={`${styles.VIEWER_ANCILLARY_FONTSIZE} font-bold`}>{title}</Text>;
|
|
389
|
+
if (icon) {
|
|
390
|
+
title = <HStack className="items-center"><Icon as={icon} className="w-[32px] h-[32px] mr-2" />{title}</HStack>
|
|
391
|
+
}
|
|
337
392
|
}
|
|
338
|
-
components.push(<VStack
|
|
393
|
+
components.push(<VStack
|
|
394
|
+
ref={(el) => (ancillaryItemsRef.current[ix +1 /* offset for "scroll to top" */] = el)}
|
|
395
|
+
key={'ancillary-' + ix}
|
|
396
|
+
className="my-3"
|
|
397
|
+
>
|
|
398
|
+
{title}
|
|
399
|
+
{element}
|
|
400
|
+
</VStack>);
|
|
339
401
|
});
|
|
340
402
|
}
|
|
341
403
|
return components;
|
|
342
404
|
},
|
|
343
405
|
onLayout = (e) => {
|
|
344
406
|
setContainerWidth(e.nativeEvent.layout.width);
|
|
345
|
-
}
|
|
407
|
+
},
|
|
408
|
+
scrollToAncillaryItem = (ix) => {
|
|
409
|
+
ancillaryItemsRef.current[ix]?.scrollIntoView({
|
|
410
|
+
behavior: 'smooth',
|
|
411
|
+
block: 'start',
|
|
412
|
+
});
|
|
413
|
+
},
|
|
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
|
+
);
|
|
346
432
|
|
|
347
433
|
useEffect(() => {
|
|
348
434
|
if (viewerSetup && record?.getSubmitValues) {
|
|
@@ -356,15 +442,27 @@ function Viewer(props) {
|
|
|
356
442
|
|
|
357
443
|
const
|
|
358
444
|
showDeleteBtn = onDelete && viewerCanDelete,
|
|
359
|
-
showCloseBtn = !isSideEditor
|
|
445
|
+
showCloseBtn = !isSideEditor && !isSmartEditor && onClose,
|
|
446
|
+
showFooter = (showDeleteBtn || showCloseBtn);
|
|
360
447
|
let additionalButtons = null,
|
|
361
448
|
viewerComponents = null,
|
|
362
|
-
ancillaryComponents = null
|
|
449
|
+
ancillaryComponents = null,
|
|
450
|
+
fab = null;
|
|
363
451
|
|
|
364
452
|
if (containerWidth) { // we need to render this component twice in order to get the container width. Skip this on first render
|
|
365
453
|
additionalButtons = buildAdditionalButtons(additionalViewButtons);
|
|
366
454
|
viewerComponents = buildFromItems();
|
|
367
455
|
ancillaryComponents = buildAncillary();
|
|
456
|
+
|
|
457
|
+
if (showAncillaryButtons && !_.isEmpty(getAncillaryButtons())) {
|
|
458
|
+
fab = <Animated.View style={fabAnimatedStyle}>
|
|
459
|
+
<DynamicFab
|
|
460
|
+
buttons={getAncillaryButtons()}
|
|
461
|
+
collapseOnPress={false}
|
|
462
|
+
tooltip="Scroll to Ancillary Item"
|
|
463
|
+
/>
|
|
464
|
+
</Animated.View>;
|
|
465
|
+
}
|
|
368
466
|
}
|
|
369
467
|
|
|
370
468
|
let canEdit = true;
|
|
@@ -385,6 +483,33 @@ function Viewer(props) {
|
|
|
385
483
|
className += ' ' + props.className;
|
|
386
484
|
}
|
|
387
485
|
|
|
486
|
+
const footer = showFooter ?
|
|
487
|
+
<Footer className="justify-end">
|
|
488
|
+
{showDeleteBtn &&
|
|
489
|
+
<HStack className="flex-1 justify-start">
|
|
490
|
+
<Button
|
|
491
|
+
{...testProps('deleteBtn')}
|
|
492
|
+
key="deleteBtn"
|
|
493
|
+
onPress={onDelete}
|
|
494
|
+
className={`
|
|
495
|
+
text-white
|
|
496
|
+
bg-warning-500
|
|
497
|
+
hover:bg-warning-600
|
|
498
|
+
`}
|
|
499
|
+
text="Delete"
|
|
500
|
+
/>
|
|
501
|
+
</HStack>}
|
|
502
|
+
{showCloseBtn &&
|
|
503
|
+
<Button
|
|
504
|
+
{...testProps('closeBtn')}
|
|
505
|
+
key="closeBtn"
|
|
506
|
+
onPress={onClose}
|
|
507
|
+
className="text-white"
|
|
508
|
+
text="Close"
|
|
509
|
+
/>}
|
|
510
|
+
</Footer> : null;
|
|
511
|
+
|
|
512
|
+
const scrollToTopAnchor = <Box ref={(el) => (ancillaryItemsRef.current[0] = el)} className="h-0" />;
|
|
388
513
|
return <VStackNative
|
|
389
514
|
{...testProps(self)}
|
|
390
515
|
style={style}
|
|
@@ -396,6 +521,8 @@ function Viewer(props) {
|
|
|
396
521
|
<ScrollView
|
|
397
522
|
_web={{ height: 1 }}
|
|
398
523
|
ref={scrollViewRef}
|
|
524
|
+
onScroll={onScroll}
|
|
525
|
+
scrollEventThrottle={16 /* ms */}
|
|
399
526
|
className={`
|
|
400
527
|
Viewer-ScrollView
|
|
401
528
|
w-full
|
|
@@ -403,6 +530,7 @@ function Viewer(props) {
|
|
|
403
530
|
flex-1
|
|
404
531
|
`}
|
|
405
532
|
>
|
|
533
|
+
{scrollToTopAnchor}
|
|
406
534
|
{canEdit && onEditMode &&
|
|
407
535
|
<Toolbar className="justify-end">
|
|
408
536
|
<HStack className="flex-1 items-center">
|
|
@@ -425,40 +553,23 @@ function Viewer(props) {
|
|
|
425
553
|
</Toolbar>}
|
|
426
554
|
|
|
427
555
|
{!_.isEmpty(additionalButtons) &&
|
|
428
|
-
<Toolbar className="justify-end flex-wrap">
|
|
556
|
+
<Toolbar className="justify-end flex-wrap gap-2">
|
|
429
557
|
{additionalButtons}
|
|
430
558
|
</Toolbar>}
|
|
431
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
|
+
|
|
432
566
|
{containerWidth >= styles.FORM_ONE_COLUMN_THRESHOLD ? <HStack className="Viewer-formComponents-HStack p-4 gap-4 justify-center">{viewerComponents}</HStack> : null}
|
|
433
567
|
{containerWidth < styles.FORM_ONE_COLUMN_THRESHOLD ? <VStack className="Viewer-formComponents-VStack p-4">{viewerComponents}</VStack> : null}
|
|
434
568
|
<VStack className="Viewer-AncillaryComponents m-2 pt-4 px-2">{ancillaryComponents}</VStack>
|
|
435
569
|
</ScrollView>
|
|
436
570
|
|
|
437
|
-
{
|
|
438
|
-
|
|
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>}
|
|
571
|
+
{footer}
|
|
572
|
+
{isFabVisible && fab}
|
|
462
573
|
|
|
463
574
|
</>}
|
|
464
575
|
</VStackNative>;
|
package/src/Components/index.js
CHANGED
|
@@ -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,
|
|
@@ -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;
|