@noya-app/noya-file-explorer 0.0.16 → 0.0.18
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 +13 -13
- package/CHANGELOG.md +23 -0
- package/dist/index.css +853 -894
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +624 -33
- package/dist/index.d.ts +624 -33
- package/dist/index.js +1346 -265
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1384 -262
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -7
- package/src/MediaCollection.tsx +74 -53
- package/src/ResourceExplorer.tsx +1141 -0
- package/src/__tests__/deleteMediaItems.test.ts +7 -7
- package/src/__tests__/getDepthMap.test.ts +7 -7
- package/src/__tests__/getParentDirectories.test.ts +6 -6
- package/src/__tests__/getVisibleItems.test.ts +9 -9
- package/src/__tests__/moveMediaInsideFolder.test.ts +11 -11
- package/src/__tests__/movePathsIntoTarget.test.ts +9 -9
- package/src/__tests__/moveUpAFolder.test.ts +7 -7
- package/src/__tests__/renameMediaItemAndDescendantPaths.test.ts +6 -6
- package/src/__tests__/updateExpandedMap.test.ts +8 -8
- package/src/__tests__/validateMediaItemRename.test.ts +11 -11
- package/src/index.ts +2 -0
- package/src/utils/files.ts +25 -37
- package/src/utils/handleFileDrop.ts +38 -15
- package/src/utils/resourceUtils.ts +329 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MediaMap } from "@noya-app/noya-schemas";
|
|
2
2
|
import { expect, it } from "bun:test";
|
|
3
|
-
import {
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
192
|
+
const result = mediaValidateMediaItemRename({
|
|
193
193
|
basename,
|
|
194
194
|
selectedItemPath: "dir1/file1.jpg",
|
|
195
195
|
media: mediaMap,
|
package/src/index.ts
CHANGED
package/src/utils/files.ts
CHANGED
|
@@ -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
|
|
11
|
-
export type
|
|
11
|
+
export type MediaFileKindFilter = "assets" | "directories" | "all";
|
|
12
|
+
export type MediaExpandedMap = Record<string, boolean | undefined>;
|
|
12
13
|
|
|
13
|
-
export type
|
|
14
|
-
expandedMap:
|
|
15
|
-
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
|
|
23
|
+
export const mediaGetVisibleItems = ({
|
|
23
24
|
expandedMap,
|
|
24
25
|
fileKindFilter,
|
|
25
26
|
rootItemId,
|
|
26
27
|
tree,
|
|
27
28
|
showAllDescendants,
|
|
28
29
|
showRootItem,
|
|
29
|
-
}:
|
|
30
|
+
}: MediaGetVisibleItemsOptions) => {
|
|
30
31
|
const filteredItems: MediaItem[] = [];
|
|
31
32
|
const relativeRootItem =
|
|
32
33
|
tree.find(rootMediaItem, (item) => item.id === rootItemId) ?? rootMediaItem;
|
|
@@ -61,23 +62,7 @@ export const getVisibleItems = ({
|
|
|
61
62
|
return filteredItems;
|
|
62
63
|
};
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
* Validates if a basename follows typical file naming conventions
|
|
66
|
-
* Disallows:
|
|
67
|
-
* - Path separators (/ and \)
|
|
68
|
-
* - Characters typically invalid in filesystems (<, >, :, ", |, ?, *)
|
|
69
|
-
* - Empty names
|
|
70
|
-
*/
|
|
71
|
-
export const basenameValidator = (basename: string): boolean => {
|
|
72
|
-
// Check for empty string
|
|
73
|
-
if (!basename || basename.trim() === "") return false;
|
|
74
|
-
|
|
75
|
-
// Simple regex matching common invalid filesystem characters
|
|
76
|
-
const invalidCharsRegex = /[/\\<>:"|?*]/;
|
|
77
|
-
return !invalidCharsRegex.test(basename);
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export const validateMediaItemRename = ({
|
|
65
|
+
export const mediaValidateMediaItemRename = ({
|
|
81
66
|
basename,
|
|
82
67
|
selectedItemPath,
|
|
83
68
|
media,
|
|
@@ -94,7 +79,7 @@ export const validateMediaItemRename = ({
|
|
|
94
79
|
return true;
|
|
95
80
|
};
|
|
96
81
|
|
|
97
|
-
export const
|
|
82
|
+
export const mediaMovePathsIntoTarget = ({
|
|
98
83
|
media,
|
|
99
84
|
sourceItemPaths,
|
|
100
85
|
targetItemPath,
|
|
@@ -126,7 +111,7 @@ export const movePathsIntoTarget = ({
|
|
|
126
111
|
ancestorPath,
|
|
127
112
|
newAncestorPath
|
|
128
113
|
);
|
|
129
|
-
const newPathIsValid =
|
|
114
|
+
const newPathIsValid = mediaValidateMediaItemRename({
|
|
130
115
|
basename: path.basename(descendantPath),
|
|
131
116
|
selectedItemPath: newDescendantPath,
|
|
132
117
|
media,
|
|
@@ -140,7 +125,7 @@ export const movePathsIntoTarget = ({
|
|
|
140
125
|
return mediaClone;
|
|
141
126
|
};
|
|
142
127
|
|
|
143
|
-
export const
|
|
128
|
+
export const mediaMoveUpAFolder = ({
|
|
144
129
|
tree,
|
|
145
130
|
media,
|
|
146
131
|
selectedIds,
|
|
@@ -166,7 +151,7 @@ export const moveUpAFolder = ({
|
|
|
166
151
|
const sourceItemPaths = selectedIds
|
|
167
152
|
.map((id) => tree.idToPathMap.get(id))
|
|
168
153
|
.filter((path): path is string => Boolean(path));
|
|
169
|
-
return
|
|
154
|
+
return mediaMovePathsIntoTarget({
|
|
170
155
|
media,
|
|
171
156
|
targetItemPath: grandparentFolderPath,
|
|
172
157
|
sourceItemPaths,
|
|
@@ -174,7 +159,7 @@ export const moveUpAFolder = ({
|
|
|
174
159
|
});
|
|
175
160
|
};
|
|
176
161
|
|
|
177
|
-
export const
|
|
162
|
+
export const mediaGetDepthMap = (
|
|
178
163
|
item: MediaItem,
|
|
179
164
|
tree: MediaItemTree,
|
|
180
165
|
showAllDescendants: boolean
|
|
@@ -190,7 +175,7 @@ export const getDepthMap = (
|
|
|
190
175
|
return depthMap;
|
|
191
176
|
};
|
|
192
177
|
|
|
193
|
-
export const
|
|
178
|
+
export const mediaUpdateExpandedMap = ({
|
|
194
179
|
item,
|
|
195
180
|
expanded,
|
|
196
181
|
expandable,
|
|
@@ -200,9 +185,9 @@ export const updateExpandedMap = ({
|
|
|
200
185
|
item: MediaItem;
|
|
201
186
|
expanded: boolean;
|
|
202
187
|
expandable: boolean;
|
|
203
|
-
expandedMap:
|
|
188
|
+
expandedMap: MediaExpandedMap;
|
|
204
189
|
tree: MediaItemTree;
|
|
205
|
-
}):
|
|
190
|
+
}): MediaExpandedMap => {
|
|
206
191
|
const newExpandedMap = { ...expandedMap };
|
|
207
192
|
const inner = (item: MediaItem, expanded: boolean) => {
|
|
208
193
|
if (!expandable) return {};
|
|
@@ -219,7 +204,7 @@ export const updateExpandedMap = ({
|
|
|
219
204
|
return newExpandedMap;
|
|
220
205
|
};
|
|
221
206
|
|
|
222
|
-
export const
|
|
207
|
+
export const mediaDeleteMediaItems = ({
|
|
223
208
|
selectedIds,
|
|
224
209
|
media,
|
|
225
210
|
tree,
|
|
@@ -244,7 +229,7 @@ export const deleteMediaItems = ({
|
|
|
244
229
|
);
|
|
245
230
|
};
|
|
246
231
|
|
|
247
|
-
export const
|
|
232
|
+
export const mediaMoveMediaInsideFolder = ({
|
|
248
233
|
sourceItemIds,
|
|
249
234
|
targetItemId,
|
|
250
235
|
media,
|
|
@@ -262,7 +247,7 @@ export const moveMediaInsideFolder = ({
|
|
|
262
247
|
.map((id) => tree.idToPathMap.get(id))
|
|
263
248
|
.filter((path): path is string => Boolean(path));
|
|
264
249
|
|
|
265
|
-
return
|
|
250
|
+
return mediaMovePathsIntoTarget({
|
|
266
251
|
media,
|
|
267
252
|
sourceItemPaths,
|
|
268
253
|
targetItemPath,
|
|
@@ -270,7 +255,10 @@ export const moveMediaInsideFolder = ({
|
|
|
270
255
|
});
|
|
271
256
|
};
|
|
272
257
|
|
|
273
|
-
export const
|
|
258
|
+
export const mediaGetParentDirectories = (
|
|
259
|
+
mediaMap: MediaMap,
|
|
260
|
+
folderId: string
|
|
261
|
+
) => {
|
|
274
262
|
const tree = createMediaItemTree(mediaMap);
|
|
275
263
|
|
|
276
264
|
const indexPath = tree.findPath(
|
|
@@ -283,7 +271,7 @@ export const getParentDirectories = (mediaMap: MediaMap, folderId: string) => {
|
|
|
283
271
|
return tree.accessPath(rootMediaItem, indexPath);
|
|
284
272
|
};
|
|
285
273
|
|
|
286
|
-
export const
|
|
274
|
+
export const mediaRenameMediaItemAndDescendantPaths = ({
|
|
287
275
|
newName,
|
|
288
276
|
selectedItemPath,
|
|
289
277
|
media,
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import {
|
|
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";
|
|
2
10
|
import { path } from "imfs";
|
|
3
|
-
import { createMediaAsset, createMediaFolder } from "./mediaItemTree";
|
|
4
11
|
|
|
5
12
|
function isDirectoryEntry(
|
|
6
13
|
entry: FileSystemEntry
|
|
@@ -13,16 +20,16 @@ function isFileEntry(entry: FileSystemEntry): entry is FileSystemFileEntry {
|
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
export async function handleDataTransfer({
|
|
23
|
+
accessibleByFileId,
|
|
16
24
|
dataTransfer,
|
|
17
25
|
rootItemPath,
|
|
18
|
-
|
|
19
|
-
uploadAsset,
|
|
26
|
+
resourceMap,
|
|
20
27
|
}: {
|
|
28
|
+
accessibleByFileId: string;
|
|
21
29
|
dataTransfer: DataTransfer;
|
|
22
30
|
rootItemPath: string;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}): Promise<MediaMap | undefined> {
|
|
31
|
+
resourceMap: ResourceMap;
|
|
32
|
+
}): Promise<ResourceCreateMap | undefined> {
|
|
26
33
|
try {
|
|
27
34
|
const dataTransferItems = Array.from(dataTransfer.items ?? []);
|
|
28
35
|
|
|
@@ -114,26 +121,42 @@ export async function handleDataTransfer({
|
|
|
114
121
|
}
|
|
115
122
|
}
|
|
116
123
|
|
|
117
|
-
const folderEntries: [string,
|
|
124
|
+
const folderEntries: [string, Resource][] = Array.from(folderRelativePaths)
|
|
118
125
|
.map((rel) => path.join(rootItemPath, rel))
|
|
119
|
-
.filter((full) => !
|
|
120
|
-
.map((full) => [
|
|
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
|
+
]);
|
|
121
136
|
|
|
122
137
|
const uploadResults = await Promise.all(
|
|
123
138
|
dropped.map(
|
|
124
|
-
async ({ file, relativePath }): Promise<[string,
|
|
125
|
-
const asset = await uploadAsset(file);
|
|
126
|
-
|
|
139
|
+
async ({ file, relativePath }): Promise<[string, ResourceCreate]> => {
|
|
127
140
|
return [
|
|
128
141
|
path.join(rootItemPath, relativePath),
|
|
129
|
-
|
|
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,
|
|
130
153
|
];
|
|
131
154
|
}
|
|
132
155
|
)
|
|
133
156
|
);
|
|
134
157
|
|
|
135
158
|
return {
|
|
136
|
-
...
|
|
159
|
+
...resourceMap,
|
|
137
160
|
...Object.fromEntries([...folderEntries, ...uploadResults]),
|
|
138
161
|
};
|
|
139
162
|
} catch (error) {
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createResourceTree,
|
|
3
|
+
Resource,
|
|
4
|
+
ResourceMap,
|
|
5
|
+
ResourceTree,
|
|
6
|
+
rootResource,
|
|
7
|
+
} from "@noya-app/noya-schemas";
|
|
8
|
+
import { path } from "imfs";
|
|
9
|
+
import { ancestorPaths } from "tree-visit";
|
|
10
|
+
|
|
11
|
+
export type FileKindFilter = "assets" | "directories" | "resources" | "all";
|
|
12
|
+
export type ExpandedMap = Record<string, boolean | undefined>;
|
|
13
|
+
|
|
14
|
+
export type GetVisibleItemsOptions = {
|
|
15
|
+
expandedMap: ExpandedMap;
|
|
16
|
+
fileKindFilter: FileKindFilter;
|
|
17
|
+
rootItemId: string;
|
|
18
|
+
tree: ResourceTree;
|
|
19
|
+
showAllDescendants: boolean;
|
|
20
|
+
showRootItem: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const getVisibleItems = ({
|
|
24
|
+
expandedMap,
|
|
25
|
+
fileKindFilter,
|
|
26
|
+
rootItemId,
|
|
27
|
+
tree,
|
|
28
|
+
showAllDescendants,
|
|
29
|
+
showRootItem,
|
|
30
|
+
}: GetVisibleItemsOptions) => {
|
|
31
|
+
const filteredItems: Resource[] = [];
|
|
32
|
+
const relativeRootItem =
|
|
33
|
+
tree.find(rootResource, (item) => item.id === rootItemId) ?? rootResource;
|
|
34
|
+
|
|
35
|
+
tree.visit(relativeRootItem, (item) => {
|
|
36
|
+
if (relativeRootItem.id === item.id) {
|
|
37
|
+
if (showRootItem) {
|
|
38
|
+
filteredItems.push(item);
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (item.type === "file" && fileKindFilter === "all") {
|
|
43
|
+
filteredItems.push(item);
|
|
44
|
+
}
|
|
45
|
+
if (
|
|
46
|
+
item.type === "asset" &&
|
|
47
|
+
(fileKindFilter === "assets" || fileKindFilter === "all")
|
|
48
|
+
) {
|
|
49
|
+
filteredItems.push(item);
|
|
50
|
+
}
|
|
51
|
+
if (
|
|
52
|
+
item.type === "directory" &&
|
|
53
|
+
(fileKindFilter === "directories" || fileKindFilter === "all")
|
|
54
|
+
) {
|
|
55
|
+
filteredItems.push(item);
|
|
56
|
+
}
|
|
57
|
+
if (
|
|
58
|
+
item.type === "resource" &&
|
|
59
|
+
(fileKindFilter === "resources" || fileKindFilter === "all")
|
|
60
|
+
) {
|
|
61
|
+
filteredItems.push(item);
|
|
62
|
+
}
|
|
63
|
+
if (!expandedMap[item.id] || !showAllDescendants) return "skip";
|
|
64
|
+
});
|
|
65
|
+
return filteredItems;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validates if a basename follows typical file naming conventions
|
|
70
|
+
* Disallows:
|
|
71
|
+
* - Path separators (/ and \)
|
|
72
|
+
* - Characters typically invalid in filesystems (<, >, :, ", |, ?, *)
|
|
73
|
+
* - Empty names
|
|
74
|
+
*/
|
|
75
|
+
export const basenameValidator = (basename: string): boolean => {
|
|
76
|
+
// Check for empty string
|
|
77
|
+
if (!basename || basename.trim() === "") return false;
|
|
78
|
+
|
|
79
|
+
// Simple regex matching common invalid filesystem characters
|
|
80
|
+
const invalidCharsRegex = /[/\\<>:"|?*]/;
|
|
81
|
+
return !invalidCharsRegex.test(basename);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const validateResourceRename = ({
|
|
85
|
+
basename,
|
|
86
|
+
selectedItemPath,
|
|
87
|
+
media,
|
|
88
|
+
}: {
|
|
89
|
+
basename: string;
|
|
90
|
+
selectedItemPath: string;
|
|
91
|
+
media: ResourceMap;
|
|
92
|
+
}): boolean => {
|
|
93
|
+
if (!basenameValidator(basename)) return false;
|
|
94
|
+
const newItemPath = path.join(path.dirname(selectedItemPath), basename);
|
|
95
|
+
// traverse the tree and check if there is already a path with the new name
|
|
96
|
+
const newPathExists = media[newItemPath];
|
|
97
|
+
if (newPathExists) return false;
|
|
98
|
+
return true;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const movePathsIntoTarget = ({
|
|
102
|
+
media,
|
|
103
|
+
sourceItemPaths,
|
|
104
|
+
targetItemPath,
|
|
105
|
+
tree,
|
|
106
|
+
}: {
|
|
107
|
+
media: ResourceMap;
|
|
108
|
+
sourceItemPaths: string[];
|
|
109
|
+
targetItemPath: string;
|
|
110
|
+
tree: ResourceTree;
|
|
111
|
+
}) => {
|
|
112
|
+
const ancestors = ancestorPaths(
|
|
113
|
+
sourceItemPaths.map((path) => path.split("/"))
|
|
114
|
+
);
|
|
115
|
+
const mediaClone = { ...media };
|
|
116
|
+
for (const ancestor of ancestors) {
|
|
117
|
+
const ancestorPath = ancestor.join("/");
|
|
118
|
+
const ancestorItem = mediaClone[ancestorPath];
|
|
119
|
+
const newAncestorPath = path.join(
|
|
120
|
+
targetItemPath,
|
|
121
|
+
path.basename(ancestorPath)
|
|
122
|
+
);
|
|
123
|
+
if (!ancestorItem) continue;
|
|
124
|
+
const descendantPaths = tree
|
|
125
|
+
.flat(ancestorItem)
|
|
126
|
+
.map((item) => tree.idToPathMap.get(item.id));
|
|
127
|
+
for (const descendantPath of descendantPaths) {
|
|
128
|
+
if (!descendantPath) continue;
|
|
129
|
+
const newDescendantPath = descendantPath.replace(
|
|
130
|
+
ancestorPath,
|
|
131
|
+
newAncestorPath
|
|
132
|
+
);
|
|
133
|
+
const newPathIsValid = validateResourceRename({
|
|
134
|
+
basename: path.basename(descendantPath),
|
|
135
|
+
selectedItemPath: newDescendantPath,
|
|
136
|
+
media,
|
|
137
|
+
});
|
|
138
|
+
if (newPathIsValid) {
|
|
139
|
+
mediaClone[newDescendantPath] = mediaClone[descendantPath];
|
|
140
|
+
delete mediaClone[descendantPath];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return mediaClone;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const moveUpAFolder = ({
|
|
148
|
+
tree,
|
|
149
|
+
media,
|
|
150
|
+
selectedIds,
|
|
151
|
+
}: {
|
|
152
|
+
tree: ResourceTree;
|
|
153
|
+
media: ResourceMap;
|
|
154
|
+
selectedIds: string[];
|
|
155
|
+
}) => {
|
|
156
|
+
const indexPath = tree.findPath(
|
|
157
|
+
rootResource,
|
|
158
|
+
(item) => item.id === selectedIds[0]
|
|
159
|
+
);
|
|
160
|
+
if (!indexPath) return;
|
|
161
|
+
|
|
162
|
+
// Get the grandparent folder (where items will be moved to)
|
|
163
|
+
const grandparentFolder = tree.access(
|
|
164
|
+
rootResource,
|
|
165
|
+
indexPath.slice(0, indexPath.length - 2)
|
|
166
|
+
);
|
|
167
|
+
const grandparentFolderPath = tree.idToPathMap.get(grandparentFolder.id);
|
|
168
|
+
if (!grandparentFolderPath) return;
|
|
169
|
+
|
|
170
|
+
const sourceItemPaths = selectedIds
|
|
171
|
+
.map((id) => tree.idToPathMap.get(id))
|
|
172
|
+
.filter((path): path is string => Boolean(path));
|
|
173
|
+
return movePathsIntoTarget({
|
|
174
|
+
media,
|
|
175
|
+
targetItemPath: grandparentFolderPath,
|
|
176
|
+
sourceItemPaths,
|
|
177
|
+
tree,
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export const getDepthMap = (
|
|
182
|
+
item: Resource,
|
|
183
|
+
tree: ResourceTree,
|
|
184
|
+
showAllDescendants: boolean
|
|
185
|
+
): Record<string, number> => {
|
|
186
|
+
const depthMap: Record<string, number> = {};
|
|
187
|
+
tree.visit(item, (item, indexPath) => {
|
|
188
|
+
if (showAllDescendants) {
|
|
189
|
+
depthMap[item.id] = Math.max(indexPath.length - 1, 0);
|
|
190
|
+
} else {
|
|
191
|
+
depthMap[item.id] = 0;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
return depthMap;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export const updateExpandedMap = ({
|
|
198
|
+
item,
|
|
199
|
+
expanded,
|
|
200
|
+
expandable,
|
|
201
|
+
expandedMap,
|
|
202
|
+
tree,
|
|
203
|
+
}: {
|
|
204
|
+
item: Resource;
|
|
205
|
+
expanded: boolean;
|
|
206
|
+
expandable: boolean;
|
|
207
|
+
expandedMap: ExpandedMap;
|
|
208
|
+
tree: ResourceTree;
|
|
209
|
+
}): ExpandedMap => {
|
|
210
|
+
const newExpandedMap = { ...expandedMap };
|
|
211
|
+
const inner = (item: Resource, expanded: boolean) => {
|
|
212
|
+
if (!expandable) return {};
|
|
213
|
+
// Don't allow toggling the fake root
|
|
214
|
+
if (item.id === rootResource.id) return {};
|
|
215
|
+
if (!expanded) {
|
|
216
|
+
// recursively collapse all children
|
|
217
|
+
const children = tree.getChildren(item, []);
|
|
218
|
+
children.forEach((child) => inner(child, false));
|
|
219
|
+
}
|
|
220
|
+
newExpandedMap[item.id] = expanded;
|
|
221
|
+
};
|
|
222
|
+
inner(item, expanded);
|
|
223
|
+
return newExpandedMap;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export const deleteResources = ({
|
|
227
|
+
selectedIds,
|
|
228
|
+
media,
|
|
229
|
+
tree,
|
|
230
|
+
}: {
|
|
231
|
+
selectedIds: string[];
|
|
232
|
+
media: ResourceMap;
|
|
233
|
+
tree: ResourceTree;
|
|
234
|
+
}): ResourceMap => {
|
|
235
|
+
const itemsToDelete = selectedIds.flatMap((mediaItemId) => {
|
|
236
|
+
const mediaItem = tree.resourcesWithRoot.find(
|
|
237
|
+
(item) => item.id === mediaItemId
|
|
238
|
+
);
|
|
239
|
+
if (!mediaItem) return [];
|
|
240
|
+
return tree.flat(mediaItem);
|
|
241
|
+
});
|
|
242
|
+
const itemKeysToDelete = new Set(
|
|
243
|
+
itemsToDelete.map((item) => tree.idToPathMap.get(item.id))
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
return Object.fromEntries(
|
|
247
|
+
Object.entries(media).filter(([key]) => !itemKeysToDelete.has(key))
|
|
248
|
+
);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export const moveMediaInsideFolder = ({
|
|
252
|
+
sourceItemIds,
|
|
253
|
+
targetItemId,
|
|
254
|
+
media,
|
|
255
|
+
tree,
|
|
256
|
+
}: {
|
|
257
|
+
sourceItemIds: string[];
|
|
258
|
+
targetItemId: string;
|
|
259
|
+
media: ResourceMap;
|
|
260
|
+
tree: ResourceTree;
|
|
261
|
+
}) => {
|
|
262
|
+
const targetItemPath = tree.idToPathMap.get(targetItemId);
|
|
263
|
+
if (!targetItemPath) return media;
|
|
264
|
+
|
|
265
|
+
const sourceItemPaths = sourceItemIds
|
|
266
|
+
.map((id) => tree.idToPathMap.get(id))
|
|
267
|
+
.filter((path): path is string => Boolean(path));
|
|
268
|
+
|
|
269
|
+
return movePathsIntoTarget({
|
|
270
|
+
media,
|
|
271
|
+
sourceItemPaths,
|
|
272
|
+
targetItemPath,
|
|
273
|
+
tree,
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
export const getParentDirectories = (
|
|
278
|
+
resourceMap: ResourceMap,
|
|
279
|
+
folderId: string
|
|
280
|
+
) => {
|
|
281
|
+
const tree = createResourceTree(resourceMap);
|
|
282
|
+
|
|
283
|
+
const indexPath = tree.findPath(rootResource, (item) => item.id === folderId);
|
|
284
|
+
|
|
285
|
+
if (!indexPath) return [rootResource];
|
|
286
|
+
|
|
287
|
+
return tree.accessPath(rootResource, indexPath);
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
export const renameResourceAndDescendantPaths = ({
|
|
291
|
+
newName,
|
|
292
|
+
selectedItemPath,
|
|
293
|
+
media,
|
|
294
|
+
tree,
|
|
295
|
+
}: {
|
|
296
|
+
newName: string;
|
|
297
|
+
selectedItemPath: string;
|
|
298
|
+
media: ResourceMap;
|
|
299
|
+
tree: ResourceTree;
|
|
300
|
+
}) => {
|
|
301
|
+
const mediaClone = { ...media };
|
|
302
|
+
const selectedItem = mediaClone[selectedItemPath];
|
|
303
|
+
if (!selectedItem) return mediaClone;
|
|
304
|
+
|
|
305
|
+
const parentPath = path.dirname(selectedItemPath);
|
|
306
|
+
const newItemPath = path.join(parentPath, newName);
|
|
307
|
+
|
|
308
|
+
const descendants = tree
|
|
309
|
+
.flat(selectedItem)
|
|
310
|
+
.map((item) => tree.idToPathMap.get(item.id));
|
|
311
|
+
|
|
312
|
+
// Update all descendants' paths
|
|
313
|
+
for (const descendantPath of descendants) {
|
|
314
|
+
if (!descendantPath) continue;
|
|
315
|
+
|
|
316
|
+
// Use the same approach as movePathsIntoTarget
|
|
317
|
+
const newDescendantPath = descendantPath.replace(
|
|
318
|
+
selectedItemPath,
|
|
319
|
+
newItemPath
|
|
320
|
+
);
|
|
321
|
+
mediaClone[newDescendantPath] = {
|
|
322
|
+
...mediaClone[descendantPath],
|
|
323
|
+
path: newDescendantPath,
|
|
324
|
+
};
|
|
325
|
+
delete mediaClone[descendantPath];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return mediaClone;
|
|
329
|
+
};
|