@jobber/components-native 0.40.0 → 0.41.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.
Files changed (95) hide show
  1. package/dist/src/FormatFile/FormatFile.js +114 -0
  2. package/dist/src/FormatFile/FormatFile.style.js +16 -0
  3. package/dist/src/FormatFile/components/ErrorIcon/ErrorIcon.js +8 -0
  4. package/dist/src/FormatFile/components/ErrorIcon/ErrorIcon.style.js +10 -0
  5. package/dist/src/FormatFile/components/ErrorIcon/index.js +1 -0
  6. package/dist/src/FormatFile/components/FileView/FileView.js +67 -0
  7. package/dist/src/FormatFile/components/FileView/FileView.style.js +64 -0
  8. package/dist/src/FormatFile/components/FileView/index.js +1 -0
  9. package/dist/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.js +22 -0
  10. package/dist/src/FormatFile/components/FormatFileBottomSheet/index.js +1 -0
  11. package/dist/src/FormatFile/components/FormatFileBottomSheet/messages.js +13 -0
  12. package/dist/src/FormatFile/components/MediaView/MediaView.js +56 -0
  13. package/dist/src/FormatFile/components/MediaView/MediaView.style.js +27 -0
  14. package/dist/src/FormatFile/components/MediaView/index.js +1 -0
  15. package/dist/src/FormatFile/components/ProgressBar/ProgressBar.js +29 -0
  16. package/dist/src/FormatFile/components/ProgressBar/ProgressBar.style.js +15 -0
  17. package/dist/src/FormatFile/components/ProgressBar/index.js +1 -0
  18. package/dist/src/FormatFile/components/_mocks/mockFiles.js +78 -0
  19. package/dist/src/FormatFile/constants.js +14 -0
  20. package/dist/src/FormatFile/context/FormatFileContext.js +8 -0
  21. package/dist/src/FormatFile/context/types.js +1 -0
  22. package/dist/src/FormatFile/index.js +1 -0
  23. package/dist/src/FormatFile/messages.js +23 -0
  24. package/dist/src/FormatFile/types.js +8 -0
  25. package/dist/src/FormatFile/utils/computeA11yLabel.js +12 -0
  26. package/dist/src/FormatFile/utils/createUseCreateThumbnail.js +22 -0
  27. package/dist/src/FormatFile/utils/index.js +1 -0
  28. package/dist/src/InputText/InputText.js +8 -1
  29. package/dist/src/index.js +1 -0
  30. package/dist/src/utils/test/wait.js +1 -1
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/dist/types/src/Form/components/FormMessage/FormMessage.d.ts +1 -0
  33. package/dist/types/src/FormatFile/FormatFile.d.ts +47 -0
  34. package/dist/types/src/FormatFile/FormatFile.style.d.ts +14 -0
  35. package/dist/types/src/FormatFile/components/ErrorIcon/ErrorIcon.d.ts +2 -0
  36. package/dist/types/src/FormatFile/components/ErrorIcon/ErrorIcon.style.d.ts +8 -0
  37. package/dist/types/src/FormatFile/components/ErrorIcon/index.d.ts +1 -0
  38. package/dist/types/src/FormatFile/components/FileView/FileView.d.ts +12 -0
  39. package/dist/types/src/FormatFile/components/FileView/FileView.style.d.ts +62 -0
  40. package/dist/types/src/FormatFile/components/FileView/index.d.ts +1 -0
  41. package/dist/types/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.d.ts +11 -0
  42. package/dist/types/src/FormatFile/components/FormatFileBottomSheet/index.d.ts +2 -0
  43. package/dist/types/src/FormatFile/components/FormatFileBottomSheet/messages.d.ts +12 -0
  44. package/dist/types/src/FormatFile/components/MediaView/MediaView.d.ts +12 -0
  45. package/dist/types/src/FormatFile/components/MediaView/MediaView.style.d.ts +25 -0
  46. package/dist/types/src/FormatFile/components/MediaView/index.d.ts +1 -0
  47. package/dist/types/src/FormatFile/components/ProgressBar/ProgressBar.d.ts +19 -0
  48. package/dist/types/src/FormatFile/components/ProgressBar/ProgressBar.style.d.ts +13 -0
  49. package/dist/types/src/FormatFile/components/ProgressBar/index.d.ts +1 -0
  50. package/dist/types/src/FormatFile/components/_mocks/mockFiles.d.ts +18 -0
  51. package/dist/types/src/FormatFile/constants.d.ts +6 -0
  52. package/dist/types/src/FormatFile/context/FormatFileContext.d.ts +10 -0
  53. package/dist/types/src/FormatFile/context/types.d.ts +9 -0
  54. package/dist/types/src/FormatFile/index.d.ts +3 -0
  55. package/dist/types/src/FormatFile/messages.d.ts +22 -0
  56. package/dist/types/src/FormatFile/types.d.ts +105 -0
  57. package/dist/types/src/FormatFile/utils/computeA11yLabel.d.ts +9 -0
  58. package/dist/types/src/FormatFile/utils/createUseCreateThumbnail.d.ts +5 -0
  59. package/dist/types/src/FormatFile/utils/index.d.ts +1 -0
  60. package/dist/types/src/InputCurrency/InputCurrency.d.ts +1 -1
  61. package/dist/types/src/index.d.ts +1 -0
  62. package/package.json +3 -2
  63. package/src/FormatFile/FormatFile.style.ts +17 -0
  64. package/src/FormatFile/FormatFile.test.tsx +333 -0
  65. package/src/FormatFile/FormatFile.tsx +300 -0
  66. package/src/FormatFile/components/ErrorIcon/ErrorIcon.style.ts +11 -0
  67. package/src/FormatFile/components/ErrorIcon/ErrorIcon.tsx +12 -0
  68. package/src/FormatFile/components/ErrorIcon/index.ts +1 -0
  69. package/src/FormatFile/components/FileView/FileView.style.ts +65 -0
  70. package/src/FormatFile/components/FileView/FileView.tsx +134 -0
  71. package/src/FormatFile/components/FileView/index.ts +1 -0
  72. package/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.test.tsx +108 -0
  73. package/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.tsx +56 -0
  74. package/src/FormatFile/components/FormatFileBottomSheet/index.ts +2 -0
  75. package/src/FormatFile/components/FormatFileBottomSheet/messages.ts +14 -0
  76. package/src/FormatFile/components/MediaView/MediaView.style.ts +28 -0
  77. package/src/FormatFile/components/MediaView/MediaView.tsx +145 -0
  78. package/src/FormatFile/components/MediaView/index.ts +1 -0
  79. package/src/FormatFile/components/ProgressBar/ProgressBar.style.tsx +16 -0
  80. package/src/FormatFile/components/ProgressBar/ProgressBar.tsx +57 -0
  81. package/src/FormatFile/components/ProgressBar/index.ts +1 -0
  82. package/src/FormatFile/components/_mocks/mockFiles.ts +105 -0
  83. package/src/FormatFile/constants.ts +15 -0
  84. package/src/FormatFile/context/FormatFileContext.ts +13 -0
  85. package/src/FormatFile/context/types.ts +12 -0
  86. package/src/FormatFile/index.ts +13 -0
  87. package/src/FormatFile/messages.ts +24 -0
  88. package/src/FormatFile/types.ts +126 -0
  89. package/src/FormatFile/utils/computeA11yLabel.ts +26 -0
  90. package/src/FormatFile/utils/createUseCreateThumbnail.ts +33 -0
  91. package/src/FormatFile/utils/index.ts +1 -0
  92. package/src/InputCurrency/InputCurrency.tsx +1 -1
  93. package/src/InputText/InputText.tsx +8 -1
  94. package/src/index.ts +1 -0
  95. package/src/utils/test/wait.ts +3 -1
