@jobber/components-native 0.38.0 → 0.40.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/src/AtlantisContext/AtlantisContext.js +2 -0
- package/dist/src/Form/Form.js +187 -0
- package/dist/src/Form/Form.style.js +33 -0
- package/dist/src/Form/components/FormActionBar/FormActionBar.js +21 -0
- package/dist/src/Form/components/FormActionBar/FormActionBar.style.js +5 -0
- package/dist/src/Form/components/FormActionBar/index.js +1 -0
- package/dist/src/Form/components/FormBody/FormBody.js +20 -0
- package/dist/src/Form/components/FormBody/FormBody.style.js +26 -0
- package/dist/src/Form/components/FormBody/index.js +1 -0
- package/dist/src/Form/components/FormCache/FormCache.js +34 -0
- package/dist/src/Form/components/FormErrorBanner/FormErrorBanner.js +21 -0
- package/dist/src/Form/components/FormErrorBanner/index.js +1 -0
- package/dist/src/Form/components/FormErrorBanner/messages.js +13 -0
- package/dist/src/Form/components/FormMask/FormMask.js +11 -0
- package/dist/src/Form/components/FormMask/FormMask.style.js +15 -0
- package/dist/src/Form/components/FormMask/index.js +1 -0
- package/dist/src/Form/components/FormMessage/FormMessage.js +48 -0
- package/dist/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.js +28 -0
- package/dist/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.style.js +17 -0
- package/dist/src/Form/components/FormMessage/components/InternalFormMessage/index.js +1 -0
- package/dist/src/Form/components/FormMessage/components/InternalFormMessage/messages.js +8 -0
- package/dist/src/Form/components/FormMessage/index.js +1 -0
- package/dist/src/Form/components/FormMessageBanner/FormMessageBanner.js +15 -0
- package/dist/src/Form/components/FormMessageBanner/index.js +1 -0
- package/dist/src/Form/components/FormSaveButton/FormSaveButton.js +69 -0
- package/dist/src/Form/components/FormSaveButton/index.js +1 -0
- package/dist/src/Form/components/FormSaveButton/messages.js +8 -0
- package/dist/src/Form/constants.js +2 -0
- package/dist/src/Form/context/AtlantisFormContext.js +16 -0
- package/dist/src/Form/context/index.js +1 -0
- package/dist/src/Form/context/types.js +1 -0
- package/dist/src/Form/hooks/useFormViewRefs.js +14 -0
- package/dist/src/Form/hooks/useInternalForm.js +37 -0
- package/dist/src/Form/hooks/useOfflineHandler.js +24 -0
- package/dist/src/Form/hooks/useSaveButtonPosition.js +25 -0
- package/dist/src/Form/hooks/useScreenInformation.js +15 -0
- package/dist/src/Form/hooks/useScrollToError/index.js +1 -0
- package/dist/src/Form/hooks/useScrollToError/useScrollToError.js +63 -0
- package/dist/src/Form/index.js +4 -0
- package/dist/src/Form/messages.js +28 -0
- package/dist/src/Form/types.js +10 -0
- package/dist/src/InputDate/InputDate.js +76 -0
- package/dist/src/InputDate/index.js +1 -0
- package/dist/src/InputDate/messages.js +8 -0
- package/dist/src/Menu/Menu.js +67 -0
- package/dist/src/Menu/Menu.style.js +6 -0
- package/dist/src/Menu/components/MenuOption/MenuOption.js +25 -0
- package/dist/src/Menu/components/MenuOption/MenuOption.style.js +10 -0
- package/dist/src/Menu/components/MenuOption/index.js +1 -0
- package/dist/src/Menu/components/Overlay/Overlay.js +9 -0
- package/dist/src/Menu/components/Overlay/Overlay.style.js +6 -0
- package/dist/src/Menu/components/Overlay/index.js +1 -0
- package/dist/src/Menu/index.js +1 -0
- package/dist/src/Menu/messages.js +8 -0
- package/dist/src/Menu/types.js +1 -0
- package/dist/src/Menu/utils.js +84 -0
- package/dist/src/index.js +3 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/src/AtlantisContext/AtlantisContext.d.ts +7 -1
- package/dist/types/src/Form/Form.d.ts +4 -0
- package/dist/types/src/Form/Form.style.d.ts +31 -0
- package/dist/types/src/Form/components/FormActionBar/FormActionBar.d.ts +13 -0
- package/dist/types/src/Form/components/FormActionBar/FormActionBar.style.d.ts +15 -0
- package/dist/types/src/Form/components/FormActionBar/index.d.ts +2 -0
- package/dist/types/src/Form/components/FormBody/FormBody.d.ts +10 -0
- package/dist/types/src/Form/components/FormBody/FormBody.style.d.ts +24 -0
- package/dist/types/src/Form/components/FormBody/index.d.ts +1 -0
- package/dist/types/src/Form/components/FormCache/FormCache.d.ts +10 -0
- package/dist/types/src/Form/components/FormErrorBanner/FormErrorBanner.d.ts +3 -0
- package/dist/types/src/Form/components/FormErrorBanner/index.d.ts +1 -0
- package/dist/types/src/Form/components/FormErrorBanner/messages.d.ts +12 -0
- package/dist/types/src/Form/components/FormMask/FormMask.d.ts +2 -0
- package/dist/types/src/Form/components/FormMask/FormMask.style.d.ts +13 -0
- package/dist/types/src/Form/components/FormMask/index.d.ts +1 -0
- package/dist/types/src/Form/components/FormMessage/FormMessage.d.ts +19 -0
- package/dist/types/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.d.ts +8 -0
- package/dist/types/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.style.d.ts +20 -0
- package/dist/types/src/Form/components/FormMessage/components/InternalFormMessage/index.d.ts +1 -0
- package/dist/types/src/Form/components/FormMessage/components/InternalFormMessage/messages.d.ts +7 -0
- package/dist/types/src/Form/components/FormMessage/index.d.ts +1 -0
- package/dist/types/src/Form/components/FormMessageBanner/FormMessageBanner.d.ts +7 -0
- package/dist/types/src/Form/components/FormMessageBanner/index.d.ts +1 -0
- package/dist/types/src/Form/components/FormSaveButton/FormSaveButton.d.ts +3 -0
- package/dist/types/src/Form/components/FormSaveButton/index.d.ts +1 -0
- package/dist/types/src/Form/components/FormSaveButton/messages.d.ts +7 -0
- package/dist/types/src/Form/constants.d.ts +2 -0
- package/dist/types/src/Form/context/AtlantisFormContext.d.ts +12 -0
- package/dist/types/src/Form/context/index.d.ts +2 -0
- package/dist/types/src/Form/context/types.d.ts +26 -0
- package/dist/types/src/Form/hooks/useFormViewRefs.d.ts +10 -0
- package/dist/types/src/Form/hooks/useInternalForm.d.ts +19 -0
- package/dist/types/src/Form/hooks/useOfflineHandler.d.ts +1 -0
- package/dist/types/src/Form/hooks/useSaveButtonPosition.d.ts +12 -0
- package/dist/types/src/Form/hooks/useScreenInformation.d.ts +8 -0
- package/dist/types/src/Form/hooks/useScrollToError/index.d.ts +1 -0
- package/dist/types/src/Form/hooks/useScrollToError/useScrollToError.d.ts +10 -0
- package/dist/types/src/Form/index.d.ts +5 -0
- package/dist/types/src/Form/messages.d.ts +27 -0
- package/dist/types/src/Form/types.d.ts +199 -0
- package/dist/types/src/InputDate/InputDate.d.ts +74 -0
- package/dist/types/src/InputDate/index.d.ts +1 -0
- package/dist/types/src/InputDate/messages.d.ts +7 -0
- package/dist/types/src/InputNumber/InputNumber.d.ts +1 -1
- package/dist/types/src/Menu/Menu.d.ts +3 -0
- package/dist/types/src/Menu/Menu.style.d.ts +18 -0
- package/dist/types/src/Menu/components/MenuOption/MenuOption.d.ts +3 -0
- package/dist/types/src/Menu/components/MenuOption/MenuOption.style.d.ts +8 -0
- package/dist/types/src/Menu/components/MenuOption/index.d.ts +1 -0
- package/dist/types/src/Menu/components/Overlay/Overlay.d.ts +3 -0
- package/dist/types/src/Menu/components/Overlay/Overlay.style.d.ts +12 -0
- package/dist/types/src/Menu/components/Overlay/index.d.ts +1 -0
- package/dist/types/src/Menu/index.d.ts +2 -0
- package/dist/types/src/Menu/messages.d.ts +7 -0
- package/dist/types/src/Menu/types.d.ts +22 -0
- package/dist/types/src/Menu/utils.d.ts +10 -0
- package/dist/types/src/index.d.ts +3 -0
- package/package.json +3 -2
- package/src/AtlantisContext/AtlantisContext.tsx +10 -1
- package/src/Form/Form.style.ts +34 -0
- package/src/Form/Form.test.tsx +588 -0
- package/src/Form/Form.tsx +296 -0
- package/src/Form/components/FormActionBar/FormActionBar.style.ts +11 -0
- package/src/Form/components/FormActionBar/FormActionBar.tsx +63 -0
- package/src/Form/components/FormActionBar/index.ts +2 -0
- package/src/Form/components/FormBody/FormBody.style.ts +27 -0
- package/src/Form/components/FormBody/FormBody.tsx +62 -0
- package/src/Form/components/FormBody/index.ts +1 -0
- package/src/Form/components/FormCache/FormCache.tsx +50 -0
- package/src/Form/components/FormErrorBanner/FormErrorBanner.test.tsx +124 -0
- package/src/Form/components/FormErrorBanner/FormErrorBanner.tsx +34 -0
- package/src/Form/components/FormErrorBanner/index.ts +1 -0
- package/src/Form/components/FormErrorBanner/messages.ts +14 -0
- package/src/Form/components/FormMask/FormMask.style.tsx +16 -0
- package/src/Form/components/FormMask/FormMask.tsx +19 -0
- package/src/Form/components/FormMask/index.ts +1 -0
- package/src/Form/components/FormMessage/FormMessage.test.tsx +72 -0
- package/src/Form/components/FormMessage/FormMessage.tsx +63 -0
- package/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.style.ts +18 -0
- package/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.tsx +55 -0
- package/src/Form/components/FormMessage/components/InternalFormMessage/index.ts +1 -0
- package/src/Form/components/FormMessage/components/InternalFormMessage/messages.ts +10 -0
- package/src/Form/components/FormMessage/index.ts +1 -0
- package/src/Form/components/FormMessageBanner/FormMessageBanner.test.tsx +27 -0
- package/src/Form/components/FormMessageBanner/FormMessageBanner.tsx +33 -0
- package/src/Form/components/FormMessageBanner/index.ts +1 -0
- package/src/Form/components/FormSaveButton/FormSaveButton.test.tsx +159 -0
- package/src/Form/components/FormSaveButton/FormSaveButton.tsx +103 -0
- package/src/Form/components/FormSaveButton/index.ts +1 -0
- package/src/Form/components/FormSaveButton/messages.ts +9 -0
- package/src/Form/constants.ts +2 -0
- package/src/Form/context/AtlantisFormContext.test.tsx +45 -0
- package/src/Form/context/AtlantisFormContext.tsx +21 -0
- package/src/Form/context/index.ts +5 -0
- package/src/Form/context/types.ts +34 -0
- package/src/Form/hooks/useFormViewRefs.ts +23 -0
- package/src/Form/hooks/useInternalForm.ts +99 -0
- package/src/Form/hooks/useOfflineHandler.ts +36 -0
- package/src/Form/hooks/useSaveButtonPosition.ts +52 -0
- package/src/Form/hooks/useScreenInformation.ts +25 -0
- package/src/Form/hooks/useScrollToError/index.ts +1 -0
- package/src/Form/hooks/useScrollToError/useScrollToError.test.tsx +103 -0
- package/src/Form/hooks/useScrollToError/useScrollToError.ts +102 -0
- package/src/Form/index.ts +13 -0
- package/src/Form/messages.ts +33 -0
- package/src/Form/types.ts +255 -0
- package/src/InputDate/InputDate.test.tsx +295 -0
- package/src/InputDate/InputDate.tsx +231 -0
- package/src/InputDate/index.ts +1 -0
- package/src/InputDate/messages.ts +9 -0
- package/src/InputNumber/InputNumber.tsx +1 -1
- package/src/Menu/Menu.style.ts +16 -0
- package/src/Menu/Menu.test.tsx +201 -0
- package/src/Menu/Menu.tsx +116 -0
- package/src/Menu/components/MenuOption/MenuOption.style.tsx +11 -0
- package/src/Menu/components/MenuOption/MenuOption.tsx +63 -0
- package/src/Menu/components/MenuOption/index.ts +1 -0
- package/src/Menu/components/Overlay/Overlay.style.ts +13 -0
- package/src/Menu/components/Overlay/Overlay.tsx +16 -0
- package/src/Menu/components/Overlay/index.ts +1 -0
- package/src/Menu/index.ts +6 -0
- package/src/Menu/messages.ts +9 -0
- package/src/Menu/types.ts +25 -0
- package/src/Menu/utils.ts +151 -0
- package/src/index.ts +3 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
import { tokens } from "../../../utils/design";
|
|
3
|
+
|
|
4
|
+
export const styles = StyleSheet.create({
|
|
5
|
+
mask: {
|
|
6
|
+
zIndex: tokens["elevation-modal"],
|
|
7
|
+
width: "100%",
|
|
8
|
+
height: "100%",
|
|
9
|
+
position: "absolute",
|
|
10
|
+
top: 0,
|
|
11
|
+
left: 0,
|
|
12
|
+
backgroundColor: tokens["color-overlay--dimmed"],
|
|
13
|
+
padding: tokens["space-base"],
|
|
14
|
+
justifyContent: "center",
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { useIntl } from "react-intl";
|
|
4
|
+
import { styles } from "./FormMask.style";
|
|
5
|
+
import { ActivityIndicator } from "../../../ActivityIndicator";
|
|
6
|
+
import { messages } from "../../messages";
|
|
7
|
+
|
|
8
|
+
export function FormMask(): JSX.Element {
|
|
9
|
+
const { formatMessage } = useIntl();
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<View
|
|
13
|
+
style={styles.mask}
|
|
14
|
+
accessibilityLabel={formatMessage(messages.loadingA11YLabel)}
|
|
15
|
+
>
|
|
16
|
+
<ActivityIndicator />
|
|
17
|
+
</View>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FormMask } from "./FormMask";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fireEvent, render } from "@testing-library/react-native";
|
|
3
|
+
import { FormMessage } from ".";
|
|
4
|
+
|
|
5
|
+
describe("FormMessage", () => {
|
|
6
|
+
it("should render null when there are no message to show", () => {
|
|
7
|
+
const view = render(<FormMessage />);
|
|
8
|
+
expect(view.toJSON()).toMatchInlineSnapshot(`null`);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should show the message", () => {
|
|
12
|
+
const { getByText } = render(<FormMessage />);
|
|
13
|
+
|
|
14
|
+
const description = "🔥";
|
|
15
|
+
FormMessage.show({ description });
|
|
16
|
+
expect(getByText(description)).toBeDefined();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should close the message", () => {
|
|
20
|
+
const { getByText, queryByText } = render(<FormMessage />);
|
|
21
|
+
|
|
22
|
+
const description = "🌚";
|
|
23
|
+
FormMessage.show({ description });
|
|
24
|
+
expect(getByText(description)).toBeDefined();
|
|
25
|
+
|
|
26
|
+
FormMessage.close();
|
|
27
|
+
expect(queryByText(description)).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("Opening another message through a message", () => {
|
|
31
|
+
const firstMessage = "I am the first message";
|
|
32
|
+
const secondMessage = "Second message here";
|
|
33
|
+
|
|
34
|
+
const showMessage = () =>
|
|
35
|
+
FormMessage.show({
|
|
36
|
+
description: firstMessage,
|
|
37
|
+
primaryAction: {
|
|
38
|
+
label: "Click me",
|
|
39
|
+
onPress: () => FormMessage.show({ description: secondMessage }),
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should show the most recent message", () => {
|
|
44
|
+
const { getByText, queryByText, getByLabelText } = render(
|
|
45
|
+
<FormMessage />,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
showMessage();
|
|
49
|
+
|
|
50
|
+
expect(getByText(firstMessage)).toBeDefined();
|
|
51
|
+
expect(queryByText(secondMessage)).toBeNull();
|
|
52
|
+
|
|
53
|
+
fireEvent.press(getByLabelText("Click me"));
|
|
54
|
+
|
|
55
|
+
expect(getByText(secondMessage)).toBeDefined();
|
|
56
|
+
expect(queryByText(firstMessage)).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should close the most recent message", () => {
|
|
60
|
+
const { getByText, queryByText, getByLabelText } = render(
|
|
61
|
+
<FormMessage />,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
showMessage();
|
|
65
|
+
fireEvent.press(getByLabelText("Click me"));
|
|
66
|
+
FormMessage.close();
|
|
67
|
+
|
|
68
|
+
expect(getByText(firstMessage)).toBeDefined();
|
|
69
|
+
expect(queryByText(secondMessage)).toBeNull();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React, { useCallback, useState } from "react";
|
|
2
|
+
import { InternalFormMessage } from "./components/InternalFormMessage";
|
|
3
|
+
import { EmptyStateProps } from "../../../EmptyState";
|
|
4
|
+
|
|
5
|
+
type FormMessageData = EmptyStateProps;
|
|
6
|
+
|
|
7
|
+
let open: ((messageData: FormMessageData) => void) | undefined;
|
|
8
|
+
let close: (() => void) | undefined;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Show a message that takes over the whole screen to the user. This provides a
|
|
12
|
+
* more urgent feedback when the user does an action that requires attention
|
|
13
|
+
* their full attention.
|
|
14
|
+
*
|
|
15
|
+
* By default, rendering `<FormMessage />` on a screen won't show any messages
|
|
16
|
+
* because it's only a container. Use `FormMessage.open(...)`. to show a
|
|
17
|
+
* message to the user. Use `FormMessage.close()` to close the most
|
|
18
|
+
* recent message.
|
|
19
|
+
*/
|
|
20
|
+
export const FormMessage = (): JSX.Element => {
|
|
21
|
+
const [data, setData] = useState<FormMessageData[]>([]);
|
|
22
|
+
|
|
23
|
+
open = useCallback(
|
|
24
|
+
(messageData: FormMessageData) => {
|
|
25
|
+
setData([...data, messageData]);
|
|
26
|
+
},
|
|
27
|
+
[data],
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
close = useCallback(() => {
|
|
31
|
+
const newValue = data.slice(0, -1);
|
|
32
|
+
setData(newValue);
|
|
33
|
+
}, [data]);
|
|
34
|
+
|
|
35
|
+
if (data.length === 0) {
|
|
36
|
+
return <></>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const lastMessage = data[data.length - 1];
|
|
40
|
+
return <InternalFormMessage data={lastMessage} onRequestClose={close} />;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
FormMessage.show = (messageData: FormMessageData) => {
|
|
44
|
+
if (open) {
|
|
45
|
+
open(messageData);
|
|
46
|
+
} else {
|
|
47
|
+
warnOnUndefinedFunctions("show");
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
FormMessage.close = () => {
|
|
52
|
+
if (close) {
|
|
53
|
+
close();
|
|
54
|
+
} else {
|
|
55
|
+
warnOnUndefinedFunctions("close");
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function warnOnUndefinedFunctions(method: string) {
|
|
60
|
+
console.warn(
|
|
61
|
+
`Could not ${method} "FormMessage". Either you're calling this method before the component mounts or you're using this without the "<Form />" component. If you're using "FormMessage" without the "Form", include "<FormMessage />" on your component to start using it.`,
|
|
62
|
+
);
|
|
63
|
+
}
|
package/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.style.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
import { tokens } from "../../../../../utils/design";
|
|
3
|
+
|
|
4
|
+
export const styles = StyleSheet.create({
|
|
5
|
+
wrapper: {
|
|
6
|
+
backgroundColor: tokens["color-surface"],
|
|
7
|
+
flex: 1,
|
|
8
|
+
justifyContent: "center",
|
|
9
|
+
},
|
|
10
|
+
closeAction: {
|
|
11
|
+
position: "absolute",
|
|
12
|
+
top: 0,
|
|
13
|
+
right: 0,
|
|
14
|
+
zIndex: 1,
|
|
15
|
+
},
|
|
16
|
+
scrollWrapper: { height: "100%" },
|
|
17
|
+
scrollWrapperContent: { flexGrow: 1, justifyContent: "center" },
|
|
18
|
+
});
|
package/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.tsx
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
3
|
+
import { Modal, StatusBar, View } from "react-native";
|
|
4
|
+
import { ScrollView } from "react-native-gesture-handler";
|
|
5
|
+
import { useIntl } from "react-intl";
|
|
6
|
+
import { styles } from "./InternalFormMessage.style";
|
|
7
|
+
import { messages } from "./messages";
|
|
8
|
+
import { EmptyState, EmptyStateProps } from "../../../../../EmptyState";
|
|
9
|
+
|
|
10
|
+
interface FormMessageProps {
|
|
11
|
+
readonly data: EmptyStateProps;
|
|
12
|
+
readonly onRequestClose: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function InternalFormMessage({
|
|
16
|
+
data,
|
|
17
|
+
onRequestClose,
|
|
18
|
+
}: FormMessageProps): JSX.Element {
|
|
19
|
+
const { formatMessage } = useIntl();
|
|
20
|
+
const emptyStateData: EmptyStateProps = useMemo(() => {
|
|
21
|
+
if (data.secondaryAction) {
|
|
22
|
+
return data;
|
|
23
|
+
} else {
|
|
24
|
+
return {
|
|
25
|
+
...data,
|
|
26
|
+
secondaryAction: {
|
|
27
|
+
label: formatMessage(messages.goBackButton),
|
|
28
|
+
onPress: onRequestClose,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}, [data, formatMessage, onRequestClose]);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Modal
|
|
36
|
+
animationType="fade"
|
|
37
|
+
transparent={true}
|
|
38
|
+
visible={true}
|
|
39
|
+
onRequestClose={onRequestClose}
|
|
40
|
+
>
|
|
41
|
+
<View style={styles.wrapper}>
|
|
42
|
+
<StatusBar barStyle="dark-content" />
|
|
43
|
+
<SafeAreaView>
|
|
44
|
+
<ScrollView
|
|
45
|
+
style={styles.scrollWrapper}
|
|
46
|
+
contentContainerStyle={styles.scrollWrapperContent}
|
|
47
|
+
centerContent={true}
|
|
48
|
+
>
|
|
49
|
+
<EmptyState {...emptyStateData} />
|
|
50
|
+
</ScrollView>
|
|
51
|
+
</SafeAreaView>
|
|
52
|
+
</View>
|
|
53
|
+
</Modal>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { InternalFormMessage } from "./InternalFormMessage";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FormMessage } from "./FormMessage";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "@testing-library/react-native";
|
|
3
|
+
import { FormMessageBanner } from "./FormMessageBanner";
|
|
4
|
+
import { FormBannerMessageType } from "../../types";
|
|
5
|
+
|
|
6
|
+
const noticeMessage = {
|
|
7
|
+
messageType: FormBannerMessageType.NoticeMessage,
|
|
8
|
+
message: "Take note of this information.",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const warningMessage = {
|
|
12
|
+
messageType: FormBannerMessageType.WarningMessage,
|
|
13
|
+
message: "Caution is warranted in this case.",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe("FormMessageBanner", () => {
|
|
17
|
+
it.each([
|
|
18
|
+
[FormBannerMessageType.NoticeMessage, noticeMessage],
|
|
19
|
+
[FormBannerMessageType.WarningMessage, warningMessage],
|
|
20
|
+
])("should render a %s", async (_messageType, message) => {
|
|
21
|
+
const { getByText } = render(
|
|
22
|
+
<FormMessageBanner bannerMessages={[message]} />,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(getByText(message.message)).toBeDefined();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { FormBannerMessage, FormBannerMessageType } from "../../types";
|
|
3
|
+
import { Banner, BannerTypes } from "../../../Banner";
|
|
4
|
+
|
|
5
|
+
interface FormMessageBannerProps {
|
|
6
|
+
bannerMessages?: FormBannerMessage[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function FormMessageBanner({
|
|
10
|
+
bannerMessages,
|
|
11
|
+
}: FormMessageBannerProps): JSX.Element {
|
|
12
|
+
return (
|
|
13
|
+
<>
|
|
14
|
+
{bannerMessages?.map((message, index) => (
|
|
15
|
+
<Banner
|
|
16
|
+
key={index}
|
|
17
|
+
text={message.message}
|
|
18
|
+
type={getBannerType(message.messageType)}
|
|
19
|
+
/>
|
|
20
|
+
))}
|
|
21
|
+
</>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getBannerType(messageType: FormBannerMessageType): BannerTypes {
|
|
26
|
+
switch (messageType) {
|
|
27
|
+
case FormBannerMessageType.WarningMessage:
|
|
28
|
+
return "warning";
|
|
29
|
+
case FormBannerMessageType.NoticeMessage:
|
|
30
|
+
default:
|
|
31
|
+
return "notice";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FormMessageBanner } from "./FormMessageBanner";
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fireEvent, render, waitFor } from "@testing-library/react-native";
|
|
3
|
+
import { Host } from "react-native-portalize";
|
|
4
|
+
import { IconNames } from "@jobber/design";
|
|
5
|
+
import { FormSaveButton } from "./FormSaveButton";
|
|
6
|
+
import { messages } from "./messages";
|
|
7
|
+
import { messages as buttonGroupMessage } from "../../../ButtonGroup/messages";
|
|
8
|
+
|
|
9
|
+
interface TestSecondaryActionProp {
|
|
10
|
+
label: string;
|
|
11
|
+
icon?: IconNames | undefined;
|
|
12
|
+
handleAction: {
|
|
13
|
+
onBeforeSubmit?: jest.Mock;
|
|
14
|
+
onSubmit: () => Promise<void>;
|
|
15
|
+
onSubmitError?: () => void;
|
|
16
|
+
resetFormOnSubmit?: boolean;
|
|
17
|
+
};
|
|
18
|
+
destructive?: boolean;
|
|
19
|
+
}
|
|
20
|
+
interface TestFormSaveButtonProps {
|
|
21
|
+
primaryAction: () => Promise<void>;
|
|
22
|
+
loading: boolean;
|
|
23
|
+
label?: string;
|
|
24
|
+
secondaryAction?: TestSecondaryActionProp[];
|
|
25
|
+
setSecondaryActionLoading: (bool: boolean) => void;
|
|
26
|
+
}
|
|
27
|
+
jest.mock("react-hook-form", () => ({
|
|
28
|
+
...jest.requireActual("react-hook-form"),
|
|
29
|
+
useFormContext: () => ({
|
|
30
|
+
reset: () => jest.fn(),
|
|
31
|
+
}),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
function ButtonGroupForTest(props: TestFormSaveButtonProps) {
|
|
35
|
+
return (
|
|
36
|
+
<Host>
|
|
37
|
+
<FormSaveButton
|
|
38
|
+
primaryAction={props.primaryAction}
|
|
39
|
+
loading={props.loading}
|
|
40
|
+
label={props.label}
|
|
41
|
+
setSecondaryActionLoading={props.setSecondaryActionLoading}
|
|
42
|
+
secondaryActions={props.secondaryAction}
|
|
43
|
+
/>
|
|
44
|
+
</Host>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("the form save button is enabled", () => {
|
|
49
|
+
const loading = false;
|
|
50
|
+
it("renders the form save button with default label", () => {
|
|
51
|
+
const pressHandler = jest.fn();
|
|
52
|
+
const { getByLabelText } = render(
|
|
53
|
+
<ButtonGroupForTest
|
|
54
|
+
primaryAction={pressHandler}
|
|
55
|
+
loading={loading}
|
|
56
|
+
setSecondaryActionLoading={jest.fn()}
|
|
57
|
+
/>,
|
|
58
|
+
);
|
|
59
|
+
const saveButton = getByLabelText("Save");
|
|
60
|
+
expect(saveButton).toBeTruthy();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("renders a save button and calls the onPress handler when pressed", () => {
|
|
64
|
+
const pressHandler = jest.fn();
|
|
65
|
+
const saveButtonText = messages.saveButton.defaultMessage;
|
|
66
|
+
const { getByLabelText } = render(
|
|
67
|
+
<ButtonGroupForTest
|
|
68
|
+
primaryAction={pressHandler}
|
|
69
|
+
loading={loading}
|
|
70
|
+
setSecondaryActionLoading={jest.fn()}
|
|
71
|
+
/>,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
fireEvent.press(getByLabelText(saveButtonText));
|
|
75
|
+
expect(pressHandler).toHaveBeenCalled();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("renders a save button with a custom label if provided", () => {
|
|
79
|
+
const pressHandler = jest.fn();
|
|
80
|
+
const saveButtonText = "MySave";
|
|
81
|
+
const { getByLabelText } = render(
|
|
82
|
+
<ButtonGroupForTest
|
|
83
|
+
primaryAction={pressHandler}
|
|
84
|
+
loading={loading}
|
|
85
|
+
setSecondaryActionLoading={jest.fn()}
|
|
86
|
+
label={saveButtonText}
|
|
87
|
+
/>,
|
|
88
|
+
);
|
|
89
|
+
const saveButton = getByLabelText(saveButtonText);
|
|
90
|
+
expect(saveButton).toBeTruthy();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("the form save button is loading", () => {
|
|
95
|
+
const loading = true;
|
|
96
|
+
it("renders the form save button as loading", () => {
|
|
97
|
+
const pressHandler = jest.fn();
|
|
98
|
+
const { getByTestId, getByRole } = render(
|
|
99
|
+
<ButtonGroupForTest
|
|
100
|
+
primaryAction={pressHandler}
|
|
101
|
+
loading={loading}
|
|
102
|
+
setSecondaryActionLoading={jest.fn()}
|
|
103
|
+
/>,
|
|
104
|
+
);
|
|
105
|
+
expect(getByTestId("loadingImage")).toBeDefined();
|
|
106
|
+
expect(getByRole("button", { busy: true })).toBeDefined();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("when a secondaryActions is passed in", () => {
|
|
111
|
+
it("renders a secondaryAction element", () => {
|
|
112
|
+
const pressHandler = jest.fn();
|
|
113
|
+
const { getByLabelText } = render(
|
|
114
|
+
<ButtonGroupForTest
|
|
115
|
+
primaryAction={pressHandler}
|
|
116
|
+
loading={false}
|
|
117
|
+
setSecondaryActionLoading={jest.fn()}
|
|
118
|
+
secondaryAction={[
|
|
119
|
+
{ label: "hi", handleAction: { onSubmit: jest.fn() } },
|
|
120
|
+
]}
|
|
121
|
+
/>,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(
|
|
125
|
+
getByLabelText(buttonGroupMessage.more.defaultMessage),
|
|
126
|
+
).toBeDefined();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("renders a secondaryAction element with and fires the onSubmit and beforeSubmit if available", async () => {
|
|
130
|
+
const pressHandler = jest.fn(() => Promise.resolve());
|
|
131
|
+
const beforeSubmitMock = jest.fn().mockImplementation(() => {
|
|
132
|
+
return Promise.resolve(true);
|
|
133
|
+
});
|
|
134
|
+
const { getByLabelText } = render(
|
|
135
|
+
<ButtonGroupForTest
|
|
136
|
+
primaryAction={pressHandler}
|
|
137
|
+
loading={false}
|
|
138
|
+
setSecondaryActionLoading={jest.fn()}
|
|
139
|
+
secondaryAction={[
|
|
140
|
+
{
|
|
141
|
+
icon: "trash",
|
|
142
|
+
label: "hi",
|
|
143
|
+
handleAction: {
|
|
144
|
+
onSubmit: pressHandler,
|
|
145
|
+
onBeforeSubmit: beforeSubmitMock,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
]}
|
|
149
|
+
/>,
|
|
150
|
+
);
|
|
151
|
+
fireEvent.press(getByLabelText(buttonGroupMessage.more.defaultMessage));
|
|
152
|
+
expect(getByLabelText("hi")).toBeDefined();
|
|
153
|
+
fireEvent.press(getByLabelText("hi"));
|
|
154
|
+
expect(beforeSubmitMock).toHaveBeenCalled();
|
|
155
|
+
await waitFor(() => {
|
|
156
|
+
expect(pressHandler).toHaveBeenCalled();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useIntl } from "react-intl";
|
|
3
|
+
import { useFormContext } from "react-hook-form";
|
|
4
|
+
import { messages } from "./messages";
|
|
5
|
+
import { FormSaveButtonProps, SecondaryActionProp } from "../../types";
|
|
6
|
+
import {
|
|
7
|
+
ButtonGroup,
|
|
8
|
+
ButtonGroupSecondaryActionProps,
|
|
9
|
+
} from "../../../ButtonGroup";
|
|
10
|
+
|
|
11
|
+
export function FormSaveButton({
|
|
12
|
+
primaryAction,
|
|
13
|
+
loading,
|
|
14
|
+
label,
|
|
15
|
+
secondaryActions,
|
|
16
|
+
setSecondaryActionLoading,
|
|
17
|
+
onOpenBottomSheet,
|
|
18
|
+
onCloseBottomSheet,
|
|
19
|
+
}: FormSaveButtonProps): JSX.Element {
|
|
20
|
+
const { formatMessage } = useIntl();
|
|
21
|
+
const formContext = useFormContext();
|
|
22
|
+
const buttonActions = useButtonGroupAction(secondaryActions);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<ButtonGroup
|
|
27
|
+
onOpenBottomSheet={onOpenBottomSheet}
|
|
28
|
+
onCloseBottomSheet={onCloseBottomSheet}
|
|
29
|
+
allowTapWhenOffline={true}
|
|
30
|
+
>
|
|
31
|
+
{buttonActions.map((action, index) => {
|
|
32
|
+
if (index === 0) {
|
|
33
|
+
return (
|
|
34
|
+
<ButtonGroup.PrimaryAction
|
|
35
|
+
key={index}
|
|
36
|
+
onPress={primaryAction}
|
|
37
|
+
label={label ?? formatMessage(messages.saveButton)}
|
|
38
|
+
loading={loading}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
} else {
|
|
42
|
+
return (
|
|
43
|
+
<ButtonGroup.SecondaryAction
|
|
44
|
+
key={index}
|
|
45
|
+
label={action.label}
|
|
46
|
+
icon={action.icon}
|
|
47
|
+
onPress={action.onPress}
|
|
48
|
+
destructive={action.destructive}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
})}
|
|
53
|
+
</ButtonGroup>
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
function useButtonGroupAction(
|
|
58
|
+
array: SecondaryActionProp[] | undefined,
|
|
59
|
+
): ButtonGroupSecondaryActionProps[] {
|
|
60
|
+
const buttonGroupActionProps: ButtonGroupSecondaryActionProps[] = array
|
|
61
|
+
? array.map(arr => {
|
|
62
|
+
return {
|
|
63
|
+
label: arr.label,
|
|
64
|
+
onPress: () => internalOnPress(arr.handleAction),
|
|
65
|
+
destructive: arr.destructive,
|
|
66
|
+
icon: arr.icon,
|
|
67
|
+
};
|
|
68
|
+
})
|
|
69
|
+
: [];
|
|
70
|
+
|
|
71
|
+
buttonGroupActionProps.unshift({
|
|
72
|
+
label: label ?? formatMessage(messages.saveButton),
|
|
73
|
+
onPress: primaryAction,
|
|
74
|
+
loading: loading,
|
|
75
|
+
icon: undefined,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return buttonGroupActionProps;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function internalOnPress(
|
|
82
|
+
handleAction: SecondaryActionProp["handleAction"],
|
|
83
|
+
) {
|
|
84
|
+
let performSubmit = true;
|
|
85
|
+
if (handleAction.onBeforeSubmit) {
|
|
86
|
+
performSubmit = await handleAction.onBeforeSubmit();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (performSubmit) {
|
|
90
|
+
setSecondaryActionLoading?.(true);
|
|
91
|
+
handleAction
|
|
92
|
+
.onSubmit(primaryAction)
|
|
93
|
+
.then(() => {
|
|
94
|
+
handleAction.resetFormOnSubmit && formContext.reset();
|
|
95
|
+
handleAction.onSubmitSuccess?.();
|
|
96
|
+
})
|
|
97
|
+
.catch(handleAction.onSubmitError)
|
|
98
|
+
.finally(() => {
|
|
99
|
+
setSecondaryActionLoading?.(false);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FormSaveButton } from "./FormSaveButton";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React, { PropsWithChildren } from "react";
|
|
2
|
+
import { renderHook } from "@testing-library/react-hooks";
|
|
3
|
+
import { AtlantisFormContextProps } from "./types";
|
|
4
|
+
import {
|
|
5
|
+
AtlantisFormContext,
|
|
6
|
+
defaultValues,
|
|
7
|
+
useAtlantisFormContext,
|
|
8
|
+
} from "./AtlantisFormContext";
|
|
9
|
+
|
|
10
|
+
const useConfirmBeforeBackMock = jest.fn();
|
|
11
|
+
const useInternalFormLocalCacheMock = jest.fn();
|
|
12
|
+
|
|
13
|
+
const providerValues: AtlantisFormContextProps = {
|
|
14
|
+
useConfirmBeforeBack: useConfirmBeforeBackMock,
|
|
15
|
+
useInternalFormLocalCache: useInternalFormLocalCacheMock,
|
|
16
|
+
headerHeight: 50,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe("AtlantisFormContext", () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
jest.resetModules();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("No Provider", () => {
|
|
25
|
+
it("should get the default values", () => {
|
|
26
|
+
const { result } = renderHook(() => useAtlantisFormContext());
|
|
27
|
+
|
|
28
|
+
expect(result.current).toMatchObject(defaultValues);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("With Provider", () => {
|
|
33
|
+
it("should get the provider values", () => {
|
|
34
|
+
const { result } = renderHook(() => useAtlantisFormContext(), {
|
|
35
|
+
wrapper: ({ children }: PropsWithChildren) => (
|
|
36
|
+
<AtlantisFormContext.Provider value={providerValues}>
|
|
37
|
+
{children}
|
|
38
|
+
</AtlantisFormContext.Provider>
|
|
39
|
+
),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(result.current).toMatchObject(providerValues);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createContext, useContext, useRef } from "react";
|
|
2
|
+
import { AtlantisFormContextProps } from "./types";
|
|
3
|
+
|
|
4
|
+
export const defaultValues = {
|
|
5
|
+
useConfirmBeforeBack: () => {
|
|
6
|
+
const ref = useRef(() => undefined);
|
|
7
|
+
return ref;
|
|
8
|
+
},
|
|
9
|
+
useInternalFormLocalCache: () => ({
|
|
10
|
+
setLocalCache: () => undefined,
|
|
11
|
+
removeLocalCache: () => undefined,
|
|
12
|
+
}),
|
|
13
|
+
headerHeight: 0,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const AtlantisFormContext =
|
|
17
|
+
createContext<AtlantisFormContextProps>(defaultValues);
|
|
18
|
+
|
|
19
|
+
export function useAtlantisFormContext(): AtlantisFormContextProps {
|
|
20
|
+
return useContext(AtlantisFormContext);
|
|
21
|
+
}
|