@noya-app/noya-file-explorer 0.0.19 → 0.0.20
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/.turbo/turbo-build.log +11 -11
- package/CHANGELOG.md +11 -0
- package/dist/index.css +850 -8
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +22 -8
- package/dist/index.d.ts +22 -8
- package/dist/index.js +102 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +100 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/MediaCollection.tsx +25 -17
- package/src/ResourceExplorer.tsx +20 -8
- package/src/index.ts +1 -0
- package/src/utils/contentType.ts +99 -0
- package/src/utils/handleFileDrop.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noya-app/noya-file-explorer",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"module": "./dist/index.mjs",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
"dev": "npm run build:main -- --watch"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@noya-app/noya-designsystem": "0.1.
|
|
24
|
-
"@noya-app/noya-icons": "0.1.
|
|
25
|
-
"@noya-app/noya-multiplayer-react": "0.1.
|
|
23
|
+
"@noya-app/noya-designsystem": "0.1.67",
|
|
24
|
+
"@noya-app/noya-icons": "0.1.14",
|
|
25
|
+
"@noya-app/noya-multiplayer-react": "0.1.67",
|
|
26
26
|
"@noya-app/noya-keymap": "0.1.4",
|
|
27
27
|
"@noya-app/react-utils": "0.1.26",
|
|
28
28
|
"@noya-app/noya-utils": "0.1.8",
|
|
29
|
-
"@noya-app/noya-schemas": "0.1.
|
|
29
|
+
"@noya-app/noya-schemas": "0.1.8",
|
|
30
30
|
"imfs": "^0.1.0",
|
|
31
31
|
"browser-fs-access": "0.35.0"
|
|
32
32
|
},
|
package/src/MediaCollection.tsx
CHANGED
|
@@ -548,11 +548,12 @@ export const MediaCollection = memo(
|
|
|
548
548
|
|
|
549
549
|
// Create assets in parallel for better performance
|
|
550
550
|
const uploadPromises = files.map(async (file) => {
|
|
551
|
-
const
|
|
551
|
+
const created = await assetManager.create(file);
|
|
552
|
+
const assetStableId = created.id;
|
|
552
553
|
const assetPath = path.join(parentPath, path.basename(file.name));
|
|
553
554
|
return {
|
|
554
555
|
assetPath,
|
|
555
|
-
asset: createMediaAsset({ assetId:
|
|
556
|
+
asset: createMediaAsset({ assetId: assetStableId }),
|
|
556
557
|
};
|
|
557
558
|
});
|
|
558
559
|
|
|
@@ -585,12 +586,14 @@ export const MediaCollection = memo(
|
|
|
585
586
|
[tree.idToPathMap, setMedia, media, assetManager, onAssetsUploaded]
|
|
586
587
|
);
|
|
587
588
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
const asset =
|
|
589
|
+
const handleDownload = useCallback(
|
|
590
|
+
async (selectedItems: MediaItem[]) => {
|
|
591
|
+
const downloadPromises = selectedItems
|
|
592
|
+
.filter((item) => item.kind === "asset")
|
|
593
|
+
.map(async (item) => {
|
|
594
|
+
const asset =
|
|
595
|
+
assets.find((a) => a.stableId === item.assetId) ??
|
|
596
|
+
assets.find((a) => a.id === item.assetId);
|
|
594
597
|
if (!asset?.url) return;
|
|
595
598
|
return downloadUrl(asset.url, tree.getNameForId(item.id));
|
|
596
599
|
});
|
|
@@ -600,12 +603,14 @@ export const MediaCollection = memo(
|
|
|
600
603
|
[assets, tree]
|
|
601
604
|
);
|
|
602
605
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
const asset =
|
|
606
|
+
const handlePreview = useCallback(
|
|
607
|
+
async (selectedItems: MediaItem[]) => {
|
|
608
|
+
const previewPromises = selectedItems
|
|
609
|
+
.filter((item) => item.kind === "asset")
|
|
610
|
+
.map(async (item) => {
|
|
611
|
+
const asset =
|
|
612
|
+
assets.find((a) => a.stableId === item.assetId) ??
|
|
613
|
+
assets.find((a) => a.id === item.assetId);
|
|
609
614
|
if (!asset?.url) return;
|
|
610
615
|
return window?.open(asset.url, "_blank");
|
|
611
616
|
});
|
|
@@ -621,7 +626,8 @@ export const MediaCollection = memo(
|
|
|
621
626
|
const file = await fileOpen();
|
|
622
627
|
if (!file) return;
|
|
623
628
|
// Create the asset
|
|
624
|
-
const
|
|
629
|
+
const created = await assetManager.create(file);
|
|
630
|
+
const assetStableId = created.id;
|
|
625
631
|
const oldFile = selectedItem;
|
|
626
632
|
const oldFilePath = tree.idToPathMap.get(oldFile.id);
|
|
627
633
|
if (!oldFilePath || oldFile.kind !== "asset") return;
|
|
@@ -631,7 +637,7 @@ export const MediaCollection = memo(
|
|
|
631
637
|
...media,
|
|
632
638
|
[oldFilePath]: createMediaAsset({
|
|
633
639
|
...oldFile,
|
|
634
|
-
assetId:
|
|
640
|
+
assetId: assetStableId,
|
|
635
641
|
}),
|
|
636
642
|
}
|
|
637
643
|
);
|
|
@@ -905,7 +911,9 @@ export const MediaCollection = memo(
|
|
|
905
911
|
renderAction={renderAction}
|
|
906
912
|
renderDetail={(file, selected) => {
|
|
907
913
|
if (file.kind !== "asset") return null;
|
|
908
|
-
const asset =
|
|
914
|
+
const asset =
|
|
915
|
+
assets.find((a) => a.stableId === file.assetId) ??
|
|
916
|
+
assets.find((a) => a.id === file.assetId);
|
|
909
917
|
if (!asset) return null;
|
|
910
918
|
return (
|
|
911
919
|
<FileExplorerDetail selected={selected} size={size}>
|
package/src/ResourceExplorer.tsx
CHANGED
|
@@ -74,6 +74,7 @@ import {
|
|
|
74
74
|
import { Size } from "@noya-app/noya-geometry";
|
|
75
75
|
import { path } from "imfs";
|
|
76
76
|
import React from "react";
|
|
77
|
+
import { getContentTypeFromFile } from "./utils/contentType";
|
|
77
78
|
import { handleDataTransfer } from "./utils/handleFileDrop";
|
|
78
79
|
import {
|
|
79
80
|
deleteResources,
|
|
@@ -235,6 +236,8 @@ type ResourceExplorerProps = {
|
|
|
235
236
|
virtualized?: boolean;
|
|
236
237
|
|
|
237
238
|
renderUser?: (resource: Resource) => ReactNode;
|
|
239
|
+
/** Show file sizes in list view detail. Defaults to true. */
|
|
240
|
+
showFileSizes?: boolean;
|
|
238
241
|
} & Pick<
|
|
239
242
|
CollectionProps<Resource, MenuAction>,
|
|
240
243
|
| "sortableId"
|
|
@@ -288,6 +291,7 @@ export const ResourceExplorer = memo(
|
|
|
288
291
|
publishedResources,
|
|
289
292
|
virtualized = false,
|
|
290
293
|
renderUser,
|
|
294
|
+
showFileSizes = true,
|
|
291
295
|
},
|
|
292
296
|
ref
|
|
293
297
|
) {
|
|
@@ -537,7 +541,7 @@ export const ResourceExplorer = memo(
|
|
|
537
541
|
id: uuid(),
|
|
538
542
|
asset: {
|
|
539
543
|
content: Base64.encode(await file.arrayBuffer()),
|
|
540
|
-
contentType: file
|
|
544
|
+
contentType: getContentTypeFromFile(file),
|
|
541
545
|
encoding: "base64",
|
|
542
546
|
},
|
|
543
547
|
path: assetPath,
|
|
@@ -581,7 +585,9 @@ export const ResourceExplorer = memo(
|
|
|
581
585
|
const downloadPromises = selectedItems
|
|
582
586
|
.filter((item) => item.type === "asset")
|
|
583
587
|
.map(async (item) => {
|
|
584
|
-
const asset =
|
|
588
|
+
const asset =
|
|
589
|
+
assets.find((a) => a.stableId === item.assetId) ??
|
|
590
|
+
assets.find((a) => a.id === item.assetId);
|
|
585
591
|
if (!asset?.url) return;
|
|
586
592
|
return downloadUrl(asset.url, tree.getNameForId(item.id));
|
|
587
593
|
});
|
|
@@ -596,7 +602,9 @@ export const ResourceExplorer = memo(
|
|
|
596
602
|
const previewPromises = selectedItems
|
|
597
603
|
.filter((item) => item.type === "asset")
|
|
598
604
|
.map(async (item) => {
|
|
599
|
-
const asset =
|
|
605
|
+
const asset =
|
|
606
|
+
assets.find((a) => a.stableId === item.assetId) ??
|
|
607
|
+
assets.find((a) => a.id === item.assetId);
|
|
600
608
|
if (!asset?.url) return;
|
|
601
609
|
return window?.open(asset.url, "_blank");
|
|
602
610
|
});
|
|
@@ -612,7 +620,8 @@ export const ResourceExplorer = memo(
|
|
|
612
620
|
const file = await fileOpen();
|
|
613
621
|
if (!file) return;
|
|
614
622
|
// Create the asset
|
|
615
|
-
const
|
|
623
|
+
const created = await assetManager.create(file);
|
|
624
|
+
const assetStableId = created.id;
|
|
616
625
|
const oldFile = selectedItem;
|
|
617
626
|
const oldFilePath = tree.idToPathMap.get(oldFile.id);
|
|
618
627
|
if (!oldFilePath || oldFile.type !== "asset") return;
|
|
@@ -622,7 +631,7 @@ export const ResourceExplorer = memo(
|
|
|
622
631
|
...media,
|
|
623
632
|
[oldFilePath]: createAssetResource({
|
|
624
633
|
...oldFile,
|
|
625
|
-
assetId:
|
|
634
|
+
assetId: assetStableId,
|
|
626
635
|
}),
|
|
627
636
|
}
|
|
628
637
|
);
|
|
@@ -887,6 +896,7 @@ export const ResourceExplorer = memo(
|
|
|
887
896
|
}}
|
|
888
897
|
getExpanded={getExpanded}
|
|
889
898
|
setExpanded={handleSetExpanded}
|
|
899
|
+
renameSelectsBeforeDot
|
|
890
900
|
getRenamable={(item) => {
|
|
891
901
|
if (item.id === rootResource.id) return false;
|
|
892
902
|
return true;
|
|
@@ -925,7 +935,7 @@ export const ResourceExplorer = memo(
|
|
|
925
935
|
|
|
926
936
|
return (
|
|
927
937
|
<div
|
|
928
|
-
style={{ display: "flex", alignItems: "center", gap: "
|
|
938
|
+
style={{ display: "flex", alignItems: "center", gap: "6px" }}
|
|
929
939
|
>
|
|
930
940
|
{publishStatus && (
|
|
931
941
|
<Chip
|
|
@@ -945,9 +955,11 @@ export const ResourceExplorer = memo(
|
|
|
945
955
|
);
|
|
946
956
|
}}
|
|
947
957
|
renderDetail={(file, selected) => {
|
|
958
|
+
if (!showFileSizes) return null;
|
|
948
959
|
if (file.type !== "asset") return null;
|
|
949
|
-
|
|
950
|
-
|
|
960
|
+
const asset =
|
|
961
|
+
assets.find((a) => a.stableId === file.assetId) ??
|
|
962
|
+
assets.find((a) => a.id === file.assetId);
|
|
951
963
|
|
|
952
964
|
if (!asset) return null;
|
|
953
965
|
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the content type (MIME type) for a file.
|
|
3
|
+
*
|
|
4
|
+
* This function first checks if the browser has determined a content type
|
|
5
|
+
* for the file. If not (which commonly happens for files like .md, .txt, etc.
|
|
6
|
+
* that don't have OS-registered MIME types), it falls back to extension-based
|
|
7
|
+
* detection.
|
|
8
|
+
*/
|
|
9
|
+
export function getContentTypeFromFile(file: File): string {
|
|
10
|
+
// If browser provides a type, use it
|
|
11
|
+
if (file.type) {
|
|
12
|
+
return file.type;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Fall back to extension-based detection
|
|
16
|
+
const extension = file.name.split(".").pop()?.toLowerCase();
|
|
17
|
+
|
|
18
|
+
return getContentTypeFromExtension(extension);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Get the content type (MIME type) for a file extension. */
|
|
22
|
+
export function getContentTypeFromExtension(
|
|
23
|
+
extension: string | undefined
|
|
24
|
+
): string {
|
|
25
|
+
if (!extension) {
|
|
26
|
+
return "application/octet-stream";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const extensionToMimeType: Record<string, string> = {
|
|
30
|
+
// Text files
|
|
31
|
+
md: "text/markdown",
|
|
32
|
+
markdown: "text/markdown",
|
|
33
|
+
txt: "text/plain",
|
|
34
|
+
json: "application/json",
|
|
35
|
+
xml: "application/xml",
|
|
36
|
+
csv: "text/csv",
|
|
37
|
+
html: "text/html",
|
|
38
|
+
htm: "text/html",
|
|
39
|
+
css: "text/css",
|
|
40
|
+
js: "text/javascript",
|
|
41
|
+
mjs: "text/javascript",
|
|
42
|
+
ts: "text/typescript",
|
|
43
|
+
tsx: "text/typescript",
|
|
44
|
+
jsx: "text/javascript",
|
|
45
|
+
|
|
46
|
+
// Images
|
|
47
|
+
svg: "image/svg+xml",
|
|
48
|
+
png: "image/png",
|
|
49
|
+
jpg: "image/jpeg",
|
|
50
|
+
jpeg: "image/jpeg",
|
|
51
|
+
gif: "image/gif",
|
|
52
|
+
webp: "image/webp",
|
|
53
|
+
bmp: "image/bmp",
|
|
54
|
+
ico: "image/x-icon",
|
|
55
|
+
tiff: "image/tiff",
|
|
56
|
+
tif: "image/tiff",
|
|
57
|
+
|
|
58
|
+
// Documents
|
|
59
|
+
pdf: "application/pdf",
|
|
60
|
+
doc: "application/msword",
|
|
61
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
62
|
+
xls: "application/vnd.ms-excel",
|
|
63
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
64
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
65
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
66
|
+
|
|
67
|
+
// Audio
|
|
68
|
+
mp3: "audio/mpeg",
|
|
69
|
+
wav: "audio/wav",
|
|
70
|
+
ogg: "audio/ogg",
|
|
71
|
+
m4a: "audio/mp4",
|
|
72
|
+
flac: "audio/flac",
|
|
73
|
+
|
|
74
|
+
// Video
|
|
75
|
+
mp4: "video/mp4",
|
|
76
|
+
webm: "video/webm",
|
|
77
|
+
mov: "video/quicktime",
|
|
78
|
+
avi: "video/x-msvideo",
|
|
79
|
+
mkv: "video/x-matroska",
|
|
80
|
+
|
|
81
|
+
// Archives
|
|
82
|
+
zip: "application/zip",
|
|
83
|
+
tar: "application/x-tar",
|
|
84
|
+
gz: "application/gzip",
|
|
85
|
+
"7z": "application/x-7z-compressed",
|
|
86
|
+
rar: "application/vnd.rar",
|
|
87
|
+
|
|
88
|
+
// Fonts
|
|
89
|
+
woff: "font/woff",
|
|
90
|
+
woff2: "font/woff2",
|
|
91
|
+
ttf: "font/ttf",
|
|
92
|
+
otf: "font/otf",
|
|
93
|
+
|
|
94
|
+
// Other
|
|
95
|
+
sketch: "application/x-sketch",
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return extensionToMimeType[extension] || "application/octet-stream";
|
|
99
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "@noya-app/noya-schemas";
|
|
9
9
|
import { Base64, uuid } from "@noya-app/noya-utils";
|
|
10
10
|
import { path } from "imfs";
|
|
11
|
+
import { getContentTypeFromFile } from "./contentType";
|
|
11
12
|
|
|
12
13
|
function isDirectoryEntry(
|
|
13
14
|
entry: FileSystemEntry
|
|
@@ -143,7 +144,7 @@ export async function handleDataTransfer({
|
|
|
143
144
|
id: uuid(),
|
|
144
145
|
asset: {
|
|
145
146
|
content: Base64.encode(await file.arrayBuffer()),
|
|
146
|
-
contentType: file
|
|
147
|
+
contentType: getContentTypeFromFile(file),
|
|
147
148
|
encoding: "base64",
|
|
148
149
|
},
|
|
149
150
|
path: path.join(rootItemPath, relativePath),
|