@noya-app/noya-file-explorer 0.0.19 → 0.0.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noya-app/noya-file-explorer",
3
- "version": "0.0.19",
3
+ "version": "0.0.21",
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.66",
24
- "@noya-app/noya-icons": "0.1.13",
25
- "@noya-app/noya-multiplayer-react": "0.1.66",
23
+ "@noya-app/noya-designsystem": "0.1.68",
24
+ "@noya-app/noya-icons": "0.1.14",
25
+ "@noya-app/noya-multiplayer-react": "0.1.68",
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.7",
29
+ "@noya-app/noya-schemas": "0.1.8",
30
30
  "imfs": "^0.1.0",
31
31
  "browser-fs-access": "0.35.0"
32
32
  },
@@ -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 asset = await assetManager.create(file);
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: asset.id }),
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
- const handleDownload = useCallback(
589
- async (selectedItems: MediaItem[]) => {
590
- const downloadPromises = selectedItems
591
- .filter((item) => item.kind === "asset")
592
- .map(async (item) => {
593
- const asset = assets.find((a) => a.id === item.assetId);
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
- const handlePreview = useCallback(
604
- async (selectedItems: MediaItem[]) => {
605
- const previewPromises = selectedItems
606
- .filter((item) => item.kind === "asset")
607
- .map(async (item) => {
608
- const asset = assets.find((a) => a.id === item.assetId);
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 asset = await assetManager.create(file);
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: asset.id,
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 = assets.find((a) => a.id === file.assetId);
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}>
@@ -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.type,
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 = assets.find((a) => a.id === item.assetId);
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 = assets.find((a) => a.id === item.assetId);
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 asset = await assetManager.create(file);
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: asset.id,
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: "1.5px" }}
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
- const asset = assets.find((a) => a.id === file.assetId);
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
@@ -1,6 +1,7 @@
1
1
  import "./index.css";
2
2
  export * from "./MediaCollection";
3
3
  export * from "./ResourceExplorer";
4
+ export * from "./utils/contentType";
4
5
  export * from "./utils/files";
5
6
  export * from "./utils/mediaItemTree";
6
7
  export * from "./utils/resourceUtils";
@@ -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.type,
147
+ contentType: getContentTypeFromFile(file),
147
148
  encoding: "base64",
148
149
  },
149
150
  path: path.join(rootItemPath, relativePath),