@jobber/components-native 0.101.11 → 0.102.1-prerelease-b56e55d.1
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/docs/FormatFile/FormatFile.md +28 -2
- package/dist/docs/Icon/Icon.md +1 -0
- package/dist/package.json +3 -3
- package/dist/src/FormatFile/FormatFile.js +2 -2
- package/dist/src/FormatFile/FormatFileThumbnail.js +3 -2
- package/dist/src/FormatFile/FormatFileThumbnail.test.js +23 -0
- package/dist/src/FormatFile/components/FileView/FileView.js +6 -23
- package/dist/src/FormatFile/components/FileView/FileView.style.js +5 -0
- package/dist/src/FormatFile/components/MediaView/MediaView.js +40 -6
- package/dist/src/FormatFile/components/MediaView/MediaView.test.js +69 -0
- package/dist/src/FormatFile/constants.js +60 -0
- package/dist/src/FormatFile/index.js +1 -0
- package/dist/src/FormatFile/utils/getFileCategory.js +62 -0
- package/dist/src/FormatFile/utils/getFileCategory.test.js +83 -0
- package/dist/src/FormatFile/utils/index.js +4 -0
- package/dist/src/FormatFile/utils/mapFileTypeToIconName.js +31 -0
- package/dist/src/FormatFile/utils/mapFileTypeToIconName.test.js +41 -0
- package/dist/src/FormatFile/utils/sanitizeFileName.js +9 -0
- package/dist/src/FormatFile/utils/sanitizeFileName.test.js +18 -0
- package/dist/src/FormatFile/utils/truncateFileNameForLine.js +58 -0
- package/dist/src/FormatFile/utils/truncateFileNameForLine.test.js +40 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types/src/FormatFile/FormatFile.d.ts +11 -3
- package/dist/types/src/FormatFile/FormatFileThumbnail.d.ts +10 -2
- package/dist/types/src/FormatFile/components/FileView/FileView.d.ts +2 -1
- package/dist/types/src/FormatFile/components/FileView/FileView.style.d.ts +2 -0
- package/dist/types/src/FormatFile/components/MediaView/MediaView.d.ts +2 -1
- package/dist/types/src/FormatFile/constants.d.ts +29 -0
- package/dist/types/src/FormatFile/index.d.ts +2 -0
- package/dist/types/src/FormatFile/utils/getFileCategory.d.ts +28 -0
- package/dist/types/src/FormatFile/utils/getFileCategory.test.d.ts +1 -0
- package/dist/types/src/FormatFile/utils/index.d.ts +5 -0
- package/dist/types/src/FormatFile/utils/mapFileTypeToIconName.d.ts +18 -0
- package/dist/types/src/FormatFile/utils/mapFileTypeToIconName.test.d.ts +1 -0
- package/dist/types/src/FormatFile/utils/sanitizeFileName.d.ts +5 -0
- package/dist/types/src/FormatFile/utils/sanitizeFileName.test.d.ts +1 -0
- package/dist/types/src/FormatFile/utils/truncateFileNameForLine.d.ts +24 -0
- package/dist/types/src/FormatFile/utils/truncateFileNameForLine.test.d.ts +1 -0
- package/package.json +3 -3
- package/src/FormatFile/FormatFile.stories.tsx +41 -1
- package/src/FormatFile/FormatFile.tsx +14 -2
- package/src/FormatFile/FormatFileThumbnail.test.tsx +44 -0
- package/src/FormatFile/FormatFileThumbnail.tsx +13 -1
- package/src/FormatFile/components/FileView/FileView.style.ts +5 -0
- package/src/FormatFile/components/FileView/FileView.tsx +12 -29
- package/src/FormatFile/components/MediaView/MediaView.test.tsx +159 -2
- package/src/FormatFile/components/MediaView/MediaView.tsx +114 -5
- package/src/FormatFile/constants.ts +64 -0
- package/src/FormatFile/index.ts +7 -0
- package/src/FormatFile/utils/getFileCategory.test.ts +96 -0
- package/src/FormatFile/utils/getFileCategory.ts +106 -0
- package/src/FormatFile/utils/index.ts +5 -0
- package/src/FormatFile/utils/mapFileTypeToIconName.test.ts +43 -0
- package/src/FormatFile/utils/mapFileTypeToIconName.ts +42 -0
- package/src/FormatFile/utils/sanitizeFileName.test.ts +25 -0
- package/src/FormatFile/utils/sanitizeFileName.ts +9 -0
- package/src/FormatFile/utils/truncateFileNameForLine.test.ts +61 -0
- package/src/FormatFile/utils/truncateFileNameForLine.ts +68 -0
- package/src/ThumbnailList/__snapshots__/ThumbnailList.test.tsx.snap +1 -0
|
@@ -22,6 +22,31 @@ image, while expanded is used to display a file alongside its metadata.
|
|
|
22
22
|
* For a thumbnail representation of a user, use [Avatar](/components/Avatar).
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
## Developer notes
|
|
26
|
+
|
|
27
|
+
`FormatFileThumbnail` distinguishes between a real video thumbnail and a video
|
|
28
|
+
file that has no preview image available.
|
|
29
|
+
|
|
30
|
+
When a video has a valid `thumbnailUrl` or decodable source, Atlantis renders
|
|
31
|
+
the thumbnail and overlays the existing small `video` icon. When no preview is
|
|
32
|
+
available, Atlantis renders a fallback placeholder and shows the `videoFile`
|
|
33
|
+
icon by default instead of showing the OS broken-image glyph.
|
|
34
|
+
|
|
35
|
+
`showFileTypeIndicator={false}` hides all file type iconography, including the
|
|
36
|
+
large `videoFile` icon used by video placeholders.
|
|
37
|
+
|
|
38
|
+
Use `surfaceColor` on `FormatFileThumbnail` when the tile needs to visually
|
|
39
|
+
blend into surrounding chrome:
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
<FormatFileThumbnail
|
|
43
|
+
file={file}
|
|
44
|
+
size={{ width: 96, height: 96 }}
|
|
45
|
+
surfaceColor="var(--color-surface)"
|
|
46
|
+
/>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
|
|
25
50
|
## Props
|
|
26
51
|
|
|
27
52
|
### Mobile
|
|
@@ -38,7 +63,7 @@ image, while expanded is used to display a file alongside its metadata.
|
|
|
38
63
|
| `onPreviewPress` | `(formattedFile: FormattedFile) => void` | No | — | Handler for the "Preview" Bottom Sheet Option press |
|
|
39
64
|
| `onRemove` | `() => void` | No | — | A function to be called on "Remove" Bottom Sheet Option press |
|
|
40
65
|
| `onTap` | `(file: T) => void` | No | — | A function which handles the onTap event. |
|
|
41
|
-
| `showFileTypeIndicator` | `boolean` | No | `true` | Set false to hide the
|
|
66
|
+
| `showFileTypeIndicator` | `boolean` | No | `true` | Set false to hide all file type iconography, including the video fallback placeholder icon. |
|
|
42
67
|
| `styleInGrid` | `boolean` | No | `false` | Uses a grid layout when multi-file upload is supported |
|
|
43
68
|
| `testID` | `string` | No | — | A reference to the element in the rendered output |
|
|
44
69
|
|
|
@@ -52,5 +77,6 @@ image, while expanded is used to display a file alongside its metadata.
|
|
|
52
77
|
| `showOverlay` | `boolean` | Yes | — | |
|
|
53
78
|
| `styleInGrid` | `boolean` | Yes | — | |
|
|
54
79
|
| `accessibilityLabel` | `string` | No | — | |
|
|
55
|
-
| `onMediaLoadEnd` | `() => void` | No | — |
|
|
80
|
+
| `onMediaLoadEnd` | `() => void` | No | — | @internal A function to be called when the media has loaded. This is only used in FormatFileThumbnail. |
|
|
56
81
|
| `skipContainerStyles` | `boolean` | No | `false` | @internal When true, the component skips its container wrapper entirely (no border, background, or dimension styles).... |
|
|
82
|
+
| `surfaceColor` | `string` | No | — | Optional override for the tile's surface colour, forwarded to the underlying `FileView` or `MediaView`. Only meaningf... |
|
package/dist/docs/Icon/Icon.md
CHANGED
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobber/components-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.102.1-prerelease-b56e55d.1+b56e55d2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React Native implementation of Atlantis",
|
|
6
6
|
"repository": {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@babel/runtime": "^7.29.2",
|
|
60
60
|
"@gorhom/bottom-sheet": "^5.2.8",
|
|
61
|
-
"@jobber/design": "0.
|
|
61
|
+
"@jobber/design": "0.101.0",
|
|
62
62
|
"@jobber/hooks": "2.20.1",
|
|
63
63
|
"@react-native-community/datetimepicker": "^8.4.5",
|
|
64
64
|
"@react-native/babel-preset": "^0.82.1",
|
|
@@ -109,5 +109,5 @@
|
|
|
109
109
|
"react-native-screens": ">=4.18.0",
|
|
110
110
|
"react-native-svg": ">=12.0.0"
|
|
111
111
|
},
|
|
112
|
-
"gitHead": "
|
|
112
|
+
"gitHead": "b56e55d265ff4465d85cd3449a2e99a900e2e837"
|
|
113
113
|
}
|
|
@@ -18,9 +18,9 @@ import { AtlantisFormatFileContext } from "./context/FormatFileContext";
|
|
|
18
18
|
import { createUseCreateThumbnail } from "./utils/createUseCreateThumbnail";
|
|
19
19
|
import { parseFile } from "./utils/parseFile";
|
|
20
20
|
import { useAtlantisI18n } from "../hooks/useAtlantisI18n";
|
|
21
|
-
export function FormatFileContent({ accessibilityLabel, file, showOverlay, styleInGrid, onUploadComplete, isMedia, skipContainerStyles = false, onMediaLoadEnd, }) {
|
|
21
|
+
export function FormatFileContent({ accessibilityLabel, file, showOverlay, styleInGrid, onUploadComplete, isMedia, skipContainerStyles = false, onMediaLoadEnd, surfaceColor, }) {
|
|
22
22
|
const styles = useStyles();
|
|
23
|
-
const content = isMedia ? (React.createElement(MediaView, { accessibilityLabel: accessibilityLabel, file: file, showOverlay: showOverlay, showError: file.error, styleInGrid: styleInGrid, onUploadComplete: onUploadComplete, onLoadEnd: onMediaLoadEnd })) : (React.createElement(FileView, { accessibilityLabel: accessibilityLabel, file: file, showOverlay: showOverlay, showError: file.error, styleInGrid: styleInGrid, onUploadComplete: onUploadComplete }));
|
|
23
|
+
const content = isMedia ? (React.createElement(MediaView, { accessibilityLabel: accessibilityLabel, file: file, showOverlay: showOverlay, showError: file.error, styleInGrid: styleInGrid, onUploadComplete: onUploadComplete, onLoadEnd: onMediaLoadEnd, surfaceColor: surfaceColor })) : (React.createElement(FileView, { accessibilityLabel: accessibilityLabel, file: file, showOverlay: showOverlay, showError: file.error, styleInGrid: styleInGrid, onUploadComplete: onUploadComplete, surfaceColor: surfaceColor }));
|
|
24
24
|
if (skipContainerStyles) {
|
|
25
25
|
return content;
|
|
26
26
|
}
|
|
@@ -26,7 +26,7 @@ import { parseFile } from "./utils/parseFile";
|
|
|
26
26
|
* where you need a single shared BottomSheet rather than one per item, and
|
|
27
27
|
* where the consumer controls the item dimensions via the `size` prop.
|
|
28
28
|
*/
|
|
29
|
-
export function FormatFileThumbnail({ file, accessibilityLabel, showFileTypeIndicator = true, createThumbnail: createThumbnailProp, size, testID, onMediaLoadEnd, }) {
|
|
29
|
+
export function FormatFileThumbnail({ file, accessibilityLabel, showFileTypeIndicator = true, createThumbnail: createThumbnailProp, size, testID, onMediaLoadEnd, surfaceColor, }) {
|
|
30
30
|
const formattedFile = useMemo(() => parseFile(file, showFileTypeIndicator), [file, showFileTypeIndicator]);
|
|
31
31
|
const styles = useStyles();
|
|
32
32
|
const [showOverlay, setShowOverlay] = useState(formattedFile.status !== StatusCode.Completed);
|
|
@@ -41,6 +41,7 @@ export function FormatFileThumbnail({ file, accessibilityLabel, showFileTypeIndi
|
|
|
41
41
|
// that belongs to the consumer, not the thumbnail itself.
|
|
42
42
|
{ marginBottom: 0 },
|
|
43
43
|
size && { width: size.width, height: size.height },
|
|
44
|
+
surfaceColor ? { backgroundColor: surfaceColor } : undefined,
|
|
44
45
|
], testID: testID },
|
|
45
|
-
React.createElement(FormatFileContent, { accessibilityLabel: accessibilityLabel, file: formattedFile, onUploadComplete: () => setShowOverlay(false), isMedia: !!formattedFile.isMedia, styleInGrid: true, showOverlay: showOverlay, skipContainerStyles: true, onMediaLoadEnd: onMediaLoadEnd }))));
|
|
46
|
+
React.createElement(FormatFileContent, { accessibilityLabel: accessibilityLabel, file: formattedFile, onUploadComplete: () => setShowOverlay(false), isMedia: !!formattedFile.isMedia, styleInGrid: true, showOverlay: showOverlay, skipContainerStyles: true, onMediaLoadEnd: onMediaLoadEnd, surfaceColor: surfaceColor }))));
|
|
46
47
|
}
|
|
@@ -190,3 +190,26 @@ describe("FormatFileThumbnail", () => {
|
|
|
190
190
|
function renderThumbnail(file) {
|
|
191
191
|
return render(React.createElement(FormatFileThumbnail, { file: file, accessibilityLabel: "Custom Label", createThumbnail: mockCreateThumbnail, size: { width: 100, height: 100 } }));
|
|
192
192
|
}
|
|
193
|
+
describe("FormatFileThumbnail surfaceColor", () => {
|
|
194
|
+
it("applies the provided surfaceColor to the outer container", () => {
|
|
195
|
+
const { getByTestId } = render(React.createElement(FormatFileThumbnail, { file: FILE_MOCK_FILE, accessibilityLabel: "Custom Label", createThumbnail: mockCreateThumbnail, size: { width: 100, height: 100 }, surfaceColor: "#F4ECD9", testID: "thumbnail-container" }));
|
|
196
|
+
const container = getByTestId("thumbnail-container");
|
|
197
|
+
const styles = Array.isArray(container.props.style)
|
|
198
|
+
? container.props.style.filter(Boolean)
|
|
199
|
+
: [container.props.style];
|
|
200
|
+
const merged = Object.assign({}, ...styles);
|
|
201
|
+
expect(merged.backgroundColor).toBe("#F4ECD9");
|
|
202
|
+
});
|
|
203
|
+
it("preserves the themed background when surfaceColor is omitted", () => {
|
|
204
|
+
const { getByTestId } = render(React.createElement(FormatFileThumbnail, { file: FILE_MOCK_FILE, accessibilityLabel: "Custom Label", createThumbnail: mockCreateThumbnail, size: { width: 100, height: 100 }, testID: "thumbnail-container" }));
|
|
205
|
+
const container = getByTestId("thumbnail-container");
|
|
206
|
+
const styles = Array.isArray(container.props.style)
|
|
207
|
+
? container.props.style.filter(Boolean)
|
|
208
|
+
: [container.props.style];
|
|
209
|
+
const merged = Object.assign({}, ...styles);
|
|
210
|
+
// The default themed background token resolves to the surface
|
|
211
|
+
// colour, not the consumer-supplied #F4ECD9.
|
|
212
|
+
expect(merged.backgroundColor).not.toBe("#F4ECD9");
|
|
213
|
+
expect(merged.backgroundColor).toBeTruthy();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -4,11 +4,11 @@ import { useStyles } from "./FileView.style";
|
|
|
4
4
|
import { Icon } from "../../../Icon";
|
|
5
5
|
import { Text } from "../../../Text";
|
|
6
6
|
import { StatusCode } from "../../types";
|
|
7
|
-
import { computeA11yLabel } from "../../utils";
|
|
7
|
+
import { computeA11yLabel, mapFileTypeToIconName, sanitizeFileName, truncateFileNameForLine, } from "../../utils";
|
|
8
8
|
import { ProgressBar } from "../ProgressBar";
|
|
9
9
|
import { ErrorIcon } from "../ErrorIcon";
|
|
10
10
|
import { useAtlantisI18n } from "../../../hooks/useAtlantisI18n";
|
|
11
|
-
export function FileView({ accessibilityLabel, styleInGrid, file, showOverlay, showError, onUploadComplete, }) {
|
|
11
|
+
export function FileView({ accessibilityLabel, styleInGrid, file, showOverlay, showError, onUploadComplete, surfaceColor, }) {
|
|
12
12
|
const { t } = useAtlantisI18n();
|
|
13
13
|
const styles = useStyles();
|
|
14
14
|
const a11yLabel = computeA11yLabel({
|
|
@@ -21,6 +21,7 @@ export function FileView({ accessibilityLabel, styleInGrid, file, showOverlay, s
|
|
|
21
21
|
return (React.createElement(View, { style: [
|
|
22
22
|
styles.fileBackground,
|
|
23
23
|
styleInGrid ? styles.fileBackgroundGrid : styles.fileBackgroundFlat,
|
|
24
|
+
surfaceColor ? { backgroundColor: surfaceColor } : undefined,
|
|
24
25
|
], accessible: true, accessibilityLabel: a11yLabel },
|
|
25
26
|
React.createElement(View, { style: [
|
|
26
27
|
styles.fileBackground,
|
|
@@ -33,7 +34,9 @@ export function FileView({ accessibilityLabel, styleInGrid, file, showOverlay, s
|
|
|
33
34
|
showError && styleInGrid ? styles.fileNameError : styles.fileName,
|
|
34
35
|
styleInGrid ? styles.fileNameGrid : styles.fileNameFlat,
|
|
35
36
|
] },
|
|
36
|
-
React.createElement(Text, { level: "textSupporting", variation: "subdued", maxLines: "single" },
|
|
37
|
+
React.createElement(Text, { level: "textSupporting", variation: "subdued", maxLines: "single" }, styleInGrid
|
|
38
|
+
? truncateFileNameForLine(sanitizeFileName(file.name))
|
|
39
|
+
: sanitizeFileName(file.name))),
|
|
37
40
|
!showError && showOverlay && (React.createElement(View, { style: [styles.fileBackground, styles.overlay, styles.fileOverlay], testID: "format-file-progress-bar-container" },
|
|
38
41
|
React.createElement(ProgressBar, { status: file.status, progress: freezeProgressBar ? 0.9 : file.progress, onComplete: onUploadComplete }))),
|
|
39
42
|
showError && (React.createElement(View, { style: [
|
|
@@ -44,23 +47,3 @@ export function FileView({ accessibilityLabel, styleInGrid, file, showOverlay, s
|
|
|
44
47
|
React.createElement(View, { style: !styleInGrid ? styles.iconCenter : undefined },
|
|
45
48
|
React.createElement(ErrorIcon, null))))));
|
|
46
49
|
}
|
|
47
|
-
function mapFileTypeToIconName({ fileName, fileType, }) {
|
|
48
|
-
if (!fileName && !fileType) {
|
|
49
|
-
return "alert";
|
|
50
|
-
}
|
|
51
|
-
if ((fileType === null || fileType === void 0 ? void 0 : fileType.includes("pdf")) || (fileName === null || fileName === void 0 ? void 0 : fileName.match(/\.pdf$/i))) {
|
|
52
|
-
return "pdf";
|
|
53
|
-
}
|
|
54
|
-
else if ((fileType === null || fileType === void 0 ? void 0 : fileType.includes("ms-word")) || (fileName === null || fileName === void 0 ? void 0 : fileName.match(/\.docx?$/i))) {
|
|
55
|
-
return "word";
|
|
56
|
-
}
|
|
57
|
-
else if ((fileType === null || fileType === void 0 ? void 0 : fileType.includes("ms-excel")) || (fileName === null || fileName === void 0 ? void 0 : fileName.match(/\.xlsx?$/i))) {
|
|
58
|
-
return "excel";
|
|
59
|
-
}
|
|
60
|
-
else if ((fileType === null || fileType === void 0 ? void 0 : fileType.includes("video")) || (fileName === null || fileName === void 0 ? void 0 : fileName.match(/\.mp4$/i))) {
|
|
61
|
-
return "video";
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
return "file";
|
|
65
|
-
}
|
|
66
|
-
}
|
|
@@ -36,12 +36,17 @@ export const useStyles = buildThemedStyles(tokens => {
|
|
|
36
36
|
borderTopWidth: tokens["space-minuscule"],
|
|
37
37
|
width: "100%",
|
|
38
38
|
marginTop: tokens["space-base"],
|
|
39
|
+
overflow: "hidden",
|
|
39
40
|
},
|
|
40
41
|
fileName: {
|
|
41
42
|
alignItems: "center",
|
|
42
43
|
borderTopColor: tokens["color-border"],
|
|
43
44
|
borderTopWidth: tokens["space-minuscule"],
|
|
44
45
|
width: "100%",
|
|
46
|
+
// Belt-and-suspenders with `numberOfLines={1}`: on Android the latter
|
|
47
|
+
// does not always enforce single-line layout for URL-shaped strings,
|
|
48
|
+
// so clip any unexpected overflow at the rounded tile border.
|
|
49
|
+
overflow: "hidden",
|
|
45
50
|
},
|
|
46
51
|
fileNameGrid: {
|
|
47
52
|
position: "absolute",
|
|
@@ -9,17 +9,20 @@ import { ProgressBar } from "../ProgressBar";
|
|
|
9
9
|
import { ErrorIcon } from "../ErrorIcon";
|
|
10
10
|
import { useAtlantisFormatFileContext } from "../../context/FormatFileContext";
|
|
11
11
|
import { useAtlantisI18n } from "../../../hooks/useAtlantisI18n";
|
|
12
|
-
|
|
12
|
+
// eslint-disable-next-line max-statements
|
|
13
|
+
export function MediaView({ accessibilityLabel, showOverlay, showError, file, styleInGrid, onUploadComplete, onLoadEnd, surfaceColor, }) {
|
|
13
14
|
const { t } = useAtlantisI18n();
|
|
14
15
|
const { useCreateThumbnail } = useAtlantisFormatFileContext();
|
|
15
16
|
const { thumbnail, error } = useCreateThumbnail(file);
|
|
16
17
|
const [isLoading, setIsLoading] = useState(false);
|
|
18
|
+
const [decodeFailed, setDecodeFailed] = useState(false);
|
|
17
19
|
/**
|
|
18
20
|
* Tracks whether onLoadEnd has fired to prevent race conditions.
|
|
19
21
|
* ImageBackground can fire onLoadEnd before onLoadStart when loading cached images,
|
|
20
22
|
* which would cause isLoading to get stuck at true, showing an infinite spinner.
|
|
21
23
|
*/
|
|
22
24
|
const hasLoadedRef = useRef(false);
|
|
25
|
+
const hasNotifiedLoadEndRef = useRef(false);
|
|
23
26
|
const a11yLabel = computeA11yLabel({
|
|
24
27
|
accessibilityLabel,
|
|
25
28
|
showOverlay,
|
|
@@ -36,17 +39,29 @@ export function MediaView({ accessibilityLabel, showOverlay, showError, file, st
|
|
|
36
39
|
hasLoadedRef.current = true;
|
|
37
40
|
setIsLoading(false);
|
|
38
41
|
};
|
|
42
|
+
const handleMediaLoadEnd = () => {
|
|
43
|
+
handleLoadEnd();
|
|
44
|
+
if (!hasNotifiedLoadEndRef.current) {
|
|
45
|
+
hasNotifiedLoadEndRef.current = true;
|
|
46
|
+
onLoadEnd === null || onLoadEnd === void 0 ? void 0 : onLoadEnd();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
39
49
|
useEffect(() => {
|
|
40
50
|
hasLoadedRef.current = false;
|
|
51
|
+
hasNotifiedLoadEndRef.current = false;
|
|
41
52
|
setIsLoading(false);
|
|
53
|
+
setDecodeFailed(false);
|
|
42
54
|
}, [uri]);
|
|
43
|
-
|
|
55
|
+
if (isVideo(file.type) && (!uri || decodeFailed)) {
|
|
56
|
+
return (React.createElement(VideoPlaceholder, { a11yLabel: a11yLabel, styleInGrid: styleInGrid, surfaceColor: surfaceColor, styles: styles, isLoading: isLoading, showOverlay: showOverlay, hasError: hasError, file: file, onUploadComplete: onUploadComplete }));
|
|
57
|
+
}
|
|
58
|
+
return (React.createElement(View, { accessible: true, accessibilityLabel: a11yLabel, style: surfaceColor ? { backgroundColor: surfaceColor } : undefined },
|
|
44
59
|
React.createElement(ImageBackground, { style: [
|
|
45
60
|
styles.imageBackground,
|
|
46
61
|
styleInGrid ? styles.imageBackgroundGrid : styles.imageBackgroundFlat,
|
|
47
|
-
], resizeMode: styleInGrid ? "cover" : "contain", source: { uri }, testID: "test-image", onLoadStart: handleLoadStart, onLoadEnd: () => {
|
|
48
|
-
|
|
49
|
-
|
|
62
|
+
], resizeMode: styleInGrid ? "cover" : "contain", source: { uri }, testID: "test-image", onLoadStart: handleLoadStart, onLoadEnd: handleMediaLoadEnd, onError: () => {
|
|
63
|
+
handleMediaLoadEnd();
|
|
64
|
+
setDecodeFailed(true);
|
|
50
65
|
} },
|
|
51
66
|
React.createElement(Overlay, { isLoading: isLoading, showOverlay: showOverlay, hasError: hasError, file: file, onUploadComplete: onUploadComplete, styles: styles }))));
|
|
52
67
|
}
|
|
@@ -61,7 +76,7 @@ function Overlay({ isLoading, showOverlay, hasError, file, onUploadComplete, sty
|
|
|
61
76
|
if (isVideo(file.type) && file.showFileTypeIndicator) {
|
|
62
77
|
return React.createElement(Icon, { name: "video", color: "white" });
|
|
63
78
|
}
|
|
64
|
-
return
|
|
79
|
+
return null;
|
|
65
80
|
}
|
|
66
81
|
function ProgressOverlay({ status, progress, onUploadComplete, styles, }) {
|
|
67
82
|
const freezeProgressBar = status !== StatusCode.Completed && progress >= 0.9;
|
|
@@ -75,3 +90,22 @@ function ErrorOverlay({ styles, }) {
|
|
|
75
90
|
function isVideo(fileType = "") {
|
|
76
91
|
return fileType.includes("video");
|
|
77
92
|
}
|
|
93
|
+
function VideoPlaceholder({ a11yLabel, styleInGrid, surfaceColor, styles, isLoading, showOverlay, hasError, file, onUploadComplete, }) {
|
|
94
|
+
return (React.createElement(View, { accessible: true, accessibilityLabel: a11yLabel, style: [
|
|
95
|
+
styles.imageBackground,
|
|
96
|
+
styleInGrid ? styles.imageBackgroundGrid : styles.imageBackgroundFlat,
|
|
97
|
+
surfaceColor ? { backgroundColor: surfaceColor } : undefined,
|
|
98
|
+
], testID: "format-file-video-placeholder" },
|
|
99
|
+
file.showFileTypeIndicator && React.createElement(Icon, { name: "videoFile", size: "large" }),
|
|
100
|
+
React.createElement(PlaceholderStatusOverlay, { isLoading: isLoading, showOverlay: showOverlay, hasError: hasError, file: file, onUploadComplete: onUploadComplete, styles: styles })));
|
|
101
|
+
}
|
|
102
|
+
function PlaceholderStatusOverlay({ isLoading, showOverlay, hasError, file, onUploadComplete, styles, }) {
|
|
103
|
+
if (isLoading)
|
|
104
|
+
return React.createElement(ActivityIndicator, null);
|
|
105
|
+
if (hasError)
|
|
106
|
+
return React.createElement(ErrorOverlay, { styles: styles });
|
|
107
|
+
if (showOverlay) {
|
|
108
|
+
return (React.createElement(ProgressOverlay, { status: file.status, progress: file.progress, onUploadComplete: onUploadComplete, styles: styles }));
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
@@ -199,4 +199,73 @@ describe("MediaView", () => {
|
|
|
199
199
|
});
|
|
200
200
|
}));
|
|
201
201
|
});
|
|
202
|
+
describe("Video placeholder", () => {
|
|
203
|
+
const videoFile = Object.assign(Object.assign({}, mockFile), { type: "video/mp4", source: undefined, thumbnailUrl: undefined, name: "clip.mp4" });
|
|
204
|
+
it("renders the videoFile placeholder when a video has no URI", () => {
|
|
205
|
+
const { getByTestId, queryByTestId } = renderWithContext(Object.assign(Object.assign({}, defaultProps), { file: videoFile }));
|
|
206
|
+
expect(getByTestId("format-file-video-placeholder")).toBeTruthy();
|
|
207
|
+
expect(queryByTestId("test-image")).toBeNull();
|
|
208
|
+
});
|
|
209
|
+
it("falls back to the placeholder when the ImageBackground errors", () => {
|
|
210
|
+
const fileWithSource = Object.assign(Object.assign({}, videoFile), { source: "https://example.com/broken-video.mp4" });
|
|
211
|
+
const { getByTestId, queryByTestId } = renderWithContext(Object.assign(Object.assign({}, defaultProps), { file: fileWithSource }));
|
|
212
|
+
const image = getByTestId("test-image");
|
|
213
|
+
expect(queryByTestId("format-file-video-placeholder")).toBeNull();
|
|
214
|
+
fireEvent(image, "error");
|
|
215
|
+
expect(getByTestId("format-file-video-placeholder")).toBeTruthy();
|
|
216
|
+
});
|
|
217
|
+
it("clears the loading spinner and fires onLoadEnd when the decoder errors", () => {
|
|
218
|
+
const onLoadEnd = jest.fn();
|
|
219
|
+
const fileWithSource = Object.assign(Object.assign({}, videoFile), { source: "https://example.com/broken-video.mp4" });
|
|
220
|
+
const { getByTestId, queryByTestId } = renderWithContext(Object.assign(Object.assign({}, defaultProps), { file: fileWithSource, onLoadEnd }));
|
|
221
|
+
const image = getByTestId("test-image");
|
|
222
|
+
fireEvent(image, "loadStart");
|
|
223
|
+
expect(queryByTestId("ActivityIndicator")).toBeTruthy();
|
|
224
|
+
fireEvent(image, "error");
|
|
225
|
+
expect(onLoadEnd).toHaveBeenCalledTimes(1);
|
|
226
|
+
expect(queryByTestId("ActivityIndicator")).toBeNull();
|
|
227
|
+
expect(getByTestId("format-file-video-placeholder")).toBeTruthy();
|
|
228
|
+
});
|
|
229
|
+
it("only fires onLoadEnd once when an error is followed by loadEnd", () => {
|
|
230
|
+
const onLoadEnd = jest.fn();
|
|
231
|
+
const fileWithSource = Object.assign(Object.assign({}, videoFile), { source: "https://example.com/broken-video.mp4" });
|
|
232
|
+
const { getByTestId } = renderWithContext(Object.assign(Object.assign({}, defaultProps), { file: fileWithSource, onLoadEnd }));
|
|
233
|
+
const image = getByTestId("test-image");
|
|
234
|
+
fireEvent(image, "error");
|
|
235
|
+
fireEvent(image, "loadEnd");
|
|
236
|
+
expect(onLoadEnd).toHaveBeenCalledTimes(1);
|
|
237
|
+
});
|
|
238
|
+
it("does not render the placeholder for non-video files with no URI", () => {
|
|
239
|
+
const imageFileWithoutUri = Object.assign(Object.assign({}, mockFile), { source: undefined, thumbnailUrl: undefined });
|
|
240
|
+
const { queryByTestId } = renderWithContext(Object.assign(Object.assign({}, defaultProps), { file: imageFileWithoutUri }));
|
|
241
|
+
expect(queryByTestId("format-file-video-placeholder")).toBeNull();
|
|
242
|
+
expect(queryByTestId("test-image")).toBeTruthy();
|
|
243
|
+
});
|
|
244
|
+
it("renders the progress overlay on top of the placeholder during upload", () => {
|
|
245
|
+
const uploadingVideo = Object.assign(Object.assign({}, videoFile), { status: StatusCode.InProgress, progress: 0.4 });
|
|
246
|
+
const { getByTestId } = renderWithContext(Object.assign(Object.assign({}, defaultProps), { file: uploadingVideo, showOverlay: true }));
|
|
247
|
+
expect(getByTestId("format-file-video-placeholder")).toBeTruthy();
|
|
248
|
+
expect(getByTestId("format-file-progress-bar-container")).toBeTruthy();
|
|
249
|
+
});
|
|
250
|
+
it("renders the error overlay on top of the placeholder when the upload fails", () => {
|
|
251
|
+
const failedVideo = Object.assign(Object.assign({}, videoFile), { status: StatusCode.Failed, error: true });
|
|
252
|
+
const { getByTestId } = renderWithContext(Object.assign(Object.assign({}, defaultProps), { file: failedVideo, showError: true }));
|
|
253
|
+
expect(getByTestId("format-file-video-placeholder")).toBeTruthy();
|
|
254
|
+
expect(getByTestId("format-file-error-container")).toBeTruthy();
|
|
255
|
+
});
|
|
256
|
+
it("hides the videoFile placeholder icon when showFileTypeIndicator is false", () => {
|
|
257
|
+
const { getByTestId, queryByTestId } = renderWithContext(Object.assign(Object.assign({}, defaultProps), { file: Object.assign(Object.assign({}, videoFile), { showFileTypeIndicator: false }) }));
|
|
258
|
+
expect(getByTestId("format-file-video-placeholder")).toBeTruthy();
|
|
259
|
+
expect(queryByTestId("videoFile")).toBeNull();
|
|
260
|
+
});
|
|
261
|
+
it("paints the placeholder with surfaceColor when provided", () => {
|
|
262
|
+
const { getByTestId } = renderWithContext(Object.assign(Object.assign({}, defaultProps), { file: videoFile, surfaceColor: "#F4ECD9" }));
|
|
263
|
+
const placeholder = getByTestId("format-file-video-placeholder");
|
|
264
|
+
const styleArray = Array.isArray(placeholder.props.style)
|
|
265
|
+
? placeholder.props.style.filter(Boolean)
|
|
266
|
+
: [placeholder.props.style];
|
|
267
|
+
const merged = Object.assign({}, ...styleArray);
|
|
268
|
+
expect(merged.backgroundColor).toBe("#F4ECD9");
|
|
269
|
+
});
|
|
270
|
+
});
|
|
202
271
|
});
|
|
@@ -9,6 +9,66 @@ export const acceptedExtensions = [
|
|
|
9
9
|
export const videoExtensions = [
|
|
10
10
|
{ type: "mov" },
|
|
11
11
|
{ type: "mp4" },
|
|
12
|
+
{ type: "m4v" },
|
|
13
|
+
{ type: "webm" },
|
|
14
|
+
{ type: "mkv" },
|
|
15
|
+
{ type: "avi" },
|
|
16
|
+
{ type: "wmv" },
|
|
17
|
+
{ type: "flv" },
|
|
18
|
+
{ type: "mpg" },
|
|
19
|
+
{ type: "mpeg" },
|
|
20
|
+
{ type: "3gp" },
|
|
12
21
|
{ type: "3gpp" },
|
|
13
22
|
{ type: "hevc" },
|
|
14
23
|
];
|
|
24
|
+
/**
|
|
25
|
+
* Extensions that should be treated as documents (Word-family + OpenDocument
|
|
26
|
+
* Text + RTF + Apple Pages + WordPerfect). Used to route to the document
|
|
27
|
+
* icon and the "document" file category.
|
|
28
|
+
*/
|
|
29
|
+
export const documentExtensions = [
|
|
30
|
+
{ type: "doc" },
|
|
31
|
+
{ type: "docx" },
|
|
32
|
+
{ type: "odt" },
|
|
33
|
+
{ type: "fodt" },
|
|
34
|
+
{ type: "rtf" },
|
|
35
|
+
{ type: "pages" },
|
|
36
|
+
{ type: "wpd" },
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* Extensions that should be treated as spreadsheets (Excel-family +
|
|
40
|
+
* OpenDocument Spreadsheet + Apple Numbers + CSV). Used to route to the
|
|
41
|
+
* spreadsheet icon and the "spreadsheet" file category.
|
|
42
|
+
*/
|
|
43
|
+
export const spreadsheetExtensions = [
|
|
44
|
+
{ type: "xls" },
|
|
45
|
+
{ type: "xlsx" },
|
|
46
|
+
{ type: "ods" },
|
|
47
|
+
{ type: "fods" },
|
|
48
|
+
{ type: "numbers" },
|
|
49
|
+
{ type: "csv" },
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* Substring patterns matched against the `Content-Type` MIME string for
|
|
53
|
+
* documents. Substring rather than exact match because real-world MIMEs
|
|
54
|
+
* vary widely (e.g. `application/msword`,
|
|
55
|
+
* `application/vnd.openxmlformats-officedocument.wordprocessingml.document`,
|
|
56
|
+
* `application/vnd.oasis.opendocument.text`).
|
|
57
|
+
*/
|
|
58
|
+
export const documentMimePatterns = [
|
|
59
|
+
"msword",
|
|
60
|
+
"ms-word",
|
|
61
|
+
"wordprocessingml",
|
|
62
|
+
"opendocument.text",
|
|
63
|
+
"rtf",
|
|
64
|
+
];
|
|
65
|
+
/**
|
|
66
|
+
* Substring patterns matched against the `Content-Type` MIME string for
|
|
67
|
+
* spreadsheets.
|
|
68
|
+
*/
|
|
69
|
+
export const spreadsheetMimePatterns = [
|
|
70
|
+
"ms-excel",
|
|
71
|
+
"spreadsheetml",
|
|
72
|
+
"opendocument.spreadsheet",
|
|
73
|
+
"csv",
|
|
74
|
+
];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { documentExtensions, documentMimePatterns, spreadsheetExtensions, spreadsheetMimePatterns, videoExtensions, } from "../constants";
|
|
2
|
+
function extensionOf(fileName) {
|
|
3
|
+
const dot = fileName.lastIndexOf(".");
|
|
4
|
+
return dot < 0 ? "" : fileName.slice(dot + 1).toLowerCase();
|
|
5
|
+
}
|
|
6
|
+
function matchesAnyExtension(fileName, list) {
|
|
7
|
+
if (!fileName)
|
|
8
|
+
return false;
|
|
9
|
+
const ext = extensionOf(fileName);
|
|
10
|
+
if (!ext)
|
|
11
|
+
return false;
|
|
12
|
+
return list.some(({ type }) => type === ext);
|
|
13
|
+
}
|
|
14
|
+
function matchesAnyMimePattern(fileType, patterns) {
|
|
15
|
+
if (!fileType)
|
|
16
|
+
return false;
|
|
17
|
+
const lower = fileType.toLowerCase();
|
|
18
|
+
return patterns.some(pattern => lower.includes(pattern));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Bucket a file into one of the generic categories the design system
|
|
22
|
+
* recognises. Decoupled from icon names so that:
|
|
23
|
+
*
|
|
24
|
+
* 1. Consumers can use generic terminology in copy, accessibility
|
|
25
|
+
* labels, and filter UI ("Document", "Spreadsheet") without
|
|
26
|
+
* coupling to the brand-named icon assets (`word`, `excel`).
|
|
27
|
+
* 2. When `@jobber/design` adds generic-document / generic-spreadsheet
|
|
28
|
+
* icons, the icon mapping can be updated without breaking
|
|
29
|
+
* consumers who already use this categorisation.
|
|
30
|
+
*
|
|
31
|
+
* Detection precedence: `fileType` (MIME) first, then `fileName`
|
|
32
|
+
* extension. The extension lists cover OpenDocument Text / Spreadsheet,
|
|
33
|
+
* RTF, Apple Pages / Numbers, CSV, and WordPerfect alongside the
|
|
34
|
+
* Microsoft Office formats so callers do not have to enumerate them.
|
|
35
|
+
*
|
|
36
|
+
* `fileType` matching is case-insensitive and accepts both full MIME
|
|
37
|
+
* strings (`"video/mp4"`, `"image/jpeg"`) and coarse type strings
|
|
38
|
+
* (`"video"`, `"image"`) since `parseFile` collapses video MIMEs to
|
|
39
|
+
* the bare token `"video"` for external files.
|
|
40
|
+
*/
|
|
41
|
+
export function getFileCategory({ fileName, fileType, }) {
|
|
42
|
+
const normalizedType = fileType === null || fileType === void 0 ? void 0 : fileType.toLowerCase();
|
|
43
|
+
if ((normalizedType === null || normalizedType === void 0 ? void 0 : normalizedType.includes("pdf")) || (fileName === null || fileName === void 0 ? void 0 : fileName.match(/\.pdf$/i))) {
|
|
44
|
+
return "pdf";
|
|
45
|
+
}
|
|
46
|
+
if (normalizedType === null || normalizedType === void 0 ? void 0 : normalizedType.includes("image")) {
|
|
47
|
+
return "image";
|
|
48
|
+
}
|
|
49
|
+
if ((normalizedType === null || normalizedType === void 0 ? void 0 : normalizedType.includes("video")) ||
|
|
50
|
+
matchesAnyExtension(fileName, videoExtensions)) {
|
|
51
|
+
return "video";
|
|
52
|
+
}
|
|
53
|
+
if (matchesAnyMimePattern(normalizedType, documentMimePatterns) ||
|
|
54
|
+
matchesAnyExtension(fileName, documentExtensions)) {
|
|
55
|
+
return "document";
|
|
56
|
+
}
|
|
57
|
+
if (matchesAnyMimePattern(normalizedType, spreadsheetMimePatterns) ||
|
|
58
|
+
matchesAnyExtension(fileName, spreadsheetExtensions)) {
|
|
59
|
+
return "spreadsheet";
|
|
60
|
+
}
|
|
61
|
+
return "other";
|
|
62
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { getFileCategory } from "./getFileCategory";
|
|
2
|
+
describe("getFileCategory", () => {
|
|
3
|
+
it("returns 'other' when neither name nor type is provided", () => {
|
|
4
|
+
expect(getFileCategory({})).toBe("other");
|
|
5
|
+
});
|
|
6
|
+
it.each([
|
|
7
|
+
["application/pdf", undefined, "pdf"],
|
|
8
|
+
[undefined, "report.pdf", "pdf"],
|
|
9
|
+
[undefined, "REPORT.PDF", "pdf"],
|
|
10
|
+
])("pdf: type=%s fileName=%s → %s", (fileType, fileName, expected) => {
|
|
11
|
+
expect(getFileCategory({ fileType, fileName })).toBe(expected);
|
|
12
|
+
});
|
|
13
|
+
it.each([
|
|
14
|
+
["image/jpeg", undefined, "image"],
|
|
15
|
+
["image/png", "photo.png", "image"],
|
|
16
|
+
])("image: type=%s fileName=%s → %s", (fileType, fileName, expected) => {
|
|
17
|
+
expect(getFileCategory({ fileType, fileName })).toBe(expected);
|
|
18
|
+
});
|
|
19
|
+
it.each([
|
|
20
|
+
["video/mp4", undefined, "video"],
|
|
21
|
+
["video/quicktime", undefined, "video"],
|
|
22
|
+
[undefined, "clip.mp4", "video"],
|
|
23
|
+
[undefined, "clip.mov", "video"],
|
|
24
|
+
[undefined, "clip.webm", "video"],
|
|
25
|
+
[undefined, "clip.mkv", "video"],
|
|
26
|
+
[undefined, "clip.avi", "video"],
|
|
27
|
+
])("video: type=%s fileName=%s → %s", (fileType, fileName, expected) => {
|
|
28
|
+
expect(getFileCategory({ fileType, fileName })).toBe(expected);
|
|
29
|
+
});
|
|
30
|
+
it.each([
|
|
31
|
+
["application/msword", undefined, "document"],
|
|
32
|
+
[
|
|
33
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
34
|
+
undefined,
|
|
35
|
+
"document",
|
|
36
|
+
],
|
|
37
|
+
["application/vnd.oasis.opendocument.text", undefined, "document"],
|
|
38
|
+
["application/rtf", undefined, "document"],
|
|
39
|
+
["text/rtf", undefined, "document"],
|
|
40
|
+
[undefined, "report.doc", "document"],
|
|
41
|
+
[undefined, "report.docx", "document"],
|
|
42
|
+
[undefined, "report.odt", "document"],
|
|
43
|
+
[undefined, "report.fodt", "document"],
|
|
44
|
+
[undefined, "memo.rtf", "document"],
|
|
45
|
+
[undefined, "notes.pages", "document"],
|
|
46
|
+
[undefined, "legacy.wpd", "document"],
|
|
47
|
+
])("document: type=%s fileName=%s → %s", (fileType, fileName, expected) => {
|
|
48
|
+
expect(getFileCategory({ fileType, fileName })).toBe(expected);
|
|
49
|
+
});
|
|
50
|
+
it.each([
|
|
51
|
+
["application/vnd.ms-excel", undefined, "spreadsheet"],
|
|
52
|
+
[
|
|
53
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
54
|
+
undefined,
|
|
55
|
+
"spreadsheet",
|
|
56
|
+
],
|
|
57
|
+
[
|
|
58
|
+
"application/vnd.oasis.opendocument.spreadsheet",
|
|
59
|
+
undefined,
|
|
60
|
+
"spreadsheet",
|
|
61
|
+
],
|
|
62
|
+
["text/csv", undefined, "spreadsheet"],
|
|
63
|
+
[undefined, "data.xls", "spreadsheet"],
|
|
64
|
+
[undefined, "data.xlsx", "spreadsheet"],
|
|
65
|
+
[undefined, "data.ods", "spreadsheet"],
|
|
66
|
+
[undefined, "data.fods", "spreadsheet"],
|
|
67
|
+
[undefined, "data.numbers", "spreadsheet"],
|
|
68
|
+
[undefined, "data.csv", "spreadsheet"],
|
|
69
|
+
])("spreadsheet: type=%s fileName=%s → %s", (fileType, fileName, expected) => {
|
|
70
|
+
expect(getFileCategory({ fileType, fileName })).toBe(expected);
|
|
71
|
+
});
|
|
72
|
+
it.each([
|
|
73
|
+
["application/octet-stream", "unknown.bin", "other"],
|
|
74
|
+
[undefined, "no-extension", "other"],
|
|
75
|
+
[undefined, "presentation.pptx", "other"],
|
|
76
|
+
[undefined, "presentation.odp", "other"],
|
|
77
|
+
])("other: type=%s fileName=%s → %s", (fileType, fileName, expected) => {
|
|
78
|
+
expect(getFileCategory({ fileType, fileName })).toBe(expected);
|
|
79
|
+
});
|
|
80
|
+
it("prefers MIME over extension when both are present and disagree", () => {
|
|
81
|
+
expect(getFileCategory({ fileType: "application/pdf", fileName: "weird.docx" })).toBe("pdf");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
export { computeA11yLabel } from "./computeA11yLabel";
|
|
2
2
|
export { parseFile, isMediaFile } from "./parseFile";
|
|
3
|
+
export { mapFileTypeToIconName } from "./mapFileTypeToIconName";
|
|
4
|
+
export { getFileCategory } from "./getFileCategory";
|
|
5
|
+
export { sanitizeFileName } from "./sanitizeFileName";
|
|
6
|
+
export { truncateFileNameForLine } from "./truncateFileNameForLine";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getFileCategory } from "./getFileCategory";
|
|
2
|
+
/**
|
|
3
|
+
* Pick the file-type icon name for a given file. Today the design system
|
|
4
|
+
* exposes brand-named icons (`word`, `excel`) which double-duty as the
|
|
5
|
+
* generic "document" / "spreadsheet" indicators; when generic icons land
|
|
6
|
+
* in `@jobber/design`, the mapping below is the one place that needs to
|
|
7
|
+
* change.
|
|
8
|
+
*
|
|
9
|
+
* The actual file-type detection lives in `getFileCategory` so consumers
|
|
10
|
+
* that need a generic category name (e.g. for accessibility labels or
|
|
11
|
+
* filter UI) do not have to thread brand vocabulary through their copy.
|
|
12
|
+
*/
|
|
13
|
+
export function mapFileTypeToIconName({ fileName, fileType, }) {
|
|
14
|
+
if (!fileName && !fileType) {
|
|
15
|
+
return "alert";
|
|
16
|
+
}
|
|
17
|
+
switch (getFileCategory({ fileName, fileType })) {
|
|
18
|
+
case "pdf":
|
|
19
|
+
return "pdf";
|
|
20
|
+
case "document":
|
|
21
|
+
return "word";
|
|
22
|
+
case "spreadsheet":
|
|
23
|
+
return "excel";
|
|
24
|
+
case "video":
|
|
25
|
+
return "videoFile";
|
|
26
|
+
case "image":
|
|
27
|
+
case "other":
|
|
28
|
+
default:
|
|
29
|
+
return "file";
|
|
30
|
+
}
|
|
31
|
+
}
|