@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.
@@ -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 treeWithTempItem = useMemo(
204
- () =>
205
- createMediaItemTree({
206
- ...media,
207
- ...(tempItem ? { [tempItem[0]]: tempItem[1] } : {}),
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: mediaWithTempItem,
328
+ media: temp.media,
329
329
  });
330
330
  if (!renameIsValid) {
331
331
  setTempItem(undefined);
332
332
  return;
333
333
  }
334
- const mediaClone = { ...media };
335
- delete mediaClone[selectedItemPath];
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
- [media, renamable, setMedia, tempItem, treeWithTempItem.idToPathMap]
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={gridSize}
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
+ });
@@ -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: ["react-virtualized", "@noya-app/noya-designsystem/index.css"],
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
  });