@traky-app/ui 1.6.7 → 1.6.8

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.
@@ -0,0 +1,99 @@
1
+ export const STORAGE_FILE_PROVIDERS = ["traky", "legacy"] as const
2
+
3
+ export type FileStorageProvider = (typeof STORAGE_FILE_PROVIDERS)[number]
4
+
5
+ export type FileItem = {
6
+ name: string
7
+ url: string
8
+ type: string
9
+ /** `traky` = objeto nuevo; `legacy` = externo. El host decide el valor. */
10
+ provider?: FileStorageProvider
11
+ }
12
+
13
+ function inferMimeFromName(name: string) {
14
+ const lower = name.toLowerCase()
15
+ if (/\.pdf$/i.test(lower)) return "application/pdf"
16
+ if (/\.(webm)$/i.test(lower)) return "video/webm"
17
+ if (/\.(mp4|mov|mkv)$/i.test(lower)) return "video/mp4"
18
+ if (/\.(png|jpe?g|webp|gif|bmp|svg)$/i.test(lower)) return "image/jpeg"
19
+ return ""
20
+ }
21
+
22
+ function resolveFileProvider(explicit?: unknown): FileStorageProvider | undefined {
23
+ if (explicit === "traky" || explicit === "legacy") return explicit
24
+ return undefined
25
+ }
26
+
27
+ /** Normaliza entradas (strings, objetos incompletos o sin `type`). */
28
+ export function normalizeFileItem(file: unknown): FileItem | null {
29
+ if (typeof file === "string" && file.trim()) {
30
+ const url = file.trim()
31
+ const name = url.split("/").pop()?.split("?")[0] || "archivo"
32
+ return {
33
+ name,
34
+ url,
35
+ type: inferMimeFromName(name),
36
+ }
37
+ }
38
+
39
+ if (!file || typeof file !== "object") return null
40
+
41
+ const item = file as Record<string, unknown>
42
+ const urlCandidate = [item.url, item.fileUrl, item.href, item.path, item.src].find(
43
+ (value) => typeof value === "string" && value.trim(),
44
+ )
45
+ if (typeof urlCandidate !== "string") return null
46
+
47
+ const url = urlCandidate.trim()
48
+ const name =
49
+ (typeof item.name === "string" && item.name.trim()) ||
50
+ (typeof item.fileName === "string" && item.fileName.trim()) ||
51
+ url.split("/").pop()?.split("?")[0] ||
52
+ "archivo"
53
+ const type =
54
+ (typeof item.type === "string" && item.type) ||
55
+ (typeof item.mimeType === "string" && item.mimeType) ||
56
+ inferMimeFromName(name) ||
57
+ inferMimeFromName(url)
58
+
59
+ return {
60
+ name,
61
+ url,
62
+ type,
63
+ provider: resolveFileProvider(item.provider),
64
+ }
65
+ }
66
+
67
+ export function normalizeFileItems(files: unknown): FileItem[] {
68
+ if (typeof files === "string") {
69
+ const trimmed = files.trim()
70
+ if (!trimmed) return []
71
+
72
+ if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
73
+ try {
74
+ return normalizeFileItems(JSON.parse(trimmed) as unknown)
75
+ } catch {
76
+ // JSON inválido: tratar como URL suelta abajo.
77
+ }
78
+ }
79
+
80
+ const single = normalizeFileItem(trimmed)
81
+ return single ? [single] : []
82
+ }
83
+
84
+ if (files && typeof files === "object" && !Array.isArray(files)) {
85
+ const single = normalizeFileItem(files)
86
+ return single ? [single] : []
87
+ }
88
+
89
+ if (!Array.isArray(files)) return []
90
+ return files
91
+ .map(normalizeFileItem)
92
+ .filter((file): file is FileItem => file !== null)
93
+ }
94
+
95
+ export function fileListSignature(files: unknown): string {
96
+ return normalizeFileItems(files)
97
+ .map((file) => `${file.url}|${file.name}|${file.type}`)
98
+ .join("\n")
99
+ }
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import {
3
+ getFileDisplayName,
4
+ isAcceptedFile,
5
+ isImageFile,
6
+ isPdfFile,
7
+ toFileItem,
8
+ } from "./file-manager-utils"
9
+ import { normalizeFileItems } from "./file-items"
10
+
11
+ describe("file-manager-utils", () => {
12
+ it("detecta imagen, pdf y tipos aceptados", () => {
13
+ expect(
14
+ isImageFile({ name: "a.png", url: "https://cdn.example/a.png", type: "" }),
15
+ ).toBe(true)
16
+ expect(
17
+ isPdfFile({
18
+ name: "doc.pdf",
19
+ url: "https://cdn.example/doc.pdf",
20
+ type: "",
21
+ }),
22
+ ).toBe(true)
23
+ expect(
24
+ isAcceptedFile(
25
+ new File(["x"], "foto.jpg", { type: "image/jpeg" }),
26
+ false,
27
+ ),
28
+ ).toBe(true)
29
+ expect(
30
+ isAcceptedFile(new File(["x"], "clip.mp4", { type: "video/mp4" }), false),
31
+ ).toBe(false)
32
+ expect(
33
+ isAcceptedFile(new File(["x"], "clip.mp4", { type: "video/mp4" }), true),
34
+ ).toBe(true)
35
+ })
36
+
37
+ it("arma FileItem desde el archivo subido", () => {
38
+ const file = new File(["x"], "bodega.webp", { type: "image/webp" })
39
+ expect(toFileItem(file, "https://cdn.example/bodega.webp", "traky")).toEqual({
40
+ name: "bodega.webp",
41
+ url: "https://cdn.example/bodega.webp",
42
+ type: "image/webp",
43
+ provider: "traky",
44
+ })
45
+ })
46
+
47
+ it("acorta nombres largos conservando extensión", () => {
48
+ expect(
49
+ getFileDisplayName(
50
+ {
51
+ name: "foto-de-bodega-muy-larga-para-mostrar.png",
52
+ url: "https://cdn.example/x.png",
53
+ type: "image/png",
54
+ },
55
+ 20,
56
+ ),
57
+ ).toMatch(/…\.png$/)
58
+ })
59
+
60
+ it("normaliza JSON y conserva provider explícito", () => {
61
+ const files = normalizeFileItems({
62
+ url: "https://cdn.example.com/foto.jpg",
63
+ name: "foto.jpg",
64
+ provider: "legacy",
65
+ })
66
+ expect(files[0]).toMatchObject({
67
+ url: "https://cdn.example.com/foto.jpg",
68
+ provider: "legacy",
69
+ })
70
+ })
71
+ })
@@ -0,0 +1,116 @@
1
+ import type { FileItem, FileStorageProvider } from "./file-items"
2
+
3
+ export const ACCEPTED_TYPES = ".png,.jpg,.jpeg,.webp,.gif,.pdf"
4
+ export const ACCEPTED_TYPES_WITH_VIDEO =
5
+ ".png,.jpg,.jpeg,.webp,.gif,.pdf,.mp4,.webm,.mov,.mkv"
6
+ export const ACCEPTED_MIME = [
7
+ "image/png",
8
+ "image/jpeg",
9
+ "image/webp",
10
+ "image/gif",
11
+ "application/pdf",
12
+ ]
13
+ export const VIDEO_MIME = [
14
+ "video/mp4",
15
+ "video/webm",
16
+ "video/quicktime",
17
+ "video/x-matroska",
18
+ ]
19
+
20
+ export function isImageFile(file: FileItem) {
21
+ const type = file.type ?? ""
22
+ const name = file.name ?? ""
23
+ const url = file.url ?? ""
24
+ return (
25
+ type.startsWith("image/") ||
26
+ /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(name) ||
27
+ /\.(png|jpe?g|gif|webp|bmp|svg)(\?|$)/i.test(url)
28
+ )
29
+ }
30
+
31
+ export function isPdfFile(file: FileItem) {
32
+ const type = file.type ?? ""
33
+ const name = file.name ?? ""
34
+ const url = file.url ?? ""
35
+ return (
36
+ type.includes("pdf") || /\.pdf$/i.test(name) || /\.pdf(\?|$)/i.test(url)
37
+ )
38
+ }
39
+
40
+ export function isVideoFile(file: FileItem) {
41
+ const type = file.type ?? ""
42
+ const name = file.name ?? ""
43
+ const url = file.url ?? ""
44
+ return (
45
+ type.startsWith("video/") ||
46
+ /\.(mp4|webm|mov|mkv)$/i.test(name) ||
47
+ /\.(mp4|webm|mov|mkv)(\?|$)/i.test(url)
48
+ )
49
+ }
50
+
51
+ export function isAcceptedFile(file: File, acceptVideos: boolean) {
52
+ const type = file.type ?? ""
53
+ if (ACCEPTED_MIME.includes(type)) return true
54
+ if (acceptVideos && (VIDEO_MIME.includes(type) || type.startsWith("video/"))) {
55
+ return true
56
+ }
57
+ if (/\.(png|jpe?g|webp|gif|pdf)$/i.test(file.name)) return true
58
+ if (acceptVideos && /\.(mp4|webm|mov|mkv)$/i.test(file.name)) return true
59
+ return false
60
+ }
61
+
62
+ export function getFileDisplayName(file: FileItem, maxLength = 40): string {
63
+ let label = file.name?.trim() ?? ""
64
+ const looksLikeUrl =
65
+ /^https?:\/\//i.test(label) || (label.length > 72 && !label.includes(" "))
66
+
67
+ if (!label || looksLikeUrl) {
68
+ try {
69
+ const pathname = decodeURIComponent(new URL(file.url ?? "").pathname)
70
+ const fromUrl = pathname.split("/").filter(Boolean).pop() ?? ""
71
+ if (fromUrl) label = fromUrl
72
+ } catch {
73
+ // ignore invalid URL
74
+ }
75
+ }
76
+
77
+ if (!label) return "Archivo"
78
+ if (label.length <= maxLength) return label
79
+
80
+ const dot = label.lastIndexOf(".")
81
+ const ext = dot > 0 && label.length - dot <= 8 ? label.slice(dot) : ""
82
+ const stem = ext ? label.slice(0, dot) : label
83
+ const budget = maxLength - ext.length - 1
84
+ if (budget < 8) return `${label.slice(0, maxLength - 1)}…`
85
+ return `${stem.slice(0, budget)}…${ext}`
86
+ }
87
+
88
+ export function getFileFullName(file: FileItem): string {
89
+ const label = file.name?.trim()
90
+ if (label && !/^https?:\/\//i.test(label)) return label
91
+ return getFileDisplayName(file, 200)
92
+ }
93
+
94
+ export function downloadFile(file: FileItem) {
95
+ const link = document.createElement("a")
96
+ link.href = file.url
97
+ link.download = file.name || "archivo"
98
+ link.target = "_blank"
99
+ link.rel = "noopener noreferrer"
100
+ document.body.appendChild(link)
101
+ link.click()
102
+ link.remove()
103
+ }
104
+
105
+ export function toFileItem(
106
+ file: File,
107
+ url: string,
108
+ provider?: FileStorageProvider,
109
+ ): FileItem {
110
+ return {
111
+ name: file.name,
112
+ url,
113
+ type: file.type,
114
+ ...(provider ? { provider } : {}),
115
+ }
116
+ }