@onehat/ui 0.4.61 → 0.4.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +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/Field/Tag/Tag.js +2 -2
- package/src/Components/Form/Form.js +81 -26
- package/src/Components/Hoc/withData.js +16 -0
- package/src/Components/Hoc/withEditor.js +18 -1
- package/src/Components/Hoc/withPresetButtons.js +43 -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/Components/Window/UploadsDownloadsWindow.js +57 -52
- package/src/Constants/Commands.js +1 -0
- 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
|
|
@@ -61,8 +61,8 @@ function TagComponent(props) {
|
|
|
61
61
|
await repository.waitUntilDoneLoading();
|
|
62
62
|
}
|
|
63
63
|
let record = repository.getById(id); // first try to get from entities in memory
|
|
64
|
-
if (!record && repository.
|
|
65
|
-
record = await repository.
|
|
64
|
+
if (!record && repository.loadOneAdditionalEntity) {
|
|
65
|
+
record = await repository.loadOneAdditionalEntity(id);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
if (!record) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState, useRef, isValidElement, } from 'react';
|
|
1
|
+
import { useEffect, useCallback, useState, useRef, isValidElement, } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
Box,
|
|
4
4
|
HStack,
|
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
VStack,
|
|
9
9
|
VStackNative,
|
|
10
10
|
} from '@project-components/Gluestack';
|
|
11
|
+
import Animated, {
|
|
12
|
+
useSharedValue,
|
|
13
|
+
useAnimatedStyle,
|
|
14
|
+
withTiming,
|
|
15
|
+
} from 'react-native-reanimated';
|
|
11
16
|
import {
|
|
12
17
|
VIEW,
|
|
13
18
|
} from '../../Constants/Commands.js';
|
|
@@ -40,7 +45,7 @@ import testProps from '../../Functions/testProps.js';
|
|
|
40
45
|
import Toolbar from '../Toolbar/Toolbar.js';
|
|
41
46
|
import Button from '../Buttons/Button.js';
|
|
42
47
|
import IconButton from '../Buttons/IconButton.js';
|
|
43
|
-
import DynamicFab from '../
|
|
48
|
+
import DynamicFab from '../Fab/DynamicFab.js';
|
|
44
49
|
import AngleLeft from '../Icons/AngleLeft.js';
|
|
45
50
|
import Eye from '../Icons/Eye.js';
|
|
46
51
|
import Rotate from '../Icons/Rotate.js';
|
|
@@ -71,12 +76,16 @@ import _ from 'lodash';
|
|
|
71
76
|
// EDITOR_TYPE__PLAIN
|
|
72
77
|
// Form is embedded on screen in some other way. Mainly use startingValues, items, validator
|
|
73
78
|
|
|
79
|
+
const FAB_FADE_TIME = 300; // ms
|
|
80
|
+
|
|
74
81
|
function Form(props) {
|
|
75
82
|
const {
|
|
76
83
|
editorType = EDITOR_TYPE__WINDOWED, // EDITOR_TYPE__INLINE | EDITOR_TYPE__WINDOWED | EDITOR_TYPE__SIDE | EDITOR_TYPE__SMART | EDITOR_TYPE__PLAIN
|
|
77
84
|
startingValues = {},
|
|
78
85
|
items = [], // Columns, FieldSets, Fields, etc to define the form
|
|
86
|
+
isItemsCustomLayout = false,
|
|
79
87
|
ancillaryItems = [], // additional items which are not controllable form elements, but should appear in the form
|
|
88
|
+
showAncillaryButtons = false,
|
|
80
89
|
columnDefaults = {}, // defaults for each Column defined in items (above)
|
|
81
90
|
columnsConfig, // Which columns are shown in Grid, so the inline editor can match. Used only for EDITOR_TYPE__INLINE
|
|
82
91
|
validator, // custom validator, mainly for EDITOR_TYPE__PLAIN
|
|
@@ -140,7 +149,13 @@ function Form(props) {
|
|
|
140
149
|
} = props,
|
|
141
150
|
formRef = useRef(),
|
|
142
151
|
ancillaryItemsRef = useRef({}),
|
|
143
|
-
|
|
152
|
+
ancillaryButtons = useRef([]),
|
|
153
|
+
setAncillaryButtons = (array) => {
|
|
154
|
+
ancillaryButtons.current = array;
|
|
155
|
+
},
|
|
156
|
+
getAncillaryButtons = () => {
|
|
157
|
+
return ancillaryButtons.current;
|
|
158
|
+
},
|
|
144
159
|
styles = UiGlobals.styles,
|
|
145
160
|
record = props.record?.length === 1 ? props.record[0] : props.record;
|
|
146
161
|
|
|
@@ -158,6 +173,14 @@ function Form(props) {
|
|
|
158
173
|
forceUpdate = useForceUpdate(),
|
|
159
174
|
[previousRecord, setPreviousRecord] = useState(record),
|
|
160
175
|
[containerWidth, setContainerWidth] = useState(),
|
|
176
|
+
[isFabVisible, setIsFabVisible] = useState(false),
|
|
177
|
+
fabOpacity = useSharedValue(0),
|
|
178
|
+
fabAnimatedStyle = useAnimatedStyle(() => {
|
|
179
|
+
return {
|
|
180
|
+
opacity: withTiming(fabOpacity.value, { duration: FAB_FADE_TIME }), // Smooth fade animation
|
|
181
|
+
pointerEvents: fabOpacity.value > 0 ? 'auto' : 'none', // Disable interaction when invisible
|
|
182
|
+
};
|
|
183
|
+
}),
|
|
161
184
|
initialValues = _.merge(startingValues, (record && !record.isDestroyed ? record.submitValues : {})),
|
|
162
185
|
defaultValues = isMultiple ? getNullFieldValues(initialValues, Repository) : initialValues, // when multiple entities, set all default values to null
|
|
163
186
|
validatorToUse = validator || (isMultiple ? disableRequiredYupFields(Repository?.schema?.model?.validator) : Repository?.schema?.model?.validator) || yup.object(),
|
|
@@ -902,13 +925,15 @@ function Form(props) {
|
|
|
902
925
|
},
|
|
903
926
|
buildAncillary = () => {
|
|
904
927
|
const components = [];
|
|
905
|
-
|
|
928
|
+
setAncillaryButtons([]);
|
|
906
929
|
if (ancillaryItems.length) {
|
|
907
930
|
|
|
908
931
|
// add the "scroll to top" button
|
|
909
|
-
|
|
932
|
+
getAncillaryButtons().push({
|
|
910
933
|
icon: ArrowUp,
|
|
934
|
+
reference: 'scrollToTop',
|
|
911
935
|
onPress: () => scrollToAncillaryItem(0),
|
|
936
|
+
tooltip: 'Scroll to top',
|
|
912
937
|
});
|
|
913
938
|
|
|
914
939
|
_.each(ancillaryItems, (item, ix) => {
|
|
@@ -925,11 +950,12 @@ function Form(props) {
|
|
|
925
950
|
return;
|
|
926
951
|
}
|
|
927
952
|
if (icon) {
|
|
928
|
-
//
|
|
929
|
-
// If they don't, the ix will be wrong
|
|
930
|
-
|
|
953
|
+
// NOTE: this assumes that if one Ancillary item has an icon, they all do.
|
|
954
|
+
// If they don't, the ix will be wrong!
|
|
955
|
+
getAncillaryButtons().push({
|
|
931
956
|
icon,
|
|
932
957
|
onPress: () => scrollToAncillaryItem(ix +1), // offset for the "scroll to top" button
|
|
958
|
+
tooltip: title,
|
|
933
959
|
});
|
|
934
960
|
}
|
|
935
961
|
if (type.match(/Grid/) && !itemPropsToPass.h) {
|
|
@@ -959,7 +985,7 @@ function Form(props) {
|
|
|
959
985
|
`}
|
|
960
986
|
>{title}</Text>;
|
|
961
987
|
if (icon) {
|
|
962
|
-
title = <HStack className="items-center"><Icon as={icon}
|
|
988
|
+
title = <HStack className="items-center"><Icon as={icon} className="w-[32px] h-[32px] mr-2" />{title}</HStack>
|
|
963
989
|
}
|
|
964
990
|
}
|
|
965
991
|
if (description) {
|
|
@@ -988,12 +1014,6 @@ function Form(props) {
|
|
|
988
1014
|
}
|
|
989
1015
|
return components;
|
|
990
1016
|
},
|
|
991
|
-
scrollToAncillaryItem = (ix) => {
|
|
992
|
-
ancillaryItemsRef.current[ix]?.scrollIntoView({
|
|
993
|
-
behavior: 'smooth',
|
|
994
|
-
block: 'start',
|
|
995
|
-
});
|
|
996
|
-
},
|
|
997
1017
|
onSubmitError = (errors, e) => {
|
|
998
1018
|
if (editorType === EDITOR_TYPE__INLINE) {
|
|
999
1019
|
alert(errors.message);
|
|
@@ -1026,7 +1046,31 @@ function Form(props) {
|
|
|
1026
1046
|
}
|
|
1027
1047
|
|
|
1028
1048
|
setContainerWidth(e.nativeEvent.layout.width);
|
|
1029
|
-
}
|
|
1049
|
+
},
|
|
1050
|
+
scrollToAncillaryItem = (ix) => {
|
|
1051
|
+
ancillaryItemsRef.current[ix]?.scrollIntoView({
|
|
1052
|
+
behavior: 'smooth',
|
|
1053
|
+
block: 'start',
|
|
1054
|
+
});
|
|
1055
|
+
},
|
|
1056
|
+
onScroll = useCallback(
|
|
1057
|
+
_.debounce((e) => {
|
|
1058
|
+
if (!showAncillaryButtons) {
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
const
|
|
1062
|
+
scrollY = e.nativeEvent.contentOffset.y,
|
|
1063
|
+
isFabVisible = scrollY > 50;
|
|
1064
|
+
fabOpacity.value = isFabVisible ? 1 : 0;
|
|
1065
|
+
if (isFabVisible) {
|
|
1066
|
+
setIsFabVisible(true);
|
|
1067
|
+
} else {
|
|
1068
|
+
// delay removal from DOM until fade-out is complete
|
|
1069
|
+
setTimeout(() => setIsFabVisible(isFabVisible), FAB_FADE_TIME);
|
|
1070
|
+
}
|
|
1071
|
+
}, 100), // delay
|
|
1072
|
+
[]
|
|
1073
|
+
);
|
|
1030
1074
|
|
|
1031
1075
|
useEffect(() => {
|
|
1032
1076
|
if (skipAll) {
|
|
@@ -1111,8 +1155,9 @@ function Form(props) {
|
|
|
1111
1155
|
style.maxHeight = maxHeight;
|
|
1112
1156
|
}
|
|
1113
1157
|
|
|
1114
|
-
const formButtons = [];
|
|
1115
1158
|
let modeHeader = null,
|
|
1159
|
+
formButtons = null,
|
|
1160
|
+
scrollButtons = null,
|
|
1116
1161
|
footer = null,
|
|
1117
1162
|
footerButtons = null,
|
|
1118
1163
|
formComponents,
|
|
@@ -1143,8 +1188,8 @@ function Form(props) {
|
|
|
1143
1188
|
formComponents = buildFromItems();
|
|
1144
1189
|
const formAncillaryComponents = buildAncillary();
|
|
1145
1190
|
editor = <>
|
|
1146
|
-
{containerWidth >= styles.FORM_ONE_COLUMN_THRESHOLD ? <HStack className="Form-formComponents-HStack p-4 gap-4 justify-center">{formComponents}</HStack> : null}
|
|
1147
|
-
{containerWidth < styles.FORM_ONE_COLUMN_THRESHOLD ? <VStack className="Form-formComponents-VStack p-4">{formComponents}</VStack> : null}
|
|
1191
|
+
{containerWidth >= styles.FORM_ONE_COLUMN_THRESHOLD && !isItemsCustomLayout ? <HStack className="Form-formComponents-HStack p-4 gap-4 justify-center">{formComponents}</HStack> : null}
|
|
1192
|
+
{containerWidth < styles.FORM_ONE_COLUMN_THRESHOLD || isItemsCustomLayout ? <VStack className="Form-formComponents-VStack p-4">{formComponents}</VStack> : null}
|
|
1148
1193
|
{formAncillaryComponents.length ? <VStack className="Form-AncillaryComponents m-2 pt-4 px-2">{formAncillaryComponents}</VStack> : null}
|
|
1149
1194
|
</>;
|
|
1150
1195
|
|
|
@@ -1188,16 +1233,19 @@ function Form(props) {
|
|
|
1188
1233
|
</Toolbar>;
|
|
1189
1234
|
}
|
|
1190
1235
|
if (getEditorMode() === EDITOR_MODE__EDIT && !_.isEmpty(additionalButtons)) {
|
|
1191
|
-
formButtons
|
|
1236
|
+
formButtons = <Toolbar className="justify-end flex-wrap gap-2">
|
|
1192
1237
|
{additionalButtons}
|
|
1193
|
-
</Toolbar
|
|
1238
|
+
</Toolbar>;
|
|
1194
1239
|
}
|
|
1195
|
-
if (!_.isEmpty(
|
|
1196
|
-
fab = <
|
|
1197
|
-
|
|
1240
|
+
if (showAncillaryButtons && !_.isEmpty(getAncillaryButtons())) {
|
|
1241
|
+
fab = <Animated.View style={fabAnimatedStyle}>
|
|
1242
|
+
<DynamicFab
|
|
1243
|
+
buttons={getAncillaryButtons()}
|
|
1198
1244
|
collapseOnPress={false}
|
|
1199
1245
|
className="bottom-[55px]"
|
|
1200
|
-
|
|
1246
|
+
tooltip="Scroll to Ancillary Item"
|
|
1247
|
+
/>
|
|
1248
|
+
</Animated.View>;
|
|
1201
1249
|
}
|
|
1202
1250
|
}
|
|
1203
1251
|
|
|
@@ -1412,6 +1460,8 @@ function Form(props) {
|
|
|
1412
1460
|
pb-1
|
|
1413
1461
|
web:min-h-[${minHeight}px]
|
|
1414
1462
|
`}
|
|
1463
|
+
onScroll={onScroll}
|
|
1464
|
+
scrollEventThrottle={16 /* ms */}
|
|
1415
1465
|
contentContainerStyle={{
|
|
1416
1466
|
// height: '100%',
|
|
1417
1467
|
}}
|
|
@@ -1420,11 +1470,16 @@ function Form(props) {
|
|
|
1420
1470
|
{modeHeader}
|
|
1421
1471
|
{formHeader}
|
|
1422
1472
|
{formButtons}
|
|
1473
|
+
{showAncillaryButtons && !_.isEmpty(getAncillaryButtons()) &&
|
|
1474
|
+
<Toolbar className="justify-start flex-wrap gap-2">
|
|
1475
|
+
<Text>Scroll:</Text>
|
|
1476
|
+
{buildAdditionalButtons(_.omitBy(getAncillaryButtons(), (btnConfig) => btnConfig.reference === 'scrollToTop'))}
|
|
1477
|
+
</Toolbar>}
|
|
1423
1478
|
{editor}
|
|
1424
1479
|
</ScrollView>}
|
|
1425
1480
|
|
|
1426
1481
|
{footer}
|
|
1427
|
-
{fab}
|
|
1482
|
+
{isFabVisible && fab}
|
|
1428
1483
|
|
|
1429
1484
|
</>}
|
|
1430
1485
|
</VStackNative>;
|
|
@@ -99,6 +99,22 @@ export default function withData(WrappedComponent) {
|
|
|
99
99
|
|
|
100
100
|
}, []);
|
|
101
101
|
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!baseParams || !LocalRepository) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// If baseParams changes, re-load the Repository
|
|
108
|
+
if (LocalRepository.isLoaded && !_.isEqual(LocalRepository.getBaseParams(), baseParams)) {
|
|
109
|
+
LocalRepository.setBaseParams(baseParams);
|
|
110
|
+
|
|
111
|
+
if (LocalRepository.isRemote && !LocalRepository.isLoading) {
|
|
112
|
+
LocalRepository.load();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
}, [baseParams, LocalRepository]);
|
|
117
|
+
|
|
102
118
|
if (!isReady) {
|
|
103
119
|
return null;
|
|
104
120
|
}
|
|
@@ -29,6 +29,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
29
29
|
userCanEdit = true, // not permissions, but capability
|
|
30
30
|
userCanView = true,
|
|
31
31
|
canEditorViewOnly = false, // whether the editor can *ever* change state out of 'View' mode
|
|
32
|
+
canProceedWithCrud, // fn returns bool on if the CRUD operation can proceed
|
|
32
33
|
disableAdd = false,
|
|
33
34
|
disableEdit = false,
|
|
34
35
|
disableDelete = false,
|
|
@@ -50,6 +51,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
50
51
|
newEntityDisplayValue,
|
|
51
52
|
newEntityDisplayProperty, // in case the field to set for newEntityDisplayValue is different from model
|
|
52
53
|
defaultValues,
|
|
54
|
+
initialEditorMode = EDITOR_MODE__VIEW,
|
|
53
55
|
stayInEditModeOnSelectionChange = false,
|
|
54
56
|
|
|
55
57
|
// withComponent
|
|
@@ -81,7 +83,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
81
83
|
listeners = useRef({}),
|
|
82
84
|
editorStateRef = useRef(),
|
|
83
85
|
newEntityDisplayValueRef = useRef(),
|
|
84
|
-
editorModeRef = useRef(
|
|
86
|
+
editorModeRef = useRef(initialEditorMode),
|
|
85
87
|
isIgnoreNextSelectionChangeRef = useRef(false),
|
|
86
88
|
[currentRecord, setCurrentRecord] = useState(null),
|
|
87
89
|
[isAdding, setIsAdding] = useState(false),
|
|
@@ -152,6 +154,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
152
154
|
showPermissionsError(ADD);
|
|
153
155
|
return;
|
|
154
156
|
}
|
|
157
|
+
if (canProceedWithCrud && !canProceedWithCrud()) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
155
160
|
|
|
156
161
|
const selection = getSelection();
|
|
157
162
|
let addValues = values;
|
|
@@ -251,6 +256,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
251
256
|
showPermissionsError(EDIT);
|
|
252
257
|
return;
|
|
253
258
|
}
|
|
259
|
+
if (canProceedWithCrud && !canProceedWithCrud()) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
254
262
|
const selection = getSelection();
|
|
255
263
|
if (_.isEmpty(selection) || (_.isArray(selection) && (selection.length > 1 || selection[0]?.isDestroyed))) {
|
|
256
264
|
return;
|
|
@@ -270,6 +278,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
270
278
|
showPermissionsError(DELETE);
|
|
271
279
|
return;
|
|
272
280
|
}
|
|
281
|
+
if (canProceedWithCrud && !canProceedWithCrud()) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
273
284
|
let cb = null;
|
|
274
285
|
if (_.isFunction(args)) {
|
|
275
286
|
cb = args;
|
|
@@ -367,6 +378,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
367
378
|
showPermissionsError(VIEW);
|
|
368
379
|
return;
|
|
369
380
|
}
|
|
381
|
+
if (canProceedWithCrud && !canProceedWithCrud()) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
370
384
|
if (editorType === EDITOR_TYPE__INLINE) {
|
|
371
385
|
alert('Cannot view in inline editor.');
|
|
372
386
|
return; // inline editor doesn't have a view mode
|
|
@@ -394,6 +408,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
|
|
|
394
408
|
showPermissionsError(DUPLICATE);
|
|
395
409
|
return;
|
|
396
410
|
}
|
|
411
|
+
if (canProceedWithCrud && !canProceedWithCrud()) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
397
414
|
|
|
398
415
|
const selection = getSelection();
|
|
399
416
|
if (selection.length !== 1) {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
DUPLICATE,
|
|
9
9
|
PRINT,
|
|
10
10
|
UPLOAD_DOWNLOAD,
|
|
11
|
+
DOWNLOAD,
|
|
11
12
|
} from '../../Constants/Commands.js';
|
|
12
13
|
import Clipboard from '../Icons/Clipboard.js';
|
|
13
14
|
import Duplicate from '../Icons/Duplicate.js';
|
|
@@ -17,6 +18,7 @@ import Trash from '../Icons/Trash.js';
|
|
|
17
18
|
import Plus from '../Icons/Plus.js';
|
|
18
19
|
import Print from '../Icons/Print.js';
|
|
19
20
|
import UploadDownload from '../Icons/UploadDownload.js';
|
|
21
|
+
import Download from '../Icons/Download.js';
|
|
20
22
|
import inArray from '../../Functions/inArray.js';
|
|
21
23
|
import UploadsDownloadsWindow from '../Window/UploadsDownloadsWindow.js';
|
|
22
24
|
import _ from 'lodash';
|
|
@@ -33,6 +35,7 @@ const presetButtons = [
|
|
|
33
35
|
DUPLICATE,
|
|
34
36
|
// PRINT,
|
|
35
37
|
UPLOAD_DOWNLOAD,
|
|
38
|
+
DOWNLOAD,
|
|
36
39
|
];
|
|
37
40
|
|
|
38
41
|
export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
@@ -48,6 +51,7 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
48
51
|
contextMenuItems = [],
|
|
49
52
|
additionalToolbarButtons = [],
|
|
50
53
|
useUploadDownload = false,
|
|
54
|
+
useDownload = false,
|
|
51
55
|
uploadHeaders,
|
|
52
56
|
uploadParams,
|
|
53
57
|
onUpload,
|
|
@@ -176,6 +180,13 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
176
180
|
isDisabled = true;
|
|
177
181
|
}
|
|
178
182
|
break;
|
|
183
|
+
case DOWNLOAD:
|
|
184
|
+
if (!useDownload) {
|
|
185
|
+
isDisabled = true;
|
|
186
|
+
} else if (canUser && !(canUser(DOWNLOAD) || canUser(UPLOAD_DOWNLOAD))) { // check Permissions
|
|
187
|
+
isDisabled = true;
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
179
190
|
default:
|
|
180
191
|
}
|
|
181
192
|
return isDisabled;
|
|
@@ -209,7 +220,9 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
209
220
|
case ADD:
|
|
210
221
|
key = 'addBtn';
|
|
211
222
|
text = 'Add';
|
|
212
|
-
handler = (parent, e) =>
|
|
223
|
+
handler = (parent, e) => {
|
|
224
|
+
onAdd();
|
|
225
|
+
};
|
|
213
226
|
icon = Plus;
|
|
214
227
|
if (isNoSelectorSelected() ||
|
|
215
228
|
(isTree && isEmptySelection())
|
|
@@ -220,7 +233,9 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
220
233
|
case EDIT:
|
|
221
234
|
key = 'editBtn';
|
|
222
235
|
text = 'Edit';
|
|
223
|
-
handler = (parent, e) =>
|
|
236
|
+
handler = (parent, e) => {
|
|
237
|
+
onEdit();
|
|
238
|
+
};
|
|
224
239
|
icon = Edit;
|
|
225
240
|
if (isNoSelectorSelected() ||
|
|
226
241
|
isEmptySelection() ||
|
|
@@ -235,7 +250,9 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
235
250
|
key = 'deleteBtn';
|
|
236
251
|
text = 'Delete';
|
|
237
252
|
handler = onDelete;
|
|
238
|
-
handler = (parent, e) =>
|
|
253
|
+
handler = (parent, e) => {
|
|
254
|
+
onDelete();
|
|
255
|
+
};
|
|
239
256
|
icon = Trash;
|
|
240
257
|
if (isNoSelectorSelected() ||
|
|
241
258
|
isEmptySelection() ||
|
|
@@ -280,7 +297,9 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
280
297
|
case DUPLICATE:
|
|
281
298
|
key = 'duplicateBtn';
|
|
282
299
|
text = 'Duplicate';
|
|
283
|
-
handler = (parent, e) =>
|
|
300
|
+
handler = (parent, e) => {
|
|
301
|
+
onDuplicate();
|
|
302
|
+
};
|
|
284
303
|
icon = Duplicate;
|
|
285
304
|
isDisabled = !selection.length || selection.length !== 1;
|
|
286
305
|
if (isNoSelectorSelected() ||
|
|
@@ -302,6 +321,12 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
302
321
|
handler = (parent, e) => onUploadDownload();
|
|
303
322
|
icon = UploadDownload;
|
|
304
323
|
break;
|
|
324
|
+
case DOWNLOAD:
|
|
325
|
+
key = 'downloadBtn';
|
|
326
|
+
text = 'Download';
|
|
327
|
+
handler = (parent, e) => onDownload();
|
|
328
|
+
icon = Download;
|
|
329
|
+
break;
|
|
305
330
|
default:
|
|
306
331
|
}
|
|
307
332
|
return {
|
|
@@ -376,6 +401,20 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
|
|
|
376
401
|
/>,
|
|
377
402
|
onCancel: hideModal,
|
|
378
403
|
});
|
|
404
|
+
},
|
|
405
|
+
onDownload = () => {
|
|
406
|
+
showModal({
|
|
407
|
+
body: <UploadsDownloadsWindow
|
|
408
|
+
reference="downloads"
|
|
409
|
+
onClose={hideModal}
|
|
410
|
+
isDownloadOnly={true}
|
|
411
|
+
Repository={Repository}
|
|
412
|
+
columnsConfig={props.columnsConfig}
|
|
413
|
+
downloadHeaders={downloadHeaders}
|
|
414
|
+
downloadParams={downloadParams}
|
|
415
|
+
/>,
|
|
416
|
+
onCancel: hideModal,
|
|
417
|
+
});
|
|
379
418
|
};
|
|
380
419
|
// onPrint = () => {
|
|
381
420
|
// debugger;
|
|
@@ -11,6 +11,7 @@ export default function withTooltip(WrappedComponent) {
|
|
|
11
11
|
tooltip,
|
|
12
12
|
tooltipPlacement = 'bottom',
|
|
13
13
|
tooltipClassName,
|
|
14
|
+
tooltipTriggerClassName,
|
|
14
15
|
_tooltip = {},
|
|
15
16
|
...propsToPass
|
|
16
17
|
} = props;
|
|
@@ -22,6 +23,7 @@ export default function withTooltip(WrappedComponent) {
|
|
|
22
23
|
label={tooltip}
|
|
23
24
|
placement={tooltipPlacement}
|
|
24
25
|
className={tooltipClassName}
|
|
26
|
+
triggerClassName={tooltipTriggerClassName}
|
|
25
27
|
{..._tooltip}
|
|
26
28
|
>
|
|
27
29
|
{component}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from '../../Constants/ReportTypes.js';
|
|
19
19
|
import Form from '../Form/Form.js';
|
|
20
20
|
import withComponent from '../Hoc/withComponent.js';
|
|
21
|
+
import withAlert from '../Hoc/withAlert.js';
|
|
21
22
|
import testProps from '../../Functions/testProps.js';
|
|
22
23
|
import ChartLine from '../Icons/ChartLine.js';
|
|
23
24
|
import Pdf from '../Icons/Pdf.js';
|
|
@@ -36,8 +37,13 @@ function Report(props) {
|
|
|
36
37
|
disablePdf = false,
|
|
37
38
|
disableExcel = false,
|
|
38
39
|
showReportHeaders = true,
|
|
40
|
+
alert,
|
|
39
41
|
} = props,
|
|
40
|
-
buttons = []
|
|
42
|
+
buttons = [],
|
|
43
|
+
downloadReport = (args) => {
|
|
44
|
+
getReport(args);
|
|
45
|
+
alert('Download started');
|
|
46
|
+
};
|
|
41
47
|
|
|
42
48
|
const propsIcon = props._icon || {};
|
|
43
49
|
propsIcon.className = 'w-full h-full text-primary-500';
|
|
@@ -59,7 +65,7 @@ function Report(props) {
|
|
|
59
65
|
key: 'excelBtn',
|
|
60
66
|
text: 'Download Excel',
|
|
61
67
|
icon: Excel,
|
|
62
|
-
onPress: (data) =>
|
|
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>;
|
|
@@ -25,6 +25,7 @@ function UploadsDownloadsWindow(props) {
|
|
|
25
25
|
downloadHeaders,
|
|
26
26
|
uploadParams = {},
|
|
27
27
|
downloadParams = {},
|
|
28
|
+
isDownloadOnly = false,
|
|
28
29
|
onUpload,
|
|
29
30
|
|
|
30
31
|
// withComponent
|
|
@@ -124,6 +125,61 @@ function UploadsDownloadsWindow(props) {
|
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
};
|
|
128
|
+
|
|
129
|
+
const items = [
|
|
130
|
+
{
|
|
131
|
+
type: 'DisplayField',
|
|
132
|
+
text: 'Download an Excel file of the current grid contents.',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
type: 'Button',
|
|
136
|
+
text: 'Download',
|
|
137
|
+
isEditable: false,
|
|
138
|
+
icon: Excel,
|
|
139
|
+
_icon: {
|
|
140
|
+
size: 'md',
|
|
141
|
+
},
|
|
142
|
+
onPress: () => onDownload(),
|
|
143
|
+
className: 'mb-5',
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
if (!isDownloadOnly) {
|
|
147
|
+
items.push({
|
|
148
|
+
type: 'DisplayField',
|
|
149
|
+
text: 'Upload an Excel file to the current grid.',
|
|
150
|
+
});
|
|
151
|
+
items.push({
|
|
152
|
+
type: 'File',
|
|
153
|
+
name: 'file',
|
|
154
|
+
onChangeValue: setImportFile,
|
|
155
|
+
accept: '.xlsx',
|
|
156
|
+
});
|
|
157
|
+
items.push({
|
|
158
|
+
type: 'Row',
|
|
159
|
+
className: 'mt-2',
|
|
160
|
+
items: [
|
|
161
|
+
{
|
|
162
|
+
type: 'Button',
|
|
163
|
+
text: 'Upload',
|
|
164
|
+
isEditable: false,
|
|
165
|
+
icon: Upload,
|
|
166
|
+
_icon: {
|
|
167
|
+
size: 'md',
|
|
168
|
+
},
|
|
169
|
+
isDisabled: !importFile,
|
|
170
|
+
onPress: onUploadLocal,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
type: 'Button',
|
|
174
|
+
text: 'Get Template',
|
|
175
|
+
icon: Download,
|
|
176
|
+
isEditable: false,
|
|
177
|
+
onPress: onDownloadTemplate,
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
}
|
|
127
183
|
|
|
128
184
|
return <Panel
|
|
129
185
|
{...props}
|
|
@@ -151,58 +207,7 @@ function UploadsDownloadsWindow(props) {
|
|
|
151
207
|
"type": "Column",
|
|
152
208
|
"flex": 1,
|
|
153
209
|
"defaults": {},
|
|
154
|
-
"items":
|
|
155
|
-
{
|
|
156
|
-
type: 'DisplayField',
|
|
157
|
-
text: 'Download an Excel file of the current grid contents.',
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
type: 'Button',
|
|
161
|
-
text: 'Download',
|
|
162
|
-
isEditable: false,
|
|
163
|
-
icon: Excel,
|
|
164
|
-
_icon: {
|
|
165
|
-
size: 'md',
|
|
166
|
-
},
|
|
167
|
-
onPress: () => onDownload(),
|
|
168
|
-
className: 'mb-5',
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
type: 'DisplayField',
|
|
172
|
-
text: 'Upload an Excel file to the current grid.',
|
|
173
|
-
},
|
|
174
|
-
{
|
|
175
|
-
type: 'File',
|
|
176
|
-
name: 'file',
|
|
177
|
-
onChangeValue: setImportFile,
|
|
178
|
-
accept: '.xlsx',
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
type: 'Row',
|
|
182
|
-
className: 'mt-2',
|
|
183
|
-
items: [
|
|
184
|
-
{
|
|
185
|
-
type: 'Button',
|
|
186
|
-
text: 'Upload',
|
|
187
|
-
isEditable: false,
|
|
188
|
-
icon: Upload,
|
|
189
|
-
_icon: {
|
|
190
|
-
size: 'md',
|
|
191
|
-
},
|
|
192
|
-
isDisabled: !importFile,
|
|
193
|
-
onPress: onUploadLocal,
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
type: 'Button',
|
|
197
|
-
text: 'Get Template',
|
|
198
|
-
icon: Download,
|
|
199
|
-
isEditable: false,
|
|
200
|
-
onPress: onDownloadTemplate,
|
|
201
|
-
},
|
|
202
|
-
|
|
203
|
-
],
|
|
204
|
-
},
|
|
205
|
-
]
|
|
210
|
+
"items": items,
|
|
206
211
|
},
|
|
207
212
|
]}
|
|
208
213
|
// record={selection}
|
|
@@ -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
|
-
};
|