@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.
@@ -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,4 +1,6 @@
1
1
  import "./index.css";
2
2
  export * from "./MediaCollection";
3
+ export * from "./ResourceExplorer";
3
4
  export * from "./utils/files";
4
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;
@@ -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 movePathsIntoTarget = ({
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 = validateMediaItemRename({
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 moveUpAFolder = ({
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 movePathsIntoTarget({
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 getDepthMap = (
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 updateExpandedMap = ({
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: ExpandedMap;
188
+ expandedMap: MediaExpandedMap;
204
189
  tree: MediaItemTree;
205
- }): ExpandedMap => {
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 deleteMediaItems = ({
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 moveMediaInsideFolder = ({
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 movePathsIntoTarget({
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 getParentDirectories = (mediaMap: MediaMap, folderId: string) => {
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 renameMediaItemAndDescendantPaths = ({
274
+ export const mediaRenameMediaItemAndDescendantPaths = ({
287
275
  newName,
288
276
  selectedItemPath,
289
277
  media,
@@ -1,6 +1,13 @@
1
- import { Asset, MediaItem, MediaMap } from "@noya-app/noya-schemas";
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
- media,
19
- uploadAsset,
26
+ resourceMap,
20
27
  }: {
28
+ accessibleByFileId: string;
21
29
  dataTransfer: DataTransfer;
22
30
  rootItemPath: string;
23
- media: MediaMap;
24
- uploadAsset: (file: File) => Promise<Asset>;
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, MediaItem][] = Array.from(folderRelativePaths)
124
+ const folderEntries: [string, Resource][] = Array.from(folderRelativePaths)
118
125
  .map((rel) => path.join(rootItemPath, rel))
119
- .filter((full) => !media[full])
120
- .map((full) => [full, createMediaFolder() as MediaItem]);
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, MediaItem]> => {
125
- const asset = await uploadAsset(file);
126
-
139
+ async ({ file, relativePath }): Promise<[string, ResourceCreate]> => {
127
140
  return [
128
141
  path.join(rootItemPath, relativePath),
129
- createMediaAsset({ assetId: asset.id }) as MediaItem,
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
- ...media,
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
+ };