@jobber/components-native 0.101.11 → 0.102.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/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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { IconNames } from "@jobber/design";
|
|
2
|
+
import { getFileCategory } from "./getFileCategory";
|
|
3
|
+
|
|
4
|
+
interface FileTypeToIconNameParams {
|
|
5
|
+
readonly fileName?: string;
|
|
6
|
+
readonly fileType?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Pick the file-type icon name for a given file. Today the design system
|
|
11
|
+
* exposes brand-named icons (`word`, `excel`) which double-duty as the
|
|
12
|
+
* generic "document" / "spreadsheet" indicators; when generic icons land
|
|
13
|
+
* in `@jobber/design`, the mapping below is the one place that needs to
|
|
14
|
+
* change.
|
|
15
|
+
*
|
|
16
|
+
* The actual file-type detection lives in `getFileCategory` so consumers
|
|
17
|
+
* that need a generic category name (e.g. for accessibility labels or
|
|
18
|
+
* filter UI) do not have to thread brand vocabulary through their copy.
|
|
19
|
+
*/
|
|
20
|
+
export function mapFileTypeToIconName({
|
|
21
|
+
fileName,
|
|
22
|
+
fileType,
|
|
23
|
+
}: FileTypeToIconNameParams): IconNames {
|
|
24
|
+
if (!fileName && !fileType) {
|
|
25
|
+
return "alert";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
switch (getFileCategory({ fileName, fileType })) {
|
|
29
|
+
case "pdf":
|
|
30
|
+
return "pdf";
|
|
31
|
+
case "document":
|
|
32
|
+
return "word";
|
|
33
|
+
case "spreadsheet":
|
|
34
|
+
return "excel";
|
|
35
|
+
case "video":
|
|
36
|
+
return "videoFile";
|
|
37
|
+
case "image":
|
|
38
|
+
case "other":
|
|
39
|
+
default:
|
|
40
|
+
return "file";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { sanitizeFileName } from "./sanitizeFileName";
|
|
2
|
+
|
|
3
|
+
describe("sanitizeFileName", () => {
|
|
4
|
+
it("returns an empty string when input is undefined", () => {
|
|
5
|
+
expect(sanitizeFileName(undefined)).toBe("");
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it("returns the input unchanged when no newlines are present", () => {
|
|
9
|
+
expect(sanitizeFileName("report.pdf")).toBe("report.pdf");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("collapses a single embedded \\n into a space", () => {
|
|
13
|
+
expect(sanitizeFileName("first\nsecond.txt")).toBe("first second.txt");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("collapses runs of CR / LF into a single space", () => {
|
|
17
|
+
expect(sanitizeFileName("first\r\n\r\nsecond.txt")).toBe(
|
|
18
|
+
"first second.txt",
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("leaves spaces unchanged", () => {
|
|
23
|
+
expect(sanitizeFileName("with spaces.pdf")).toBe("with spaces.pdf");
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collapse CR / LF in a filename to a single space so embedded line breaks
|
|
3
|
+
* never reach the rendered `Text` and cause vertical layout drift.
|
|
4
|
+
*/
|
|
5
|
+
export function sanitizeFileName(name?: string): string {
|
|
6
|
+
if (!name) return "";
|
|
7
|
+
|
|
8
|
+
return name.replace(/[\r\n]+/g, " ");
|
|
9
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { truncateFileNameForLine } from "./truncateFileNameForLine";
|
|
2
|
+
|
|
3
|
+
describe("truncateFileNameForLine", () => {
|
|
4
|
+
it("returns an empty string when the input is undefined", () => {
|
|
5
|
+
expect(truncateFileNameForLine(undefined)).toBe("");
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it("returns the name unchanged when it fits inside the budget", () => {
|
|
9
|
+
expect(truncateFileNameForLine("report.pdf")).toBe("report.pdf");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("preserves a short extension and truncates the stem with a middle ellipsis", () => {
|
|
13
|
+
expect(truncateFileNameForLine("file-sample_2_dx.docx")).toBe(
|
|
14
|
+
"file-samp….docx",
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("preserves an uncommon extension like .fodt", () => {
|
|
19
|
+
expect(truncateFileNameForLine("very-long-name-for-document.fodt")).toBe(
|
|
20
|
+
"very-long….fodt",
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("preserves longer extensions like .numbers", () => {
|
|
25
|
+
expect(truncateFileNameForLine("quarterly-budget.numbers")).toBe(
|
|
26
|
+
"quarte….numbers",
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("treats overly long trailing tokens as not-an-extension and tail truncates", () => {
|
|
31
|
+
expect(truncateFileNameForLine("file.verylongextensionname")).toBe(
|
|
32
|
+
"file.verylonge…",
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("truncates a URL-shaped filename with no detectable extension using a tail ellipsis", () => {
|
|
37
|
+
expect(truncateFileNameForLine("vertopal.com/converter")).toBe(
|
|
38
|
+
"vertopal.com/c…",
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("falls back to tail truncation when the extension cannot fit", () => {
|
|
43
|
+
const long = "a".repeat(50) + ".verylongextension";
|
|
44
|
+
expect(truncateFileNameForLine(long, 24)).toMatch(/^a+…$/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("respects a caller-supplied maxLength", () => {
|
|
48
|
+
expect(truncateFileNameForLine("report.pdf", 6)).toBe("r….pdf");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns a name shorter than the budget unchanged regardless of extension", () => {
|
|
52
|
+
expect(truncateFileNameForLine("notes.txt")).toBe("notes.txt");
|
|
53
|
+
expect(truncateFileNameForLine("a/b/c")).toBe("a/b/c");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("clamps maxLength to a sensible minimum to preserve the single-line guarantee", () => {
|
|
57
|
+
expect(truncateFileNameForLine("report.pdf", 0)).toBe("r…");
|
|
58
|
+
expect(truncateFileNameForLine("report.pdf", -5)).toBe("r…");
|
|
59
|
+
expect(truncateFileNameForLine("report.pdf", 1)).toBe("r…");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const DEFAULT_MAX_LENGTH = 15;
|
|
2
|
+
const MIN_MAX_LENGTH = 2;
|
|
3
|
+
const ELLIPSIS = "…";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Maximum total length (including the leading dot) of a trailing token that
|
|
7
|
+
* we are willing to treat as a file extension. 10 covers every real-world
|
|
8
|
+
* extension this package routes to a recognisable icon (`.numbers`, `.fodt`,
|
|
9
|
+
* `.docx`, `.xlsx`, etc.) while rejecting accidental long suffixes from
|
|
10
|
+
* URL-shaped filenames. Chained pseudo-extensions like `.tar.gz` collapse
|
|
11
|
+
* to the trailing portion (`.gz`), which is the common-sense user
|
|
12
|
+
* expectation.
|
|
13
|
+
*/
|
|
14
|
+
const MAX_EXTENSION_LENGTH = 10;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Truncate a filename so it fits a single line in the small file-type tile.
|
|
18
|
+
*
|
|
19
|
+
* Why this exists: on Android, RN's `Text` with `numberOfLines={1}` does not
|
|
20
|
+
* always enforce single-line layout for URL-shaped strings with no
|
|
21
|
+
* whitespace (e.g. `vertopal.com/converter`). They wrap to a second line
|
|
22
|
+
* which then bleeds outside the rounded tile border. Pre-truncating in JS
|
|
23
|
+
* guarantees the rendered string is short enough that single-line wrapping
|
|
24
|
+
* is never required.
|
|
25
|
+
*
|
|
26
|
+
* The truncation preserves the trailing file extension when one is
|
|
27
|
+
* detectable so the file type stays visible at a glance (examples below
|
|
28
|
+
* use the default 15-character budget):
|
|
29
|
+
* `vertopal.com/converter` → `vertopal.com/c…`
|
|
30
|
+
* `file-sample_2_dx.docx` → `file-samp….docx`
|
|
31
|
+
* `quarterly-budget.numbers` → `quarte….numbers`
|
|
32
|
+
* `report.pdf` → `report.pdf` (unchanged)
|
|
33
|
+
* `embedded\nname.txt` → caller should pass through
|
|
34
|
+
* `sanitizeFileName` first
|
|
35
|
+
*
|
|
36
|
+
* `maxLength` is clamped to a sensible minimum of 2 so a caller passing
|
|
37
|
+
* `0` or a negative value cannot violate the single-line guarantee.
|
|
38
|
+
*/
|
|
39
|
+
// eslint-disable-next-line max-statements
|
|
40
|
+
export function truncateFileNameForLine(
|
|
41
|
+
name?: string,
|
|
42
|
+
maxLength = DEFAULT_MAX_LENGTH,
|
|
43
|
+
): string {
|
|
44
|
+
if (!name) return "";
|
|
45
|
+
|
|
46
|
+
const budget = Math.max(MIN_MAX_LENGTH, Math.floor(maxLength));
|
|
47
|
+
if (name.length <= budget) return name;
|
|
48
|
+
|
|
49
|
+
const lastDot = name.lastIndexOf(".");
|
|
50
|
+
const extensionLength = name.length - lastDot;
|
|
51
|
+
const hasExtension =
|
|
52
|
+
lastDot > 0 &&
|
|
53
|
+
lastDot < name.length - 1 &&
|
|
54
|
+
extensionLength <= MAX_EXTENSION_LENGTH;
|
|
55
|
+
|
|
56
|
+
if (!hasExtension) {
|
|
57
|
+
return name.slice(0, budget - 1) + ELLIPSIS;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const extension = name.slice(lastDot);
|
|
61
|
+
const stemBudget = budget - extension.length - 1;
|
|
62
|
+
|
|
63
|
+
if (stemBudget <= 0) {
|
|
64
|
+
return name.slice(0, budget - 1) + ELLIPSIS;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return name.slice(0, stemBudget) + ELLIPSIS + extension;
|
|
68
|
+
}
|