@@ -0,0 +1,333 @@
1
+ import React from "react";
2
+ import { RenderAPI, fireEvent, render } from "@testing-library/react-native";
3
+ import { Host } from "react-native-portalize";
4
+ import { Alert } from "react-native";
5
+ import { useIntl } from "react-intl";
6
+ import { File, FormatFile } from ".";
7
+ import {
8
+ FILE_MOCK_FILE,
9
+ FILE_MOCK_IMAGE,
10
+ FILE_MOCK_PDF,
11
+ FILE_MOCK_VIDEO,
12
+ FILE_UPLOAD_MOCK_FILE,
13
+ FILE_UPLOAD_MOCK_IMAGE,
14
+ FILE_UPLOAD_MOCK_PDF,
15
+ } from "./components/_mocks/mockFiles";
16
+ import { messages } from "./components/FormatFileBottomSheet/messages";
17
+ import { messages as formatFileMessages } from "./messages";
18
+ import { BottomSheetOptionsSuffix } from "./components/FormatFileBottomSheet";
19
+ import { FileUpload, StatusCode } from "./types";
20
+ import { tokens } from "../utils/design";
21
+
22
+ let Platform: { OS: "ios" | "android" };
23
+
24
+ const onRemove = jest.fn();
25
+ const mockOnPreview = jest.fn();
26
+ const mockCreateThumbnail = jest.fn(async () => ({
27
+ thumbnail: "thumbnail",
28
+ error: false,
29
+ }));
30
+
31
+ beforeEach(() => {
32
+ Platform = require("react-native").Platform;
33
+ });
34
+
35
+ afterEach(() => {
36
+ jest.clearAllMocks();
37
+ });
38
+
39
+ const renderFormatFile = (
40
+ file: FileUpload | File,
41
+ bottomSheetOptionsSuffix?: BottomSheetOptionsSuffix,
42
+ showFileTypeIndicator?: boolean,
43
+ ) => {
44
+ return render(
45
+ <Host>
46
+ <FormatFile
47
+ file={file}
48
+ accessibilityLabel="Custom Label"
49
+ accessibilityHint="Custom Hint Text"
50
+ onTap={() => Alert.alert("alert")}
51
+ onRemove={onRemove}
52
+ bottomSheetOptionsSuffix={bottomSheetOptionsSuffix}
53
+ showFileTypeIndicator={showFileTypeIndicator}
54
+ onPreviewPress={mockOnPreview}
55
+ createThumbnail={mockCreateThumbnail}
56
+ />
57
+ </Host>,
58
+ );
59
+ };
60
+
61
+ function basicRenderTestWithValue() {
62
+ const progressBarAnimationTime = 500;
63
+
64
+ it.each([
65
+ [
66
+ "file",
67
+ FILE_UPLOAD_MOCK_FILE({ progress: 1, status: StatusCode.Completed }),
68
+ ],
69
+ [
70
+ "image",
71
+ FILE_UPLOAD_MOCK_IMAGE({ progress: 1, status: StatusCode.Completed }),
72
+ ],
73
+ ])(
74
+ "renders a %s with custom label and hint",
75
+ (bottomSheetOptionsSuffix, file) => {
76
+ const { getByLabelText, getByHintText } = renderFormatFile(
77
+ file,
78
+ bottomSheetOptionsSuffix as BottomSheetOptionsSuffix,
79
+ );
80
+ expect(getByLabelText("Custom Label")).toBeDefined();
81
+ expect(getByHintText("Custom Hint Text")).toBeDefined();
82
+ },
83
+ );
84
+
85
+ describe.each([
86
+ ["file", FILE_UPLOAD_MOCK_FILE({ progress: 0.9 })],
87
+ ["image", FILE_UPLOAD_MOCK_IMAGE({ progress: 0.9 })],
88
+ ])("when a local %s is being uploaded", (testIdType, file) => {
89
+ const testId = `test-${testIdType}`;
90
+ it("renders ProgressBar state when upload status is not completed", () => {
91
+ const { getByTestId } = renderFormatFile(file);
92
+ expect(getByTestId("format-file-progress-bar")).toBeDefined();
93
+ });
94
+
95
+ it("renders a helpful accessibility label", () => {
96
+ const tree = renderFormatFile(file);
97
+ expect(
98
+ tree.getByLabelText(
99
+ formatFileMessages.inProgressAccessibilityLabel.defaultMessage,
100
+ ),
101
+ ).toBeDefined();
102
+ });
103
+
104
+ it("renders an overlay on the image when upload status is not completed", () => {
105
+ const { getByTestId } = renderFormatFile(file);
106
+ const progressBarContainer = getByTestId(
107
+ "format-file-progress-bar-container",
108
+ );
109
+ const overlayStyles = progressBarContainer.props.style;
110
+
111
+ // The container needs to have a height and color in order to show the overlay
112
+ expect(overlayStyles).toEqual(
113
+ expect.arrayContaining([expect.objectContaining({ height: "100%" })]),
114
+ );
115
+ expect(overlayStyles).toEqual(
116
+ expect.arrayContaining([
117
+ expect.objectContaining({
118
+ backgroundColor: tokens["color-overlay--dimmed"],
119
+ }),
120
+ ]),
121
+ );
122
+ });
123
+
124
+ it("renders ProgressBar state advancing with the upload percentage", () => {
125
+ jest.useFakeTimers();
126
+ const { getByTestId } = renderFormatFile(file);
127
+ jest.advanceTimersByTime(progressBarAnimationTime);
128
+ const formatFileInnerProgressBar = getByTestId(
129
+ "format-file-inner-progress-bar",
130
+ );
131
+ const innerProgressBarWidth = parseInt(
132
+ formatFileInnerProgressBar.props.style.width,
133
+ 10,
134
+ );
135
+ expect(innerProgressBarWidth).toBeGreaterThan(20);
136
+ });
137
+
138
+ it("shows an alert for on tap", () => {
139
+ const { getByTestId } = renderFormatFile(file);
140
+ const spy = jest.spyOn(Alert, "alert");
141
+ fireEvent.press(getByTestId(testId));
142
+ expect(spy).toHaveBeenCalled();
143
+ });
144
+ });
145
+
146
+ describe.each([
147
+ [
148
+ "file",
149
+ {
150
+ ...FILE_UPLOAD_MOCK_FILE({ progress: 0.9, status: StatusCode.Failed }),
151
+ status: StatusCode.Failed,
152
+ },
153
+ ],
154
+ [
155
+ "image",
156
+ {
157
+ ...FILE_UPLOAD_MOCK_IMAGE({ progress: 0.9, status: StatusCode.Failed }),
158
+ status: StatusCode.Failed,
159
+ },
160
+ ],
161
+ ])("when a local %s upload has failed", (testIdType, file) => {
162
+ it("renders an error icon", () => {
163
+ const tree = renderFormatFile(file);
164
+ expect(tree.getByTestId("format-file-error-icon")).toBeDefined();
165
+ });
166
+
167
+ it("renders a helpful accessibility label", () => {
168
+ const tree = renderFormatFile(file);
169
+ expect(
170
+ tree.getByLabelText(
171
+ formatFileMessages.errorAccessibilityLabel.defaultMessage,
172
+ ),
173
+ ).toBeDefined();
174
+ });
175
+
176
+ it("does not render an overlay", () => {
177
+ const tree = renderFormatFile(file);
178
+ expect(
179
+ tree.queryByTestId("format-file-progress-bar-container"),
180
+ ).toBeNull();
181
+ });
182
+
183
+ it("shows an alert for on tap", () => {
184
+ const testId = `test-${testIdType}`;
185
+ const tree = renderFormatFile(file);
186
+ const spy = jest.spyOn(Alert, "alert");
187
+ fireEvent.press(tree.getByTestId(testId));
188
+ expect(spy).toHaveBeenCalled();
189
+ });
190
+ });
191
+
192
+ describe.each([
193
+ [
194
+ "local file",
195
+ "test-file",
196
+ FILE_UPLOAD_MOCK_FILE({ progress: 1, status: StatusCode.Completed }),
197
+ ],
198
+ [
199
+ "local image",
200
+ "test-image",
201
+ FILE_UPLOAD_MOCK_IMAGE({ progress: 1, status: StatusCode.Completed }),
202
+ ],
203
+ ["external file", "test-file", FILE_MOCK_FILE],
204
+ ["external image", "test-image", FILE_MOCK_IMAGE],
205
+ ])(
206
+ "when a uploaded %s is being used",
207
+ (bottomSheetOptionsSuffix, testId, file) => {
208
+ let tree: RenderAPI;
209
+ const { formatMessage } = useIntl();
210
+ const removeLabel = formatMessage(messages.removeButton, {
211
+ bottomSheetOptionsSuffix: bottomSheetOptionsSuffix,
212
+ });
213
+
214
+ beforeEach(() => {
215
+ jest.clearAllMocks();
216
+ jest.useFakeTimers();
217
+ tree = renderFormatFile(
218
+ file,
219
+ bottomSheetOptionsSuffix as BottomSheetOptionsSuffix,
220
+ );
221
+ jest.advanceTimersByTime(progressBarAnimationTime);
222
+ });
223
+
224
+ it("shows a BottomSheet with a remove option when tapped", () => {
225
+ const { getByTestId, getByLabelText } = tree;
226
+ fireEvent.press(getByTestId(testId));
227
+ expect(getByLabelText(removeLabel)).toBeDefined();
228
+ });
229
+
230
+ describe("when the BottomSheet remove option is tapped", () => {
231
+ it("calls the onRemove action", () => {
232
+ const { getByTestId, getByLabelText } = tree;
233
+ fireEvent.press(getByTestId(testId));
234
+ fireEvent.press(getByLabelText(removeLabel));
235
+ expect(onRemove).toHaveBeenCalledTimes(1);
236
+ });
237
+ });
238
+
239
+ it("creates a thumbnail when a media file is used", () => {
240
+ const expectedCalls = testId.includes("image") ? 2 : 0;
241
+ expect(mockCreateThumbnail).toHaveBeenCalledTimes(expectedCalls);
242
+ });
243
+ },
244
+ );
245
+
246
+ describe("when the preview option is tapped", () => {
247
+ const { formatMessage } = useIntl();
248
+
249
+ it("calls onPreview with a valid image", () => {
250
+ const previewLabel = formatMessage(messages.lightBoxPreviewButton, {
251
+ bottomSheetOptionsSuffix: "image",
252
+ });
253
+ const { getByTestId, getByLabelText } = renderFormatFile(
254
+ FILE_UPLOAD_MOCK_IMAGE({ progress: 1, status: StatusCode.Completed }),
255
+ "image",
256
+ );
257
+ fireEvent.press(getByTestId("test-image"));
258
+ fireEvent.press(getByLabelText(previewLabel));
259
+ expect(mockOnPreview).toHaveBeenCalledTimes(1);
260
+ });
261
+
262
+ it("calls onPreview with a valid pdf file", () => {
263
+ const previewLabel = formatMessage(messages.lightBoxPreviewButton, {
264
+ bottomSheetOptionsSuffix: "file",
265
+ });
266
+ const { getByTestId, getByLabelText } = renderFormatFile(
267
+ FILE_UPLOAD_MOCK_PDF({ progress: 1, status: StatusCode.Completed }),
268
+ "file",
269
+ );
270
+ fireEvent.press(getByTestId("test-file"));
271
+ fireEvent.press(getByLabelText(previewLabel));
272
+ expect(mockOnPreview).toHaveBeenCalledTimes(1);
273
+ });
274
+
275
+ it("calls onPreview with a valid external PDF file", () => {
276
+ const previewLabel = formatMessage(messages.lightBoxPreviewButton, {
277
+ bottomSheetOptionsSuffix: "file",
278
+ });
279
+ const { getByTestId, getByLabelText } = renderFormatFile(
280
+ FILE_MOCK_PDF,
281
+ "file",
282
+ );
283
+ fireEvent.press(getByTestId("test-file"));
284
+ fireEvent.press(getByLabelText(previewLabel));
285
+ expect(mockOnPreview).toHaveBeenCalledTimes(1);
286
+ });
287
+
288
+ it("does not show the preview option with an unaccepted file", () => {
289
+ const previewLabel = formatMessage(messages.lightBoxPreviewButton, {
290
+ bottomSheetOptionsSuffix: "file",
291
+ });
292
+ const { getByTestId, queryByLabelText } = renderFormatFile(
293
+ FILE_UPLOAD_MOCK_FILE({ progress: 1, status: StatusCode.Completed }),
294
+ "file",
295
+ );
296
+
297
+ fireEvent.press(getByTestId("test-file"));
298
+ expect(queryByLabelText(previewLabel)).toBeNull();
299
+ });
300
+ });
301
+
302
+ describe("when an uploaded video is being viewed", () => {
303
+ it("shows the play icon if the file type indicator is not specified", () => {
304
+ const { getByTestId } = renderFormatFile(FILE_MOCK_VIDEO, "video");
305
+ expect(getByTestId("video")).toBeDefined();
306
+ });
307
+
308
+ it("does not show the play icon if the file type indicator is set to false", () => {
309
+ const { queryByTestId } = renderFormatFile(
310
+ FILE_MOCK_VIDEO,
311
+ "video",
312
+ false,
313
+ );
314
+ expect(queryByTestId("video")).toBeNull();
315
+ });
316
+ });
317
+ }
318
+
319
+ describe("ios", () => {
320
+ beforeEach(() => {
321
+ Platform.OS = "ios";
322
+ });
323
+
324
+ basicRenderTestWithValue();
325
+ });
326
+
327
+ describe("android", () => {
328
+ beforeEach(() => {
329
+ Platform.OS = "android";
330
+ });
331
+
332
+ basicRenderTestWithValue();
333
+ });
@@ -0,0 +1,300 @@
1
+ import React, { createRef, useCallback, useState } from "react";
2
+ import { TouchableOpacity, View } from "react-native";
3
+ import { useIntl } from "react-intl";
4
+ import { messages } from "./messages";
5
+ import { styles } from "./FormatFile.style";
6
+ import { MediaView } from "./components/MediaView";
7
+ import {
8
+ BottomSheetOptionsSuffix,
9
+ FormatFileBottomSheet,
10
+ } from "./components/FormatFileBottomSheet";
11
+ import { FileView } from "./components/FileView";
12
+ import { acceptedExtensions, videoExtensions } from "./constants";
13
+ import {
14
+ CreateThumbnail,
15
+ File,
16
+ FileUpload,
17
+ FormattedFile,
18
+ StatusCode,
19
+ } from "./types";
20
+ import { AtlantisFormatFileContext } from "./context/FormatFileContext";
21
+ import { createUseCreateThumbnail } from "./utils/createUseCreateThumbnail";
22
+ import { BottomSheetRef } from "../BottomSheet/BottomSheet";
23
+
24
+ export interface FormatFileProps<T> {
25
+ /**
26
+ * File upload details object. Can be a File or a FileUpload
27
+ */
28
+ file: T;
29
+
30
+ /**
31
+ * Accessibility label
32
+ */
33
+ readonly accessibilityLabel?: string;
34
+
35
+ /**
36
+ * Accessibility hint
37
+ */
38
+ readonly accessibilityHint?: string;
39
+ /**
40
+ * A function which handles the onTap event.
41
+ */
42
+ onTap?: (file: T) => void;
43
+ /**
44
+ * A function to be called on "Remove" Bottom Sheet Option press
45
+ */
46
+ onRemove?: () => void;
47
+
48
+ /**
49
+ * Handler for the "Preview" Bottom Sheet Option press
50
+ */
51
+ onPreviewPress?: (formattedFile: FormattedFile) => void;
52
+ /**
53
+ * A file type to show at Bottom Sheet options
54
+ */
55
+ bottomSheetOptionsSuffix?: BottomSheetOptionsSuffix;
56
+
57
+ /**
58
+ * Uses a grid layout when multi-file upload is supported
59
+ */
60
+ styleInGrid?: boolean;
61
+
62
+ /**
63
+ * A reference to the element in the rendered output
64
+ */
65
+ readonly testID?: string;
66
+
67
+ /**
68
+ * Set false to hide the filetype icon
69
+ */
70
+ readonly showFileTypeIndicator?: boolean;
71
+
72
+ readonly createThumbnail?: CreateThumbnail;
73
+ }
74
+
75
+ type FormatFileInternalProps = Omit<
76
+ FormatFileProps<File | FileUpload>,
77
+ "file" | "onTap"
78
+ > & {
79
+ file: FormattedFile;
80
+ onTap: () => void;
81
+ };
82
+
83
+ interface FormatFileContentProps {
84
+ accessibilityLabel?: string;
85
+ file: FormattedFile;
86
+ showOverlay: boolean;
87
+ styleInGrid: boolean;
88
+ onUploadComplete: () => void;
89
+ isMedia: boolean;
90
+ }
91
+
92
+ function FormatFileContent({
93
+ accessibilityLabel,
94
+ file,
95
+ showOverlay,
96
+ styleInGrid,
97
+ onUploadComplete,
98
+ isMedia,
99
+ }: FormatFileContentProps): JSX.Element {
100
+ return (
101
+ <View
102
+ style={[
103
+ styles.thumbnailContainer,
104
+ styleInGrid && styles.thumbnailContainerGrid,
105
+ ]}
106
+ >
107
+ {isMedia ? (
108
+ <MediaView
109
+ accessibilityLabel={accessibilityLabel}
110
+ file={file}
111
+ showOverlay={showOverlay}
112
+ showError={file.error}
113
+ styleInGrid={styleInGrid}
114
+ onUploadComplete={onUploadComplete}
115
+ />
116
+ ) : (
117
+ <FileView
118
+ accessibilityLabel={accessibilityLabel}
119
+ file={file}
120
+ showOverlay={showOverlay}
121
+ showError={file.error}
122
+ styleInGrid={styleInGrid}
123
+ onUploadComplete={onUploadComplete}
124
+ />
125
+ )}
126
+ </View>
127
+ );
128
+ }
129
+
130
+ const FormatFileInternalMemoized = React.memo(FormatFileInternal);
131
+
132
+ function isMediaFile(fileType: string): boolean {
133
+ return fileType.includes("image") || fileType.includes("video");
134
+ }
135
+
136
+ function isVideo(fileName: string): boolean {
137
+ const extension = fileName.substring(fileName.lastIndexOf(".") + 1);
138
+
139
+ return videoExtensions.some(({ type }) => type === extension.toLowerCase());
140
+ }
141
+
142
+ function getContentType(fileName = "", fileType = "unknown"): string {
143
+ if (isVideo(fileName)) {
144
+ return "video";
145
+ }
146
+
147
+ return fileType;
148
+ }
149
+
150
+ function isAcceptedExtension(file: FormattedFile): boolean {
151
+ return acceptedExtensions.some(extension =>
152
+ // type property may return undefined on M1 Systems running iOS Simulator
153
+ (file.type || "").includes(extension.name),
154
+ );
155
+ }
156
+
157
+ function parseFile(
158
+ file: File | FileUpload,
159
+ showFileTypeIndicator: boolean,
160
+ ): FormattedFile {
161
+ let formattedFile: FormattedFile;
162
+
163
+ if ("progress" in file) {
164
+ formattedFile = {
165
+ source: file.sourcePath,
166
+ name: file.name,
167
+ size: file.size,
168
+ external: false,
169
+ progress: file.progress,
170
+ status: file.status,
171
+ error: file.status === StatusCode.Failed,
172
+ type: file.type || file.key,
173
+ isMedia: false,
174
+ showPreview: false,
175
+ showFileTypeIndicator: showFileTypeIndicator,
176
+ };
177
+ } else {
178
+ formattedFile = {
179
+ source: file.url,
180
+ thumbnailUrl: file.thumbnailUrl,
181
+ name: file.fileName,
182
+ size: file.fileSize,
183
+ external: true,
184
+ progress: 1,
185
+ status: StatusCode.Completed,
186
+ error: false,
187
+ type: getContentType(file.fileName, file.contentType),
188
+ isMedia: false,
189
+ showPreview: false,
190
+ showFileTypeIndicator: showFileTypeIndicator,
191
+ };
192
+ }
193
+
194
+ formattedFile.isMedia = isMediaFile(formattedFile.type || "");
195
+ formattedFile.showPreview =
196
+ formattedFile.isMedia || isAcceptedExtension(formattedFile);
197
+
198
+ return formattedFile;
199
+ }
200
+
201
+ export function FormatFile<T extends File | FileUpload>({
202
+ file,
203
+ accessibilityLabel,
204
+ accessibilityHint,
205
+ onTap,
206
+ onRemove,
207
+ bottomSheetOptionsSuffix,
208
+ styleInGrid = false,
209
+ testID,
210
+ showFileTypeIndicator = true,
211
+ createThumbnail,
212
+ onPreviewPress,
213
+ }: FormatFileProps<T>): JSX.Element {
214
+ const onTapModified = onTap ? () => onTap(file) : () => undefined;
215
+
216
+ const formattedFile = parseFile(file, showFileTypeIndicator);
217
+
218
+ return (
219
+ <FormatFileInternalMemoized
220
+ file={formattedFile}
221
+ accessibilityLabel={accessibilityLabel}
222
+ accessibilityHint={accessibilityHint}
223
+ onTap={onTapModified}
224
+ onRemove={onRemove}
225
+ bottomSheetOptionsSuffix={bottomSheetOptionsSuffix}
226
+ styleInGrid={styleInGrid}
227
+ testID={testID}
228
+ createThumbnail={createThumbnail}
229
+ onPreviewPress={onPreviewPress}
230
+ />
231
+ );
232
+ }
233
+
234
+ function FormatFileInternal({
235
+ file,
236
+ accessibilityLabel,
237
+ accessibilityHint,
238
+ onTap,
239
+ onRemove,
240
+ bottomSheetOptionsSuffix,
241
+ styleInGrid = false,
242
+ onPreviewPress,
243
+ testID,
244
+ createThumbnail: createThumbnailProp,
245
+ }: FormatFileInternalProps): JSX.Element {
246
+ const [showOverlay, setShowOverlay] = useState<boolean>(
247
+ file.status !== StatusCode.Completed,
248
+ );
249
+
250
+ const { formatMessage } = useIntl();
251
+ const bottomSheetRef = createRef<BottomSheetRef>();
252
+
253
+ const handlePreviewPress = useCallback(() => {
254
+ onPreviewPress?.(file);
255
+ }, [file, onPreviewPress]);
256
+ const createThumbnail = createThumbnailProp
257
+ ? createThumbnailProp
258
+ : async () => ({ error: false, thumbnail: "" });
259
+ const { useCreateThumbnail } = createUseCreateThumbnail(createThumbnail);
260
+
261
+ return (
262
+ <AtlantisFormatFileContext.Provider value={{ useCreateThumbnail }}>
263
+ <View>
264
+ <TouchableOpacity
265
+ accessibilityRole="imagebutton"
266
+ accessibilityHint={
267
+ accessibilityHint ??
268
+ formatMessage(messages.defaultAccessibilityHint)
269
+ }
270
+ onPress={handleOnPress}
271
+ testID={testID}
272
+ >
273
+ <FormatFileContent
274
+ accessibilityLabel={accessibilityLabel}
275
+ file={file}
276
+ onUploadComplete={() => setShowOverlay(false)}
277
+ isMedia={!!file.isMedia}
278
+ styleInGrid={styleInGrid}
279
+ showOverlay={showOverlay}
280
+ />
281
+ </TouchableOpacity>
282
+ <FormatFileBottomSheet
283
+ bottomSheetRef={bottomSheetRef}
284
+ onRemovePress={onRemove}
285
+ bottomSheetOptionsSuffix={bottomSheetOptionsSuffix}
286
+ onPreviewPress={file.showPreview ? handlePreviewPress : undefined}
287
+ />
288
+ </View>
289
+ </AtlantisFormatFileContext.Provider>
290
+ );
291
+
292
+ function handleOnPress() {
293
+ if (showOverlay || !onRemove) {
294
+ onTap();
295
+ return;
296
+ }
297
+
298
+ bottomSheetRef.current?.open();
299
+ }
300
+ }
@@ -0,0 +1,11 @@
1
+ import { StyleSheet } from "react-native";
2
+ import { tokens } from "../../../utils/design";
3
+
4
+ export const styles = StyleSheet.create({
5
+ circle: {
6
+ width: tokens["space-large"],
7
+ height: tokens["space-large"],
8
+ borderRadius: tokens["radius-circle"],
9
+ backgroundColor: tokens["color-surface"],
10
+ },
11
+ });
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+ import { View } from "react-native";
3
+ import { styles } from "./ErrorIcon.style";
4
+ import { Icon } from "../../../Icon";
5
+
6
+ export function ErrorIcon(): JSX.Element {
7
+ return (
8
+ <View testID="format-file-error-icon" style={styles.circle}>
9
+ <Icon name="alert" color="critical" />
10
+ </View>
11
+ );
12
+ }
@@ -0,0 +1 @@
1
+ export { ErrorIcon } from "./ErrorIcon";