@noya-app/noya-file-explorer 0.0.15 → 0.0.17

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.
@@ -1,6 +1,6 @@
1
1
  import { MediaMap } from "@noya-app/noya-schemas";
2
2
  import { expect, it } from "bun:test";
3
- import { validateMediaItemRename } from "../utils/files";
3
+ import { mediaValidateMediaItemRename } from "../utils/files";
4
4
  import { createMediaAsset, createMediaFolder } from "../utils/mediaItemTree";
5
5
 
6
6
  it("should return true for valid rename with no conflicts", () => {
@@ -11,7 +11,7 @@ it("should return true for valid rename with no conflicts", () => {
11
11
  "dir1/file1.jpg": file1,
12
12
  };
13
13
 
14
- const result = validateMediaItemRename({
14
+ const result = mediaValidateMediaItemRename({
15
15
  basename: "newname.jpg",
16
16
  selectedItemPath: "dir1/file1.jpg",
17
17
  media: mediaMap,
@@ -30,7 +30,7 @@ it("should return false when a file with the same name already exists", () => {
30
30
  "dir1/file2.jpg": file2,
31
31
  };
32
32
 
33
- const result = validateMediaItemRename({
33
+ const result = mediaValidateMediaItemRename({
34
34
  basename: "file2.jpg",
35
35
  selectedItemPath: "dir1/file1.jpg",
36
36
  media: mediaMap,
@@ -47,7 +47,7 @@ it("should return false when renaming a file to its own name", () => {
47
47
  "dir1/file1.jpg": file1,
48
48
  };
49
49
 
50
- const result = validateMediaItemRename({
50
+ const result = mediaValidateMediaItemRename({
51
51
  basename: "file1.jpg",
52
52
  selectedItemPath: "dir1/file1.jpg",
53
53
  media: mediaMap,
@@ -64,7 +64,7 @@ it("should return true when renaming a folder with no conflicts", () => {
64
64
  "dir1/dir2": dir2,
65
65
  };
66
66
 
67
- const result = validateMediaItemRename({
67
+ const result = mediaValidateMediaItemRename({
68
68
  basename: "newdir",
69
69
  selectedItemPath: "dir1/dir2",
70
70
  media: mediaMap,
@@ -83,7 +83,7 @@ it("should return false when a folder with the same name already exists", () =>
83
83
  "dir1/dir3": dir3,
84
84
  };
85
85
 
86
- const result = validateMediaItemRename({
86
+ const result = mediaValidateMediaItemRename({
87
87
  basename: "dir3",
88
88
  selectedItemPath: "dir1/dir2",
89
89
  media: mediaMap,
@@ -100,7 +100,7 @@ it("should handle special characters in basename", () => {
100
100
  "dir1/file1.jpg": file1,
101
101
  };
102
102
 
103
- const result = validateMediaItemRename({
103
+ const result = mediaValidateMediaItemRename({
104
104
  basename: "new-file@name.jpg",
105
105
  selectedItemPath: "dir1/file1.jpg",
106
106
  media: mediaMap,
@@ -121,7 +121,7 @@ it("should return true when renaming to a name that exists in a different direct
121
121
  "dir2/file2.jpg": file2,
122
122
  };
123
123
 
124
- const result = validateMediaItemRename({
124
+ const result = mediaValidateMediaItemRename({
125
125
  basename: "file2.jpg",
126
126
  selectedItemPath: "dir1/file1.jpg",
127
127
  media: mediaMap,
@@ -138,7 +138,7 @@ it("should return false when trying to rename to an empty basename", () => {
138
138
  "dir1/file1.jpg": file1,
139
139
  };
140
140
 
141
- const result = validateMediaItemRename({
141
+ const result = mediaValidateMediaItemRename({
142
142
  basename: "",
143
143
  selectedItemPath: "dir1/file1.jpg",
144
144
  media: mediaMap,
@@ -159,7 +159,7 @@ it("should handle deep nested directories", () => {
159
159
  "dir1/dir2/dir3/file1.jpg": file1,
160
160
  };
161
161
 
162
- const result = validateMediaItemRename({
162
+ const result = mediaValidateMediaItemRename({
163
163
  basename: "renamed.jpg",
164
164
  selectedItemPath: "dir1/dir2/dir3/file1.jpg",
165
165
  media: mediaMap,
@@ -189,7 +189,7 @@ it("should return false when basename contains invalid filesystem characters", (
189
189
  ];
190
190
 
191
191
  invalidNames.forEach((basename) => {
192
- const result = validateMediaItemRename({
192
+ const result = mediaValidateMediaItemRename({
193
193
  basename,
194
194
  selectedItemPath: "dir1/file1.jpg",
195
195
  media: mediaMap,
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import "./index.css";
2
- export * from "./formatByteSize";
3
2
  export * from "./MediaCollection";
3
+ export * from "./ResourceExplorer";
4
4
  export * from "./utils/files";
5
5
  export * from "./utils/mediaItemTree";
6
+ export * from "./utils/resourceUtils";
@@ -6,27 +6,28 @@ import {
6
6
  MediaItemTree,
7
7
  rootMediaItem,
8
8
  } from "./mediaItemTree";
9
+ import { basenameValidator } from "./resourceUtils";
9
10
 
10
- export type FileKindFilter = "assets" | "directories" | "all";
11
- export type ExpandedMap = Record<string, boolean | undefined>;
11
+ export type MediaFileKindFilter = "assets" | "directories" | "all";
12
+ export type MediaExpandedMap = Record<string, boolean | undefined>;
12
13
 
13
- export type GetVisibleItemsOptions = {
14
- expandedMap: ExpandedMap;
15
- fileKindFilter: FileKindFilter;
14
+ export type MediaGetVisibleItemsOptions = {
15
+ expandedMap: MediaExpandedMap;
16
+ fileKindFilter: MediaFileKindFilter;
16
17
  rootItemId: string;
17
18
  tree: MediaItemTree;
18
19
  showAllDescendants: boolean;
19
20
  showRootItem: boolean;
20
21
  };
21
22
 
22
- export const getVisibleItems = ({
23
+ export const mediaGetVisibleItems = ({
23
24
  expandedMap,
24
25
  fileKindFilter,
25
26
  rootItemId,
26
27
  tree,
27
28
  showAllDescendants,
28
29
  showRootItem,
29
- }: GetVisibleItemsOptions) => {
30
+ }: MediaGetVisibleItemsOptions) => {
30
31
  const filteredItems: MediaItem[] = [];
31
32
  const relativeRootItem =
32
33
  tree.find(rootMediaItem, (item) => item.id === rootItemId) ?? rootMediaItem;
@@ -38,6 +39,9 @@ export const getVisibleItems = ({
38
39
  }
39
40
  return;
40
41
  }
42
+ if (item.kind === "noyaFile" && fileKindFilter === "all") {
43
+ filteredItems.push(item);
44
+ }
41
45
  if (item.kind === "file" && fileKindFilter === "all") {
42
46
  filteredItems.push(item);
43
47
  }
@@ -58,23 +62,7 @@ export const getVisibleItems = ({
58
62
  return filteredItems;
59
63
  };
60
64
 
61
- /**
62
- * Validates if a basename follows typical file naming conventions
63
- * Disallows:
64
- * - Path separators (/ and \)
65
- * - Characters typically invalid in filesystems (<, >, :, ", |, ?, *)
66
- * - Empty names
67
- */
68
- export const basenameValidator = (basename: string): boolean => {
69
- // Check for empty string
70
- if (!basename || basename.trim() === "") return false;
71
-
72
- // Simple regex matching common invalid filesystem characters
73
- const invalidCharsRegex = /[/\\<>:"|?*]/;
74
- return !invalidCharsRegex.test(basename);
75
- };
76
-
77
- export const validateMediaItemRename = ({
65
+ export const mediaValidateMediaItemRename = ({
78
66
  basename,
79
67
  selectedItemPath,
80
68
  media,
@@ -91,7 +79,7 @@ export const validateMediaItemRename = ({
91
79
  return true;
92
80
  };
93
81
 
94
- export const movePathsIntoTarget = ({
82
+ export const mediaMovePathsIntoTarget = ({
95
83
  media,
96
84
  sourceItemPaths,
97
85
  targetItemPath,
@@ -123,7 +111,7 @@ export const movePathsIntoTarget = ({
123
111
  ancestorPath,
124
112
  newAncestorPath
125
113
  );
126
- const newPathIsValid = validateMediaItemRename({
114
+ const newPathIsValid = mediaValidateMediaItemRename({
127
115
  basename: path.basename(descendantPath),
128
116
  selectedItemPath: newDescendantPath,
129
117
  media,
@@ -137,7 +125,7 @@ export const movePathsIntoTarget = ({
137
125
  return mediaClone;
138
126
  };
139
127
 
140
- export const moveUpAFolder = ({
128
+ export const mediaMoveUpAFolder = ({
141
129
  tree,
142
130
  media,
143
131
  selectedIds,
@@ -163,7 +151,7 @@ export const moveUpAFolder = ({
163
151
  const sourceItemPaths = selectedIds
164
152
  .map((id) => tree.idToPathMap.get(id))
165
153
  .filter((path): path is string => Boolean(path));
166
- return movePathsIntoTarget({
154
+ return mediaMovePathsIntoTarget({
167
155
  media,
168
156
  targetItemPath: grandparentFolderPath,
169
157
  sourceItemPaths,
@@ -171,7 +159,7 @@ export const moveUpAFolder = ({
171
159
  });
172
160
  };
173
161
 
174
- export const getDepthMap = (
162
+ export const mediaGetDepthMap = (
175
163
  item: MediaItem,
176
164
  tree: MediaItemTree,
177
165
  showAllDescendants: boolean
@@ -187,7 +175,7 @@ export const getDepthMap = (
187
175
  return depthMap;
188
176
  };
189
177
 
190
- export const updateExpandedMap = ({
178
+ export const mediaUpdateExpandedMap = ({
191
179
  item,
192
180
  expanded,
193
181
  expandable,
@@ -197,9 +185,9 @@ export const updateExpandedMap = ({
197
185
  item: MediaItem;
198
186
  expanded: boolean;
199
187
  expandable: boolean;
200
- expandedMap: ExpandedMap;
188
+ expandedMap: MediaExpandedMap;
201
189
  tree: MediaItemTree;
202
- }): ExpandedMap => {
190
+ }): MediaExpandedMap => {
203
191
  const newExpandedMap = { ...expandedMap };
204
192
  const inner = (item: MediaItem, expanded: boolean) => {
205
193
  if (!expandable) return {};
@@ -216,7 +204,7 @@ export const updateExpandedMap = ({
216
204
  return newExpandedMap;
217
205
  };
218
206
 
219
- export const deleteMediaItems = ({
207
+ export const mediaDeleteMediaItems = ({
220
208
  selectedIds,
221
209
  media,
222
210
  tree,
@@ -241,7 +229,7 @@ export const deleteMediaItems = ({
241
229
  );
242
230
  };
243
231
 
244
- export const moveMediaInsideFolder = ({
232
+ export const mediaMoveMediaInsideFolder = ({
245
233
  sourceItemIds,
246
234
  targetItemId,
247
235
  media,
@@ -259,7 +247,7 @@ export const moveMediaInsideFolder = ({
259
247
  .map((id) => tree.idToPathMap.get(id))
260
248
  .filter((path): path is string => Boolean(path));
261
249
 
262
- return movePathsIntoTarget({
250
+ return mediaMovePathsIntoTarget({
263
251
  media,
264
252
  sourceItemPaths,
265
253
  targetItemPath,
@@ -267,7 +255,10 @@ export const moveMediaInsideFolder = ({
267
255
  });
268
256
  };
269
257
 
270
- export const getParentDirectories = (mediaMap: MediaMap, folderId: string) => {
258
+ export const mediaGetParentDirectories = (
259
+ mediaMap: MediaMap,
260
+ folderId: string
261
+ ) => {
271
262
  const tree = createMediaItemTree(mediaMap);
272
263
 
273
264
  const indexPath = tree.findPath(
@@ -280,7 +271,7 @@ export const getParentDirectories = (mediaMap: MediaMap, folderId: string) => {
280
271
  return tree.accessPath(rootMediaItem, indexPath);
281
272
  };
282
273
 
283
- export const renameMediaItemAndDescendantPaths = ({
274
+ export const mediaRenameMediaItemAndDescendantPaths = ({
284
275
  newName,
285
276
  selectedItemPath,
286
277
  media,
@@ -0,0 +1,165 @@
1
+ import {
2
+ createAssetResource,
3
+ createDirectoryResource,
4
+ Resource,
5
+ ResourceCreate,
6
+ ResourceCreateMap,
7
+ ResourceMap,
8
+ } from "@noya-app/noya-schemas";
9
+ import { Base64, uuid } from "@noya-app/noya-utils";
10
+ import { path } from "imfs";
11
+
12
+ function isDirectoryEntry(
13
+ entry: FileSystemEntry
14
+ ): entry is FileSystemDirectoryEntry {
15
+ return entry.isDirectory;
16
+ }
17
+
18
+ function isFileEntry(entry: FileSystemEntry): entry is FileSystemFileEntry {
19
+ return entry.isFile;
20
+ }
21
+
22
+ export async function handleDataTransfer({
23
+ accessibleByFileId,
24
+ dataTransfer,
25
+ rootItemPath,
26
+ resourceMap,
27
+ }: {
28
+ accessibleByFileId: string;
29
+ dataTransfer: DataTransfer;
30
+ rootItemPath: string;
31
+ resourceMap: ResourceMap;
32
+ }): Promise<ResourceCreateMap | undefined> {
33
+ try {
34
+ const dataTransferItems = Array.from(dataTransfer.items ?? []);
35
+
36
+ const supportsEntries = dataTransferItems.some(
37
+ (item) => typeof item?.webkitGetAsEntry === "function"
38
+ );
39
+
40
+ type DroppedFile = { file: File; relativePath: string };
41
+
42
+ const collectFromEntry = async (
43
+ entry: FileSystemEntry,
44
+ basePath: string
45
+ ): Promise<DroppedFile[]> => {
46
+ if (!entry) return [];
47
+ if (isFileEntry(entry)) {
48
+ const file: File = await new Promise((resolve) =>
49
+ (entry as FileSystemFileEntry).file((f: File) => resolve(f))
50
+ );
51
+ return [
52
+ {
53
+ file,
54
+ relativePath: path.join(basePath, file.name),
55
+ },
56
+ ];
57
+ }
58
+ if (isDirectoryEntry(entry)) {
59
+ const reader = entry.createReader();
60
+ const readAll = async (): Promise<FileSystemEntry[]> => {
61
+ const all: FileSystemEntry[] = [];
62
+ // readEntries may return partial results; loop until empty
63
+ // eslint-disable-next-line no-constant-condition
64
+ while (true) {
65
+ const entries: FileSystemEntry[] = await new Promise((resolve) =>
66
+ reader.readEntries((ents: FileSystemEntry[]) => resolve(ents))
67
+ );
68
+ if (!entries.length) break;
69
+ all.push(...entries);
70
+ }
71
+ return all;
72
+ };
73
+ const children = await readAll();
74
+ const results = await Promise.all(
75
+ children.map((child) =>
76
+ collectFromEntry(child, path.join(basePath, entry.name))
77
+ )
78
+ );
79
+ return results.flat();
80
+ }
81
+ return [];
82
+ };
83
+
84
+ let dropped: DroppedFile[] = [];
85
+
86
+ if (supportsEntries) {
87
+ const topLevelEntries = dataTransferItems.flatMap((item) => {
88
+ const entry = item.webkitGetAsEntry?.();
89
+ if (!entry) return [];
90
+ return [entry];
91
+ });
92
+ const nested = await Promise.all(
93
+ topLevelEntries.map((entry) => collectFromEntry(entry, ""))
94
+ );
95
+ dropped = nested.flat();
96
+ } else {
97
+ const files = Array.from(dataTransfer.files);
98
+ if (files.length === 0) return;
99
+ dropped = files.map((file) => ({
100
+ file,
101
+ // Best effort: try webkitRelativePath; fall back to name
102
+ relativePath:
103
+ file.webkitRelativePath && file.webkitRelativePath.length > 0
104
+ ? file.webkitRelativePath
105
+ : file.name,
106
+ }));
107
+ }
108
+
109
+ if (dropped.length === 0) return;
110
+
111
+ const folderRelativePaths = new Set<string>();
112
+ for (const { relativePath } of dropped) {
113
+ const dir = path.dirname(relativePath);
114
+ if (dir && dir !== ".") {
115
+ const parts = dir.split("/").filter(Boolean);
116
+ let acc = "";
117
+ for (const part of parts) {
118
+ acc = acc ? path.join(acc, part) : part;
119
+ folderRelativePaths.add(acc);
120
+ }
121
+ }
122
+ }
123
+
124
+ const folderEntries: [string, Resource][] = Array.from(folderRelativePaths)
125
+ .map((rel) => path.join(rootItemPath, rel))
126
+ .filter((full) => !resourceMap[full])
127
+ .map((full) => [
128
+ full,
129
+ createDirectoryResource({
130
+ id: uuid(),
131
+ path: full,
132
+ accessibleByFileId,
133
+ stableId: uuid(),
134
+ }) as Resource,
135
+ ]);
136
+
137
+ const uploadResults = await Promise.all(
138
+ dropped.map(
139
+ async ({ file, relativePath }): Promise<[string, ResourceCreate]> => {
140
+ return [
141
+ path.join(rootItemPath, relativePath),
142
+ createAssetResource({
143
+ id: uuid(),
144
+ asset: {
145
+ content: Base64.encode(await file.arrayBuffer()),
146
+ contentType: file.type,
147
+ encoding: "base64",
148
+ },
149
+ path: path.join(rootItemPath, relativePath),
150
+ accessibleByFileId,
151
+ stableId: uuid(),
152
+ }) as Resource,
153
+ ];
154
+ }
155
+ )
156
+ );
157
+
158
+ return {
159
+ ...resourceMap,
160
+ ...Object.fromEntries([...folderEntries, ...uploadResults]),
161
+ };
162
+ } catch (error) {
163
+ console.error("Failed to upload dropped files:", error);
164
+ }
165
+ }