@terreno/ui 0.13.3 → 0.14.0
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/dist/ActionSheet.d.ts +4 -4
- package/dist/ActionSheet.js.map +1 -1
- package/dist/Avatar.js +1 -1
- package/dist/Avatar.js.map +1 -1
- package/dist/Banner.js.map +1 -1
- package/dist/Box.js +2 -0
- package/dist/Box.js.map +1 -1
- package/dist/Button.d.ts +2 -2
- package/dist/Button.js +35 -23
- package/dist/Button.js.map +1 -1
- package/dist/Common.d.ts +8 -2
- package/dist/Common.js.map +1 -1
- package/dist/ConsentFormScreen.js +1 -1
- package/dist/ConsentFormScreen.js.map +1 -1
- package/dist/ConsentNavigator.d.ts +1 -1
- package/dist/ConsentNavigator.js +2 -1
- package/dist/ConsentNavigator.js.map +1 -1
- package/dist/CustomSelectField.js +3 -1
- package/dist/CustomSelectField.js.map +1 -1
- package/dist/DataTable.js +1 -1
- package/dist/DataTable.js.map +1 -1
- package/dist/DateTimeActionSheet.js +2 -1
- package/dist/DateTimeActionSheet.js.map +1 -1
- package/dist/DateTimeField.js +3 -2
- package/dist/DateTimeField.js.map +1 -1
- package/dist/DateUtilities.js.map +1 -1
- package/dist/HeightField.js.map +1 -1
- package/dist/Hyperlink.js +19 -9
- package/dist/Hyperlink.js.map +1 -1
- package/dist/IconButton.js.map +1 -1
- package/dist/ImageBackground.d.ts +2 -5
- package/dist/ImageBackground.js +1 -1
- package/dist/ImageBackground.js.map +1 -1
- package/dist/ModalSheet.d.ts +3 -2
- package/dist/ModalSheet.js +1 -1
- package/dist/ModalSheet.js.map +1 -1
- package/dist/OfflineBanner.d.ts +21 -0
- package/dist/OfflineBanner.js +25 -0
- package/dist/OfflineBanner.js.map +1 -0
- package/dist/OpenAPIContext.js +1 -1
- package/dist/OpenAPIContext.js.map +1 -1
- package/dist/Page.js +1 -0
- package/dist/Page.js.map +1 -1
- package/dist/Pagination.js.map +1 -1
- package/dist/Permissions.js +3 -0
- package/dist/Permissions.js.map +1 -1
- package/dist/PickerSelect.js +7 -4
- package/dist/PickerSelect.js.map +1 -1
- package/dist/SelectField.js +1 -1
- package/dist/SelectField.js.map +1 -1
- package/dist/SplitPage.js +7 -2
- package/dist/SplitPage.js.map +1 -1
- package/dist/SplitPage.native.js +4 -1
- package/dist/SplitPage.native.js.map +1 -1
- package/dist/TapToEdit.js +10 -11
- package/dist/TapToEdit.js.map +1 -1
- package/dist/Toast.js.map +1 -1
- package/dist/ToastNotifications.js.map +1 -1
- package/dist/Unifier.d.ts +2 -2
- package/dist/Unifier.js +1 -1
- package/dist/Unifier.js.map +1 -1
- package/dist/Utilities.d.ts +8 -4
- package/dist/Utilities.js +1 -1
- package/dist/Utilities.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/ActionSheet.test.tsx +1 -0
- package/src/ActionSheet.tsx +6 -4
- package/src/Avatar.tsx +9 -2
- package/src/Badge.test.tsx +1 -0
- package/src/Banner.tsx +1 -1
- package/src/Box.test.tsx +1 -0
- package/src/Box.tsx +10 -6
- package/src/Button.test.tsx +35 -0
- package/src/Button.tsx +65 -34
- package/src/Common.ts +32 -15
- package/src/ConsentFormScreen.test.tsx +102 -0
- package/src/ConsentFormScreen.tsx +9 -3
- package/src/ConsentNavigator.test.tsx +1 -0
- package/src/ConsentNavigator.tsx +5 -3
- package/src/CustomSelectField.tsx +3 -1
- package/src/DataTable.test.tsx +1 -0
- package/src/DataTable.tsx +1 -1
- package/src/DateTimeActionSheet.tsx +7 -3
- package/src/DateTimeField.test.tsx +1 -0
- package/src/DateTimeField.tsx +3 -2
- package/src/DateUtilities.test.ts +111 -0
- package/src/DateUtilities.tsx +6 -6
- package/src/DecimalRangeActionSheet.test.tsx +28 -0
- package/src/ErrorBoundary.test.tsx +1 -0
- package/src/HeightField.tsx +2 -1
- package/src/Hyperlink.tsx +83 -52
- package/src/IconButton.tsx +1 -1
- package/src/ImageBackground.tsx +5 -6
- package/src/ModalSheet.test.tsx +1 -5
- package/src/ModalSheet.tsx +15 -6
- package/src/NumberField.test.tsx +14 -0
- package/src/OfflineBanner.test.tsx +70 -0
- package/src/OfflineBanner.tsx +54 -0
- package/src/OpenAPIContext.tsx +3 -2
- package/src/Page.tsx +1 -0
- package/src/Pagination.tsx +1 -1
- package/src/Permissions.ts +3 -0
- package/src/PickerSelect.tsx +17 -14
- package/src/SelectBadge.test.tsx +1 -0
- package/src/SelectField.tsx +1 -1
- package/src/Signature.test.tsx +1 -0
- package/src/SplitPage.native.tsx +2 -0
- package/src/SplitPage.tsx +6 -1
- package/src/TapToEdit.test.tsx +17 -0
- package/src/TapToEdit.tsx +11 -11
- package/src/Toast.tsx +1 -1
- package/src/ToastNotifications.tsx +1 -4
- package/src/Tooltip.test.tsx +0 -7
- package/src/Unifier.ts +6 -5
- package/src/Utilities.tsx +9 -6
- package/src/__snapshots__/AddressField.test.tsx.snap +3 -1
- package/src/__snapshots__/Button.test.tsx.snap +92 -50
- package/src/__snapshots__/CustomSelectField.test.tsx.snap +21 -7
- package/src/__snapshots__/DecimalRangeActionSheet.test.tsx.snap +14 -8
- package/src/__snapshots__/ErrorPage.test.tsx.snap +7 -4
- package/src/__snapshots__/Field.test.tsx.snap +18 -6
- package/src/__snapshots__/HeightActionSheet.test.tsx.snap +14 -8
- package/src/__snapshots__/HeightField.test.tsx.snap +35 -20
- package/src/__snapshots__/InfoModalIcon.test.tsx.snap +28 -16
- package/src/__snapshots__/Modal.test.tsx.snap +19 -10
- package/src/__snapshots__/ModalSheet.test.tsx.snap +0 -1
- package/src/__snapshots__/NumberPickerActionSheet.test.tsx.snap +14 -8
- package/src/__snapshots__/Page.test.tsx.snap +7 -4
- package/src/__snapshots__/SelectField.test.tsx.snap +18 -6
- package/src/__snapshots__/TerrenoProvider.test.tsx.snap +0 -2
- package/src/__snapshots__/TimezonePicker.test.tsx.snap +18 -6
- package/src/bunSetup.ts +25 -2
- package/src/index.tsx +1 -0
- package/src/login/__snapshots__/LoginScreen.test.tsx.snap +15 -6
- package/src/signUp/__snapshots__/SignUpScreen.test.tsx.snap +10 -4
- package/src/table/__snapshots__/TableBadge.test.tsx.snap +3 -1
- package/src/types/react-native-swiper-flatlist.d.ts +1 -0
package/src/HeightField.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {type FC, useCallback, useEffect, useMemo, useRef, useState} from "react";
|
|
2
2
|
import {Platform, Pressable, type StyleProp, TextInput, View} from "react-native";
|
|
3
3
|
|
|
4
|
+
import type {ActionSheet} from "./ActionSheet";
|
|
4
5
|
import {Box} from "./Box";
|
|
5
6
|
import type {HeightFieldProps, TextStyleWithOutline} from "./Common";
|
|
6
7
|
import {FieldError, FieldHelperText, FieldTitle} from "./fieldElements";
|
|
@@ -154,7 +155,7 @@ export const HeightField: FC<HeightFieldProps> = ({
|
|
|
154
155
|
max,
|
|
155
156
|
}) => {
|
|
156
157
|
const {theme} = useTheme();
|
|
157
|
-
const actionSheetRef
|
|
158
|
+
const actionSheetRef = useRef<ActionSheet | null>(null);
|
|
158
159
|
const isMobileOrNative = isMobileDevice() || isNative();
|
|
159
160
|
|
|
160
161
|
const minInches = min ?? DEFAULT_MIN_INCHES;
|
package/src/Hyperlink.tsx
CHANGED
|
@@ -36,73 +36,94 @@
|
|
|
36
36
|
|
|
37
37
|
import * as mdurl from "mdurl";
|
|
38
38
|
import React from "react";
|
|
39
|
-
import {Linking, Platform, Text, View} from "react-native";
|
|
39
|
+
import {Linking, Platform, type StyleProp, Text, type TextStyle, View} from "react-native";
|
|
40
40
|
|
|
41
41
|
import type {HyperlinkProps} from "./Common";
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
interface LinkifyMatch {
|
|
44
|
+
index: number;
|
|
45
|
+
lastIndex: number;
|
|
46
|
+
raw: string;
|
|
47
|
+
schema: string;
|
|
48
|
+
text: string;
|
|
49
|
+
url: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface LinkifyItLike {
|
|
53
|
+
pretest: (text: string) => boolean;
|
|
54
|
+
test: (text: string) => boolean;
|
|
55
|
+
match: (text: string) => LinkifyMatch[] | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const linkifyLib: LinkifyItLike = require("linkify-it")();
|
|
44
59
|
|
|
45
60
|
const {OS} = Platform;
|
|
46
61
|
|
|
47
62
|
// Leaving this as a class component because it was easier to handle the `parse(this)` in
|
|
48
63
|
// `render()`
|
|
49
64
|
class HyperlinkComponent extends React.Component<HyperlinkProps> {
|
|
50
|
-
isTextNested = (component:
|
|
65
|
+
isTextNested = (component: React.ReactElement) => {
|
|
51
66
|
if (!React.isValidElement(component)) throw new Error("Invalid component");
|
|
52
|
-
const
|
|
67
|
+
const componentType = (component.type as {displayName?: string} | undefined) ?? {};
|
|
68
|
+
const {displayName} = componentType;
|
|
53
69
|
if (displayName !== "Text") throw new Error("Not a Text component");
|
|
54
|
-
return typeof (component.props as
|
|
70
|
+
return typeof (component.props as {children?: unknown}).children !== "string";
|
|
55
71
|
};
|
|
56
72
|
|
|
57
|
-
linkify = (component:
|
|
73
|
+
linkify = (component: React.ReactElement<{children: string; style?: unknown}>) => {
|
|
58
74
|
const linkifyIt = this.props.linkify || linkifyLib;
|
|
59
75
|
|
|
60
76
|
if (!linkifyIt.pretest(component.props.children) || !linkifyIt.test(component.props.children))
|
|
61
77
|
return component;
|
|
62
78
|
|
|
63
|
-
const elements = [];
|
|
79
|
+
const elements: React.ReactNode[] = [];
|
|
64
80
|
let _lastIndex = 0;
|
|
65
81
|
|
|
66
|
-
const {key: _key, ref: _ref, ...componentProps} = component.props
|
|
82
|
+
const {key: _key, ref: _ref, ...componentProps} = component.props as Record<string, unknown>;
|
|
67
83
|
|
|
68
84
|
try {
|
|
69
|
-
linkifyIt
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
linkifyIt
|
|
86
|
+
.match(component.props.children)
|
|
87
|
+
?.forEach(({index, lastIndex, text, url}: LinkifyMatch) => {
|
|
88
|
+
const nonLinkedText = component.props.children.substring(_lastIndex, index);
|
|
89
|
+
nonLinkedText && elements.push(nonLinkedText);
|
|
90
|
+
_lastIndex = lastIndex;
|
|
91
|
+
if (this.props.linkText)
|
|
92
|
+
text =
|
|
93
|
+
typeof this.props.linkText === "function"
|
|
94
|
+
? this.props.linkText(url)
|
|
95
|
+
: this.props.linkText;
|
|
96
|
+
|
|
97
|
+
const clickHandlerProps: {onPress?: () => void; onLongPress?: () => void} = {};
|
|
98
|
+
if (OS !== "web") {
|
|
99
|
+
if (this.props.onLongPress) {
|
|
100
|
+
clickHandlerProps.onLongPress = () => this.props.onLongPress?.(url, text);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (this.props.onPress) {
|
|
104
|
+
// The HyperlinkProps onPress signature is (url) => void per Common.ts, but this forked
|
|
105
|
+
// component invokes it with both url and text. Cast to avoid arity mismatch.
|
|
106
|
+
const onPressFn = this.props.onPress as (url: string, text: string) => void;
|
|
107
|
+
clickHandlerProps.onPress = () => onPressFn(url, text);
|
|
83
108
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
{text}
|
|
103
|
-
</Text>
|
|
104
|
-
);
|
|
105
|
-
});
|
|
109
|
+
|
|
110
|
+
let injected: Record<string, unknown> = {};
|
|
111
|
+
if (this.props.injectViewProps) {
|
|
112
|
+
injected = this.props.injectViewProps(url);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
elements.push(
|
|
116
|
+
<Text
|
|
117
|
+
{...componentProps}
|
|
118
|
+
{...clickHandlerProps}
|
|
119
|
+
key={url + index}
|
|
120
|
+
style={[component.props.style, this.props.linkStyle]}
|
|
121
|
+
{...injected}
|
|
122
|
+
>
|
|
123
|
+
{text}
|
|
124
|
+
</Text>
|
|
125
|
+
);
|
|
126
|
+
});
|
|
106
127
|
elements.push(
|
|
107
128
|
component.props.children.substring(_lastIndex, component.props.children.length)
|
|
108
129
|
);
|
|
@@ -112,11 +133,13 @@ class HyperlinkComponent extends React.Component<HyperlinkProps> {
|
|
|
112
133
|
}
|
|
113
134
|
};
|
|
114
135
|
|
|
115
|
-
parse = (component:
|
|
116
|
-
const
|
|
136
|
+
parse = (component: React.ReactElement): React.ReactElement => {
|
|
137
|
+
const props =
|
|
138
|
+
(component?.props as {children?: React.ReactNode; style?: StyleProp<TextStyle>}) ?? {};
|
|
139
|
+
const {children} = props;
|
|
117
140
|
if (!children) return component;
|
|
118
141
|
|
|
119
|
-
const {key: _key, ref: _ref, ...componentProps} = component.props
|
|
142
|
+
const {key: _key, ref: _ref, ...componentProps} = component.props as Record<string, unknown>;
|
|
120
143
|
|
|
121
144
|
const linkifyIt = this.props.linkify || linkifyLib;
|
|
122
145
|
|
|
@@ -124,15 +147,19 @@ class HyperlinkComponent extends React.Component<HyperlinkProps> {
|
|
|
124
147
|
component,
|
|
125
148
|
componentProps,
|
|
126
149
|
React.Children.map(children, (child) => {
|
|
127
|
-
const
|
|
150
|
+
const childType = (child as React.ReactElement | null)?.type as
|
|
151
|
+
| {displayName?: string}
|
|
152
|
+
| undefined;
|
|
153
|
+
const displayName = childType?.displayName;
|
|
128
154
|
if (typeof child === "string" && linkifyIt.pretest(child))
|
|
129
155
|
return this.linkify(
|
|
130
|
-
<Text {...componentProps} style={
|
|
156
|
+
<Text {...componentProps} style={props.style}>
|
|
131
157
|
{child}
|
|
132
158
|
</Text>
|
|
133
159
|
);
|
|
134
|
-
if (displayName === "Text" && !this.isTextNested(child
|
|
135
|
-
|
|
160
|
+
if (displayName === "Text" && !this.isTextNested(child as React.ReactElement))
|
|
161
|
+
return this.linkify(child as React.ReactElement<{children: string; style?: unknown}>);
|
|
162
|
+
return this.parse(child as React.ReactElement);
|
|
136
163
|
})
|
|
137
164
|
);
|
|
138
165
|
};
|
|
@@ -148,7 +175,11 @@ class HyperlinkComponent extends React.Component<HyperlinkProps> {
|
|
|
148
175
|
<View {...viewProps} style={this.props.style}>
|
|
149
176
|
{!this.props.onPress && !this.props.onLongPress && !this.props.linkStyle
|
|
150
177
|
? this.props.children
|
|
151
|
-
: (
|
|
178
|
+
: (
|
|
179
|
+
this.parse(this as unknown as React.ReactElement).props as {
|
|
180
|
+
children?: React.ReactNode;
|
|
181
|
+
}
|
|
182
|
+
).children}
|
|
152
183
|
</View>
|
|
153
184
|
);
|
|
154
185
|
}
|
package/src/IconButton.tsx
CHANGED
package/src/ImageBackground.tsx
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ImageBackground as ImageBackgroundNative,
|
|
4
|
+
type ImageBackgroundProps as NativeImageBackgroundProps,
|
|
5
|
+
} from "react-native";
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
children?: any;
|
|
6
|
-
style?: any;
|
|
7
|
-
source: any;
|
|
8
|
-
}
|
|
7
|
+
type ImageBackgroundProps = NativeImageBackgroundProps;
|
|
9
8
|
|
|
10
9
|
export class ImageBackground extends React.Component<ImageBackgroundProps, {}> {
|
|
11
10
|
render() {
|
package/src/ModalSheet.test.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
|
|
1
2
|
import {describe, expect, it, mock} from "bun:test";
|
|
2
3
|
import {forwardRef, useRef} from "react";
|
|
3
4
|
import {Text, View} from "react-native";
|
|
@@ -14,11 +15,6 @@ mock.module("react-native-modalize", () => ({
|
|
|
14
15
|
)),
|
|
15
16
|
}));
|
|
16
17
|
|
|
17
|
-
// Mock react-native-portalize
|
|
18
|
-
mock.module("react-native-portalize", () => ({
|
|
19
|
-
Portal: ({children}: {children: React.ReactNode}) => <View testID="portal">{children}</View>,
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
18
|
describe("ModalSheet", () => {
|
|
23
19
|
it("renders correctly with children", () => {
|
|
24
20
|
const {toJSON} = renderWithTheme(
|
package/src/ModalSheet.tsx
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
type MutableRefObject,
|
|
4
|
+
type ReactNode,
|
|
5
|
+
type Ref,
|
|
6
|
+
useEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
} from "react";
|
|
2
9
|
import {Animated} from "react-native";
|
|
3
10
|
import {Modalize} from "react-native-modalize";
|
|
4
11
|
import {Portal} from "react-native-portalize";
|
|
5
12
|
|
|
6
|
-
export const useCombinedRefs = (
|
|
7
|
-
|
|
13
|
+
export const useCombinedRefs = <T,>(
|
|
14
|
+
...refs: Array<Ref<T> | undefined>
|
|
15
|
+
): MutableRefObject<T | null> => {
|
|
16
|
+
const targetRef = useRef<T | null>(null);
|
|
8
17
|
|
|
9
18
|
// Iterate through the refs array, and set the ref.current value to the targetRef
|
|
10
19
|
useEffect(() => {
|
|
11
|
-
refs.forEach((ref
|
|
20
|
+
refs.forEach((ref) => {
|
|
12
21
|
if (!ref) {
|
|
13
22
|
return;
|
|
14
23
|
}
|
|
@@ -16,7 +25,7 @@ export const useCombinedRefs = (...refs: any) => {
|
|
|
16
25
|
if (typeof ref === "function") {
|
|
17
26
|
ref(targetRef.current);
|
|
18
27
|
} else {
|
|
19
|
-
ref.current = targetRef.current;
|
|
28
|
+
(ref as MutableRefObject<T | null>).current = targetRef.current;
|
|
20
29
|
}
|
|
21
30
|
});
|
|
22
31
|
}, [refs]);
|
|
@@ -25,7 +34,7 @@ export const useCombinedRefs = (...refs: any) => {
|
|
|
25
34
|
};
|
|
26
35
|
|
|
27
36
|
interface Props {
|
|
28
|
-
children:
|
|
37
|
+
children: ReactNode;
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
export const SimpleContent = forwardRef((props: Props, ref) => {
|
package/src/NumberField.test.tsx
CHANGED
|
@@ -110,6 +110,20 @@ describe("NumberField", () => {
|
|
|
110
110
|
expect(queryByText(/must be/)).toBeNull();
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
it("shows decimal error for non-numeric characters in decimal type", () => {
|
|
114
|
+
const {getByText} = renderWithTheme(
|
|
115
|
+
<NumberField label="Decimal" onChange={noOp} type="decimal" value="12abc" />
|
|
116
|
+
);
|
|
117
|
+
expect(getByText("Value must be a decimal")).toBeTruthy();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("shows decimal error for multiple decimal points", () => {
|
|
121
|
+
const {getByText} = renderWithTheme(
|
|
122
|
+
<NumberField label="Decimal" onChange={noOp} type="decimal" value="1.2.3" />
|
|
123
|
+
);
|
|
124
|
+
expect(getByText("Value must be a decimal")).toBeTruthy();
|
|
125
|
+
});
|
|
126
|
+
|
|
113
127
|
it("syncs value when prop changes", async () => {
|
|
114
128
|
const handleChange = mock((_value: string): void => {});
|
|
115
129
|
const {getByDisplayValue, unmount} = renderWithTheme(
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {describe, expect, it} from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {OfflineBanner} from "./OfflineBanner";
|
|
4
|
+
import {renderWithTheme} from "./test-utils";
|
|
5
|
+
|
|
6
|
+
describe("OfflineBanner", () => {
|
|
7
|
+
it("renders nothing when online and not syncing", () => {
|
|
8
|
+
const {toJSON} = renderWithTheme(
|
|
9
|
+
<OfflineBanner isOnline={true} isSyncing={false} queueLength={0} />
|
|
10
|
+
);
|
|
11
|
+
expect(toJSON()).toBeNull();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("shows offline banner when not online", () => {
|
|
15
|
+
const {getByTestId, getByText} = renderWithTheme(
|
|
16
|
+
<OfflineBanner isOnline={false} isSyncing={false} queueLength={0} />
|
|
17
|
+
);
|
|
18
|
+
expect(getByTestId("offline-banner")).toBeTruthy();
|
|
19
|
+
expect(getByText("You're offline.")).toBeTruthy();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("shows queue count in offline banner text", () => {
|
|
23
|
+
const {getByText} = renderWithTheme(
|
|
24
|
+
<OfflineBanner isOnline={false} isSyncing={false} queueLength={5} />
|
|
25
|
+
);
|
|
26
|
+
expect(
|
|
27
|
+
getByText("You're offline. 5 pending changes will sync when you reconnect.")
|
|
28
|
+
).toBeTruthy();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("shows singular 'change' for 1 pending", () => {
|
|
32
|
+
const {getByText} = renderWithTheme(
|
|
33
|
+
<OfflineBanner isOnline={false} isSyncing={false} queueLength={1} />
|
|
34
|
+
);
|
|
35
|
+
expect(
|
|
36
|
+
getByText("You're offline. 1 pending change will sync when you reconnect.")
|
|
37
|
+
).toBeTruthy();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("shows plural 'changes' for multiple pending", () => {
|
|
41
|
+
const {getByText} = renderWithTheme(
|
|
42
|
+
<OfflineBanner isOnline={false} isSyncing={false} queueLength={3} />
|
|
43
|
+
);
|
|
44
|
+
expect(
|
|
45
|
+
getByText("You're offline. 3 pending changes will sync when you reconnect.")
|
|
46
|
+
).toBeTruthy();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("shows syncing banner when isSyncing is true", () => {
|
|
50
|
+
const {getByTestId, getByText} = renderWithTheme(
|
|
51
|
+
<OfflineBanner isOnline={false} isSyncing={true} queueLength={2} />
|
|
52
|
+
);
|
|
53
|
+
expect(getByTestId("offline-banner")).toBeTruthy();
|
|
54
|
+
expect(getByText("Syncing offline changes...")).toBeTruthy();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("uses custom testID prop", () => {
|
|
58
|
+
const {getByTestId} = renderWithTheme(
|
|
59
|
+
<OfflineBanner isOnline={false} isSyncing={false} queueLength={0} testID="custom-banner" />
|
|
60
|
+
);
|
|
61
|
+
expect(getByTestId("custom-banner")).toBeTruthy();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("uses custom testID prop while syncing", () => {
|
|
65
|
+
const {getByTestId} = renderWithTheme(
|
|
66
|
+
<OfflineBanner isOnline={false} isSyncing={true} queueLength={2} testID="custom-banner" />
|
|
67
|
+
);
|
|
68
|
+
expect(getByTestId("custom-banner")).toBeTruthy();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
|
|
3
|
+
import {Banner} from "./Banner";
|
|
4
|
+
import {Box} from "./Box";
|
|
5
|
+
|
|
6
|
+
export interface OfflineBannerProps {
|
|
7
|
+
/** Whether the server is currently reachable */
|
|
8
|
+
isOnline: boolean;
|
|
9
|
+
/** Number of mutations waiting to be synced */
|
|
10
|
+
queueLength: number;
|
|
11
|
+
/** Whether mutations are currently being replayed */
|
|
12
|
+
isSyncing: boolean;
|
|
13
|
+
/** testID for the root element */
|
|
14
|
+
testID?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Displays offline/syncing status banners. Renders nothing when online and idle.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const {isOnline, queueLength, isSyncing} = useServerStatus();
|
|
23
|
+
* <OfflineBanner isOnline={isOnline} queueLength={queueLength} isSyncing={isSyncing} />
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export const OfflineBanner: React.FC<OfflineBannerProps> = ({
|
|
27
|
+
isOnline,
|
|
28
|
+
queueLength,
|
|
29
|
+
isSyncing,
|
|
30
|
+
testID = "offline-banner",
|
|
31
|
+
}) => {
|
|
32
|
+
if (isSyncing) {
|
|
33
|
+
return (
|
|
34
|
+
<Box marginBottom={4} testID={testID}>
|
|
35
|
+
<Banner id="syncing-status" status="info" text="Syncing offline changes..." />
|
|
36
|
+
</Box>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!isOnline) {
|
|
41
|
+
const suffix =
|
|
42
|
+
queueLength > 0
|
|
43
|
+
? ` ${queueLength} pending change${queueLength !== 1 ? "s" : ""} will sync when you reconnect.`
|
|
44
|
+
: "";
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Box marginBottom={4} testID={testID}>
|
|
48
|
+
<Banner id="offline-status" status="warning" text={`You're offline.${suffix}`} />
|
|
49
|
+
</Box>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
};
|
package/src/OpenAPIContext.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
OpenAPIContextType,
|
|
9
9
|
OpenAPIProviderProps,
|
|
10
10
|
OpenAPISpec,
|
|
11
|
+
OpenApiProperty,
|
|
11
12
|
} from "./Common";
|
|
12
13
|
|
|
13
14
|
const OpenAPIContext = createContext<OpenAPIContextType | null>(null);
|
|
@@ -39,7 +40,7 @@ export const OpenAPIProvider = ({children, specUrl}: OpenAPIProviderProps): Reac
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
for (const dotField of dotFields.slice(1)) {
|
|
42
|
-
field = (field?.properties as
|
|
43
|
+
field = (field?.properties as Record<string, OpenApiProperty> | undefined)?.[dotField];
|
|
43
44
|
}
|
|
44
45
|
return field;
|
|
45
46
|
};
|
|
@@ -55,7 +56,7 @@ export const OpenAPIProvider = ({children, specUrl}: OpenAPIProviderProps): Reac
|
|
|
55
56
|
const data = (await response.json()) as OpenAPISpec;
|
|
56
57
|
setSpec(data);
|
|
57
58
|
})
|
|
58
|
-
.catch((error:
|
|
59
|
+
.catch((error: unknown) => console.error(`Error fetching OpenAPI spec: ${String(error)}`));
|
|
59
60
|
}, [specUrl]);
|
|
60
61
|
|
|
61
62
|
return (
|
package/src/Page.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import {Spinner} from "./Spinner";
|
|
|
11
11
|
import {Text} from "./Text";
|
|
12
12
|
|
|
13
13
|
export class Page extends React.Component<PageProps, {}> {
|
|
14
|
+
// biome-ignore lint/suspicious/noExplicitAny: ActionSheet class is defined in ActionSheet.tsx which imports from Common.ts indirectly; using its type here would create a circular dependency
|
|
14
15
|
actionSheetRef: React.RefObject<any> = React.createRef();
|
|
15
16
|
|
|
16
17
|
renderHeader() {
|
package/src/Pagination.tsx
CHANGED
package/src/Permissions.ts
CHANGED
|
@@ -10,6 +10,7 @@ export async function requestPermissions(_kind: PermissionKind): Promise<Permiss
|
|
|
10
10
|
// const userPropertyKey = `PermissionsFor${capitalize(kind)}`;
|
|
11
11
|
|
|
12
12
|
// let k = kind;
|
|
13
|
+
// // noExplicitAny: Dead commented-out code; types cannot be resolved without the full uncommented context and Permissions library types
|
|
13
14
|
// let options: any = undefined;
|
|
14
15
|
// if (kind === "locationAlways") {
|
|
15
16
|
// k = "location";
|
|
@@ -21,6 +22,7 @@ export async function requestPermissions(_kind: PermissionKind): Promise<Permiss
|
|
|
21
22
|
|
|
22
23
|
// // TODO check soft request status.
|
|
23
24
|
|
|
25
|
+
// // noExplicitAny: Dead commented-out code; MAP[k] type depends on unreferenced MAP constant
|
|
24
26
|
// const current = await Permissions.check(MAP[k] as any);
|
|
25
27
|
// // Tracking.log(`[permissions] ${k} permissions are ${current}`);
|
|
26
28
|
// if (current === "denied" || current === "limited") {
|
|
@@ -31,6 +33,7 @@ export async function requestPermissions(_kind: PermissionKind): Promise<Permiss
|
|
|
31
33
|
// return resolve("authorized");
|
|
32
34
|
// }
|
|
33
35
|
|
|
36
|
+
// // noExplicitAny: Dead commented-out code; MAP[k] type depends on unreferenced MAP constant
|
|
34
37
|
// const response = await Permissions.request(MAP[k] as any, options);
|
|
35
38
|
// if (response === "granted") {
|
|
36
39
|
// // Tracking.setUserProperty(userPropertyKey, "true");
|
package/src/PickerSelect.tsx
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
Keyboard,
|
|
31
31
|
Modal,
|
|
32
32
|
type ModalProps,
|
|
33
|
+
type NativeSyntheticEvent,
|
|
33
34
|
Platform,
|
|
34
35
|
Pressable,
|
|
35
36
|
type PressableProps,
|
|
@@ -137,9 +138,7 @@ export function RNPickerSelect({
|
|
|
137
138
|
InputAccessoryView,
|
|
138
139
|
}: RNPickerSelectProps) {
|
|
139
140
|
const [showPicker, setShowPicker] = useState<boolean>(false);
|
|
140
|
-
const [animationType, setAnimationType] = useState<"
|
|
141
|
-
undefined
|
|
142
|
-
);
|
|
141
|
+
const [animationType, setAnimationType] = useState<ModalProps["animationType"]>(undefined);
|
|
143
142
|
const [orientation, setOrientation] = useState<"portrait" | "landscape">("portrait");
|
|
144
143
|
const [doneDepressed, setDoneDepressed] = useState<boolean>(false);
|
|
145
144
|
const {theme} = useTheme();
|
|
@@ -185,7 +184,7 @@ export function RNPickerSelect({
|
|
|
185
184
|
}
|
|
186
185
|
return {
|
|
187
186
|
idx,
|
|
188
|
-
selectedItem: options[idx] || {}
|
|
187
|
+
selectedItem: (options[idx] || {}) as Partial<PickerSelectItem>,
|
|
189
188
|
};
|
|
190
189
|
},
|
|
191
190
|
[options]
|
|
@@ -217,9 +216,7 @@ export function RNPickerSelect({
|
|
|
217
216
|
|
|
218
217
|
const onOrientationChange = ({
|
|
219
218
|
nativeEvent,
|
|
220
|
-
}: {
|
|
221
|
-
nativeEvent: {orientation: "portrait" | "landscape"};
|
|
222
|
-
}) => {
|
|
219
|
+
}: NativeSyntheticEvent<{orientation: "portrait" | "landscape"}>) => {
|
|
223
220
|
setOrientation(nativeEvent.orientation);
|
|
224
221
|
};
|
|
225
222
|
|
|
@@ -256,12 +253,13 @@ export function RNPickerSelect({
|
|
|
256
253
|
|
|
257
254
|
const renderPickerItems = () => {
|
|
258
255
|
return options?.map((item) => {
|
|
256
|
+
if (!item) return null;
|
|
259
257
|
return (
|
|
260
258
|
<Picker.Item
|
|
261
|
-
color={item
|
|
262
|
-
key={item
|
|
263
|
-
label={item
|
|
264
|
-
value={item
|
|
259
|
+
color={item.color}
|
|
260
|
+
key={item.key || item.label}
|
|
261
|
+
label={item.label}
|
|
262
|
+
value={item.value}
|
|
265
263
|
/>
|
|
266
264
|
);
|
|
267
265
|
});
|
|
@@ -484,9 +482,14 @@ export function RNPickerSelect({
|
|
|
484
482
|
};
|
|
485
483
|
|
|
486
484
|
const renderAndroidHeadless = () => {
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
|
|
485
|
+
// `View` and `Pressable` accept disjoint prop sets; the fork swaps between them to work
|
|
486
|
+
// around an Android touchable bug, so we cast to a structural component type that accepts
|
|
487
|
+
// the union of props actually used in JSX below.
|
|
488
|
+
const Component = (fixAndroidTouchableBug ? View : Pressable) as ComponentType<{
|
|
489
|
+
onPress?: PressableProps["onPress"];
|
|
490
|
+
testID?: string;
|
|
491
|
+
children?: ReactNode;
|
|
492
|
+
}>;
|
|
490
493
|
return (
|
|
491
494
|
<Component onPress={onOpen} testID="android_touchable_wrapper" {...touchableWrapperProps}>
|
|
492
495
|
<View>
|
package/src/SelectBadge.test.tsx
CHANGED
package/src/SelectField.tsx
CHANGED
|
@@ -19,7 +19,7 @@ export const SelectField: FC<SelectFieldProps> = ({
|
|
|
19
19
|
const clearOption = {label: placeholder ?? "---", value: ""};
|
|
20
20
|
|
|
21
21
|
return (
|
|
22
|
-
<View>
|
|
22
|
+
<View style={{width: "100%"}}>
|
|
23
23
|
{Boolean(title) && <FieldTitle text={title!} />}
|
|
24
24
|
{Boolean(errorText) && <FieldError text={errorText!} />}
|
|
25
25
|
<RNPickerSelect
|
package/src/Signature.test.tsx
CHANGED
package/src/SplitPage.native.tsx
CHANGED
|
@@ -34,6 +34,7 @@ export const SplitPage = ({
|
|
|
34
34
|
const {width} = Dimensions.get("window");
|
|
35
35
|
|
|
36
36
|
const onItemSelect = useCallback(
|
|
37
|
+
// biome-ignore lint/suspicious/noExplicitAny: SplitPage accepts heterogeneous list item shapes from consumers; the generic propagates from listViewData
|
|
37
38
|
async (item: ListRenderItemInfo<any>) => {
|
|
38
39
|
setSelectedId(item.index);
|
|
39
40
|
await onSelectionChange(item);
|
|
@@ -58,6 +59,7 @@ export const SplitPage = ({
|
|
|
58
59
|
return null;
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
// biome-ignore lint/suspicious/noExplicitAny: SplitPage accepts heterogeneous list item shapes from consumers; the generic propagates from listViewData
|
|
61
63
|
const renderItem = (itemInfo: ListRenderItemInfo<any>) => {
|
|
62
64
|
return (
|
|
63
65
|
<Box
|