@noya-app/noya-file-explorer 0.0.2 → 0.0.3
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 +9 -0
- package/dist/index.css +22 -7
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +21 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +55 -25
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +54 -25
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/MediaCollection.tsx +33 -29
- package/src/__tests__/renameMediaItemAndDescendantPaths.test.ts +139 -0
- package/src/utils/files.ts +45 -0
- package/tsup.config.ts +1 -3
package/src/MediaCollection.tsx
CHANGED
|
@@ -71,12 +71,13 @@ import {
|
|
|
71
71
|
getVisibleItems,
|
|
72
72
|
moveMediaInsideFolder,
|
|
73
73
|
moveUpAFolder,
|
|
74
|
+
renameMediaItemAndDescendantPaths,
|
|
74
75
|
updateExpandedMap,
|
|
75
76
|
validateMediaItemRename,
|
|
76
77
|
} from "./utils/files";
|
|
77
78
|
|
|
78
79
|
const MediaThumbnailInternal = memoGeneric(
|
|
79
|
-
({ item, selected }: CollectionThumbnailProps<MediaItem>) => {
|
|
80
|
+
({ item, selected, size }: CollectionThumbnailProps<MediaItem>) => {
|
|
80
81
|
const asset = useAsset(item.kind === "asset" ? item.assetId : undefined);
|
|
81
82
|
const isRoot = item.id === rootMediaItem.id;
|
|
82
83
|
const isFolder = item.kind === "folder";
|
|
@@ -86,6 +87,7 @@ const MediaThumbnailInternal = memoGeneric(
|
|
|
86
87
|
iconName={isRoot ? "HomeIcon" : isFolder ? "FolderIcon" : undefined}
|
|
87
88
|
url={asset?.url}
|
|
88
89
|
selected={selected}
|
|
90
|
+
size={size}
|
|
89
91
|
/>
|
|
90
92
|
);
|
|
91
93
|
}
|
|
@@ -200,14 +202,24 @@ export const MediaCollection = memo(
|
|
|
200
202
|
const [tempItem, setTempItem] = useState<[string, MediaItem] | undefined>(
|
|
201
203
|
undefined
|
|
202
204
|
);
|
|
203
|
-
const
|
|
204
|
-
() =>
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}),
|
|
205
|
+
const mediaWithTempItem = useMemo(
|
|
206
|
+
() => ({
|
|
207
|
+
...media,
|
|
208
|
+
...(tempItem ? { [tempItem[0]]: tempItem[1] } : {}),
|
|
209
|
+
}),
|
|
209
210
|
[media, tempItem]
|
|
210
211
|
);
|
|
212
|
+
const treeWithTempItem = useMemo(
|
|
213
|
+
() => createMediaItemTree(mediaWithTempItem),
|
|
214
|
+
[mediaWithTempItem]
|
|
215
|
+
);
|
|
216
|
+
const temp = useMemo(
|
|
217
|
+
() => ({
|
|
218
|
+
media: mediaWithTempItem,
|
|
219
|
+
tree: treeWithTempItem,
|
|
220
|
+
}),
|
|
221
|
+
[mediaWithTempItem, treeWithTempItem]
|
|
222
|
+
);
|
|
211
223
|
const [selectedIds, setSelectedIds] = useControlledOrUncontrolled<string[]>(
|
|
212
224
|
{
|
|
213
225
|
defaultValue: [],
|
|
@@ -273,14 +285,6 @@ export const MediaCollection = memo(
|
|
|
273
285
|
return itemPath.startsWith(path.dirname(firstSelectedPath));
|
|
274
286
|
});
|
|
275
287
|
|
|
276
|
-
// Convert size prop to grid size if in grid view
|
|
277
|
-
const gridSize = useMemo(() => {
|
|
278
|
-
if (viewType === "grid") {
|
|
279
|
-
return size;
|
|
280
|
-
}
|
|
281
|
-
return "medium";
|
|
282
|
-
}, [viewType, size]);
|
|
283
|
-
|
|
284
288
|
useEffect(() => {
|
|
285
289
|
if (initialExpanded) {
|
|
286
290
|
setExpandedMap(initialExpanded);
|
|
@@ -318,31 +322,29 @@ export const MediaCollection = memo(
|
|
|
318
322
|
selectedItem.id
|
|
319
323
|
);
|
|
320
324
|
if (!selectedItemPath) return;
|
|
321
|
-
const mediaWithTempItem = {
|
|
322
|
-
...media,
|
|
323
|
-
...(tempItem ? { [tempItem[0]]: tempItem[1] } : {}),
|
|
324
|
-
};
|
|
325
325
|
const renameIsValid = validateMediaItemRename({
|
|
326
326
|
basename: newName,
|
|
327
327
|
selectedItemPath,
|
|
328
|
-
media:
|
|
328
|
+
media: temp.media,
|
|
329
329
|
});
|
|
330
330
|
if (!renameIsValid) {
|
|
331
331
|
setTempItem(undefined);
|
|
332
332
|
return;
|
|
333
333
|
}
|
|
334
|
-
const
|
|
335
|
-
|
|
334
|
+
const mediaWithRenamedDescendantPaths =
|
|
335
|
+
renameMediaItemAndDescendantPaths({
|
|
336
|
+
newName,
|
|
337
|
+
selectedItemPath,
|
|
338
|
+
media: temp.media,
|
|
339
|
+
tree: temp.tree,
|
|
340
|
+
});
|
|
336
341
|
setMedia(
|
|
337
342
|
{ name: "Rename media item", timestamp: Date.now() },
|
|
338
|
-
|
|
339
|
-
...mediaClone,
|
|
340
|
-
[path.join(path.dirname(selectedItemPath), newName)]: selectedItem,
|
|
341
|
-
}
|
|
343
|
+
mediaWithRenamedDescendantPaths
|
|
342
344
|
);
|
|
343
345
|
setTempItem(undefined);
|
|
344
346
|
},
|
|
345
|
-
[
|
|
347
|
+
[renamable, setMedia, temp.media, temp.tree, treeWithTempItem.idToPathMap]
|
|
346
348
|
);
|
|
347
349
|
|
|
348
350
|
const handleAddFolder = useCallback(
|
|
@@ -650,6 +652,7 @@ export const MediaCollection = memo(
|
|
|
650
652
|
onSelect={(action) => handleMenuAction(action, selectedMediaItems)}
|
|
651
653
|
selected={selected}
|
|
652
654
|
onOpenChange={onOpenChange}
|
|
655
|
+
variant={viewType === "grid" ? "normal" : "bare"}
|
|
653
656
|
style={{
|
|
654
657
|
backgroundColor: selected
|
|
655
658
|
? cssVars.colors.primaryPastel
|
|
@@ -662,6 +665,7 @@ export const MediaCollection = memo(
|
|
|
662
665
|
handleMenuAction,
|
|
663
666
|
renderActionProp,
|
|
664
667
|
selectedMediaItems,
|
|
668
|
+
viewType,
|
|
665
669
|
]);
|
|
666
670
|
|
|
667
671
|
useImperativeHandle(ref, () => ({
|
|
@@ -696,7 +700,7 @@ export const MediaCollection = memo(
|
|
|
696
700
|
scrollable={scrollable}
|
|
697
701
|
items={visibleItems}
|
|
698
702
|
viewType={viewType}
|
|
699
|
-
size={
|
|
703
|
+
size={size}
|
|
700
704
|
getId={(file) => file.id}
|
|
701
705
|
getName={(file) => {
|
|
702
706
|
if (file.id === tempItem?.[1].id) {
|
|
@@ -736,7 +740,7 @@ export const MediaCollection = memo(
|
|
|
736
740
|
const asset = assets.find((a) => a.id === file.assetId);
|
|
737
741
|
if (!asset) return null;
|
|
738
742
|
return (
|
|
739
|
-
<FileExplorerDetail selected={selected}>
|
|
743
|
+
<FileExplorerDetail selected={selected} size={size}>
|
|
740
744
|
{(asset.size / 1024).toFixed(1)}KB
|
|
741
745
|
</FileExplorerDetail>
|
|
742
746
|
);
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { MediaMap } from "@noya-app/noya-schemas";
|
|
2
|
+
import { expect, it } from "bun:test";
|
|
3
|
+
import { renameMediaItemAndDescendantPaths } from "../utils/files";
|
|
4
|
+
import {
|
|
5
|
+
createMediaAsset,
|
|
6
|
+
createMediaFolder,
|
|
7
|
+
createMediaItemTree,
|
|
8
|
+
} from "../utils/mediaItemTree";
|
|
9
|
+
|
|
10
|
+
it("should rename a folder with no children", () => {
|
|
11
|
+
const dir1 = createMediaFolder();
|
|
12
|
+
const media: MediaMap = { dir1 };
|
|
13
|
+
|
|
14
|
+
const tree = createMediaItemTree(media);
|
|
15
|
+
|
|
16
|
+
const result = renameMediaItemAndDescendantPaths({
|
|
17
|
+
newName: "renamed-dir",
|
|
18
|
+
selectedItemPath: "dir1",
|
|
19
|
+
media,
|
|
20
|
+
tree,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(result).toEqual({ "renamed-dir": dir1 });
|
|
24
|
+
expect(result).not.toHaveProperty("dir1");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should rename a folder and update paths of its immediate children", () => {
|
|
28
|
+
const dir1 = createMediaFolder();
|
|
29
|
+
const child1 = createMediaAsset({ assetId: "asset1" });
|
|
30
|
+
const child2 = createMediaFolder();
|
|
31
|
+
const media: MediaMap = {
|
|
32
|
+
dir1,
|
|
33
|
+
"dir1/child1": child1,
|
|
34
|
+
"dir1/child2": child2,
|
|
35
|
+
};
|
|
36
|
+
const tree = createMediaItemTree(media);
|
|
37
|
+
|
|
38
|
+
const result = renameMediaItemAndDescendantPaths({
|
|
39
|
+
newName: "renamed-dir",
|
|
40
|
+
selectedItemPath: "dir1",
|
|
41
|
+
media,
|
|
42
|
+
tree,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(result).toEqual({
|
|
46
|
+
"renamed-dir": dir1,
|
|
47
|
+
"renamed-dir/child1": child1,
|
|
48
|
+
"renamed-dir/child2": child2,
|
|
49
|
+
});
|
|
50
|
+
expect(result).not.toHaveProperty("dir1");
|
|
51
|
+
expect(result).not.toHaveProperty("dir1/child1");
|
|
52
|
+
expect(result).not.toHaveProperty("dir1/child2");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should rename a folder and update paths of all nested descendants", () => {
|
|
56
|
+
const dir1 = createMediaFolder();
|
|
57
|
+
const dir2 = createMediaFolder();
|
|
58
|
+
const dir3 = createMediaFolder();
|
|
59
|
+
const asset1 = createMediaAsset({ assetId: "asset1" });
|
|
60
|
+
const media: MediaMap = {
|
|
61
|
+
dir1,
|
|
62
|
+
"dir1/dir2": dir2,
|
|
63
|
+
"dir1/dir2/dir3": dir3,
|
|
64
|
+
"dir1/dir2/dir3/asset1": asset1,
|
|
65
|
+
};
|
|
66
|
+
const tree = createMediaItemTree(media);
|
|
67
|
+
|
|
68
|
+
const result = renameMediaItemAndDescendantPaths({
|
|
69
|
+
newName: "renamed-dir",
|
|
70
|
+
selectedItemPath: "dir1",
|
|
71
|
+
media,
|
|
72
|
+
tree,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(result).toEqual({
|
|
76
|
+
"renamed-dir": dir1,
|
|
77
|
+
"renamed-dir/dir2": dir2,
|
|
78
|
+
"renamed-dir/dir2/dir3": dir3,
|
|
79
|
+
"renamed-dir/dir2/dir3/asset1": asset1,
|
|
80
|
+
});
|
|
81
|
+
expect(result).not.toHaveProperty("dir1");
|
|
82
|
+
expect(result).not.toHaveProperty("dir1/dir2");
|
|
83
|
+
expect(result).not.toHaveProperty("dir1/dir2/dir3");
|
|
84
|
+
expect(result).not.toHaveProperty("dir1/dir2/dir3/asset1");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should rename a file (asset) correctly", () => {
|
|
88
|
+
const dir1 = createMediaFolder();
|
|
89
|
+
const asset1 = createMediaAsset({ assetId: "asset1" });
|
|
90
|
+
const asset2 = createMediaAsset({ assetId: "asset2" });
|
|
91
|
+
const media: MediaMap = {
|
|
92
|
+
dir1,
|
|
93
|
+
"dir1/asset1": asset1,
|
|
94
|
+
"dir1/asset2": asset2,
|
|
95
|
+
};
|
|
96
|
+
const tree = createMediaItemTree(media);
|
|
97
|
+
|
|
98
|
+
const result = renameMediaItemAndDescendantPaths({
|
|
99
|
+
newName: "renamed-asset.png",
|
|
100
|
+
selectedItemPath: "dir1/asset1",
|
|
101
|
+
media,
|
|
102
|
+
tree,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(result).toEqual({
|
|
106
|
+
dir1,
|
|
107
|
+
"dir1/renamed-asset.png": asset1,
|
|
108
|
+
"dir1/asset2": asset2,
|
|
109
|
+
});
|
|
110
|
+
expect(result).not.toHaveProperty("dir1/asset1");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should preserve other unrelated paths when renaming", () => {
|
|
114
|
+
const dir1 = createMediaFolder();
|
|
115
|
+
const dir2 = createMediaFolder();
|
|
116
|
+
const asset1 = createMediaAsset({ assetId: "asset1" });
|
|
117
|
+
const asset2 = createMediaAsset({ assetId: "asset2" });
|
|
118
|
+
const media: MediaMap = {
|
|
119
|
+
dir1,
|
|
120
|
+
dir2,
|
|
121
|
+
"dir1/asset1": asset1,
|
|
122
|
+
"dir2/asset2": asset2,
|
|
123
|
+
};
|
|
124
|
+
const tree = createMediaItemTree(media);
|
|
125
|
+
|
|
126
|
+
const result = renameMediaItemAndDescendantPaths({
|
|
127
|
+
newName: "renamed-dir",
|
|
128
|
+
selectedItemPath: "dir1",
|
|
129
|
+
media,
|
|
130
|
+
tree,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(result).toEqual({
|
|
134
|
+
"renamed-dir": dir1,
|
|
135
|
+
"renamed-dir/asset1": asset1,
|
|
136
|
+
dir2,
|
|
137
|
+
"dir2/asset2": asset2,
|
|
138
|
+
});
|
|
139
|
+
});
|
package/src/utils/files.ts
CHANGED
|
@@ -90,6 +90,7 @@ export const validateMediaItemRename = ({
|
|
|
90
90
|
if (newPathExists) return false;
|
|
91
91
|
return true;
|
|
92
92
|
};
|
|
93
|
+
|
|
93
94
|
export const movePathsIntoTarget = ({
|
|
94
95
|
media,
|
|
95
96
|
sourceItemPaths,
|
|
@@ -135,6 +136,7 @@ export const movePathsIntoTarget = ({
|
|
|
135
136
|
}
|
|
136
137
|
return mediaClone;
|
|
137
138
|
};
|
|
139
|
+
|
|
138
140
|
export const moveUpAFolder = ({
|
|
139
141
|
tree,
|
|
140
142
|
media,
|
|
@@ -168,6 +170,7 @@ export const moveUpAFolder = ({
|
|
|
168
170
|
tree,
|
|
169
171
|
});
|
|
170
172
|
};
|
|
173
|
+
|
|
171
174
|
export const getDepthMap = (
|
|
172
175
|
item: MediaItem,
|
|
173
176
|
tree: MediaItemTree,
|
|
@@ -183,6 +186,7 @@ export const getDepthMap = (
|
|
|
183
186
|
});
|
|
184
187
|
return depthMap;
|
|
185
188
|
};
|
|
189
|
+
|
|
186
190
|
export const updateExpandedMap = ({
|
|
187
191
|
item,
|
|
188
192
|
expanded,
|
|
@@ -211,6 +215,7 @@ export const updateExpandedMap = ({
|
|
|
211
215
|
inner(item, expanded);
|
|
212
216
|
return newExpandedMap;
|
|
213
217
|
};
|
|
218
|
+
|
|
214
219
|
export const deleteMediaItems = ({
|
|
215
220
|
selectedIds,
|
|
216
221
|
media,
|
|
@@ -235,6 +240,7 @@ export const deleteMediaItems = ({
|
|
|
235
240
|
Object.entries(media).filter(([key]) => !itemKeysToDelete.has(key))
|
|
236
241
|
);
|
|
237
242
|
};
|
|
243
|
+
|
|
238
244
|
export const moveMediaInsideFolder = ({
|
|
239
245
|
sourceItemIds,
|
|
240
246
|
targetItemId,
|
|
@@ -260,6 +266,7 @@ export const moveMediaInsideFolder = ({
|
|
|
260
266
|
tree,
|
|
261
267
|
});
|
|
262
268
|
};
|
|
269
|
+
|
|
263
270
|
export const getParentDirectories = (mediaMap: MediaMap, folderId: string) => {
|
|
264
271
|
const tree = createMediaItemTree(mediaMap);
|
|
265
272
|
|
|
@@ -272,3 +279,41 @@ export const getParentDirectories = (mediaMap: MediaMap, folderId: string) => {
|
|
|
272
279
|
|
|
273
280
|
return tree.accessPath(rootMediaItem, indexPath);
|
|
274
281
|
};
|
|
282
|
+
|
|
283
|
+
export const renameMediaItemAndDescendantPaths = ({
|
|
284
|
+
newName,
|
|
285
|
+
selectedItemPath,
|
|
286
|
+
media,
|
|
287
|
+
tree,
|
|
288
|
+
}: {
|
|
289
|
+
newName: string;
|
|
290
|
+
selectedItemPath: string;
|
|
291
|
+
media: MediaMap;
|
|
292
|
+
tree: MediaItemTree;
|
|
293
|
+
}) => {
|
|
294
|
+
const mediaClone = { ...media };
|
|
295
|
+
const selectedItem = mediaClone[selectedItemPath];
|
|
296
|
+
if (!selectedItem) return mediaClone;
|
|
297
|
+
|
|
298
|
+
const parentPath = path.dirname(selectedItemPath);
|
|
299
|
+
const newItemPath = path.join(parentPath, newName);
|
|
300
|
+
|
|
301
|
+
const descendants = tree
|
|
302
|
+
.flat(selectedItem)
|
|
303
|
+
.map((item) => tree.idToPathMap.get(item.id));
|
|
304
|
+
|
|
305
|
+
// Update all descendants' paths
|
|
306
|
+
for (const descendantPath of descendants) {
|
|
307
|
+
if (!descendantPath) continue;
|
|
308
|
+
|
|
309
|
+
// Use the same approach as movePathsIntoTarget
|
|
310
|
+
const newDescendantPath = descendantPath.replace(
|
|
311
|
+
selectedItemPath,
|
|
312
|
+
newItemPath
|
|
313
|
+
);
|
|
314
|
+
mediaClone[newDescendantPath] = mediaClone[descendantPath];
|
|
315
|
+
delete mediaClone[descendantPath];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return mediaClone;
|
|
319
|
+
};
|
package/tsup.config.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import fixReactVirtualized from "esbuild-plugin-react-virtualized";
|
|
2
1
|
import { defineConfig } from "tsup";
|
|
3
2
|
|
|
4
3
|
export default defineConfig({
|
|
@@ -6,8 +5,7 @@ export default defineConfig({
|
|
|
6
5
|
format: ["cjs", "esm"],
|
|
7
6
|
dts: true,
|
|
8
7
|
sourcemap: true,
|
|
9
|
-
noExternal: ["
|
|
10
|
-
esbuildPlugins: [fixReactVirtualized],
|
|
8
|
+
noExternal: ["@noya-app/noya-designsystem/index.css"],
|
|
11
9
|
// https://github.com/egoist/tsup/issues/619
|
|
12
10
|
splitting: false,
|
|
13
11
|
});
|