@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.
- package/.turbo/turbo-build.log +13 -13
- package/CHANGELOG.md +27 -0
- package/dist/index.css +916 -843
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +11207 -45
- package/dist/index.d.ts +11207 -45
- package/dist/index.js +1361 -160
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1399 -156
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -8
- package/src/MediaCollection.tsx +156 -97
- 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 -1
- package/src/utils/files.ts +28 -37
- package/src/utils/handleFileDrop.ts +165 -0
- package/src/utils/resourceUtils.ts +329 -0
- package/src/formatByteSize.ts +0 -8
|
@@ -0,0 +1,1141 @@
|
|
|
1
|
+
/* eslint-disable @shopify/prefer-early-return */
|
|
2
|
+
import {
|
|
3
|
+
ActionMenu,
|
|
4
|
+
Chip,
|
|
5
|
+
CollectionProps,
|
|
6
|
+
CollectionRef,
|
|
7
|
+
CollectionRenderActionProps,
|
|
8
|
+
CollectionThumbnailProps,
|
|
9
|
+
CollectionViewType,
|
|
10
|
+
createSectionedMenu,
|
|
11
|
+
cssVars,
|
|
12
|
+
FileExplorerCollection,
|
|
13
|
+
FileExplorerDetail,
|
|
14
|
+
FileExplorerEmptyState,
|
|
15
|
+
FileExplorerLayout,
|
|
16
|
+
FileExplorerUploadButton,
|
|
17
|
+
formatByteSize,
|
|
18
|
+
ListView,
|
|
19
|
+
MediaThumbnail,
|
|
20
|
+
MediaThumbnailProps,
|
|
21
|
+
RelativeDropPosition,
|
|
22
|
+
useOpenConfirmationDialog,
|
|
23
|
+
} from "@noya-app/noya-designsystem";
|
|
24
|
+
import {
|
|
25
|
+
DownloadIcon,
|
|
26
|
+
FolderIcon,
|
|
27
|
+
InputIcon,
|
|
28
|
+
OpenInNewWindowIcon,
|
|
29
|
+
ResetIcon,
|
|
30
|
+
TrashIcon,
|
|
31
|
+
UpdateIcon,
|
|
32
|
+
UploadIcon,
|
|
33
|
+
} from "@noya-app/noya-icons";
|
|
34
|
+
import {
|
|
35
|
+
MultiplayerPatchMetadata,
|
|
36
|
+
useAsset,
|
|
37
|
+
useAssetManager,
|
|
38
|
+
useAssets,
|
|
39
|
+
} from "@noya-app/noya-multiplayer-react";
|
|
40
|
+
import {
|
|
41
|
+
AssetResourceCreate,
|
|
42
|
+
createAssetResource,
|
|
43
|
+
createDirectoryResource,
|
|
44
|
+
createResourceTree,
|
|
45
|
+
diffResourceMaps,
|
|
46
|
+
PLACEHOLDER_ITEM_NAME,
|
|
47
|
+
Resource,
|
|
48
|
+
ResourceCreateMap,
|
|
49
|
+
ResourceMap,
|
|
50
|
+
ResourceTree,
|
|
51
|
+
rootResource,
|
|
52
|
+
rootResourceName,
|
|
53
|
+
} from "@noya-app/noya-schemas";
|
|
54
|
+
import { Base64, groupBy, isDeepEqual, uuid } from "@noya-app/noya-utils";
|
|
55
|
+
import {
|
|
56
|
+
AutoSizer,
|
|
57
|
+
downloadUrl,
|
|
58
|
+
memoGeneric,
|
|
59
|
+
useControlledOrUncontrolled,
|
|
60
|
+
} from "@noya-app/react-utils";
|
|
61
|
+
import { fileOpen } from "browser-fs-access";
|
|
62
|
+
import {
|
|
63
|
+
forwardRef,
|
|
64
|
+
memo,
|
|
65
|
+
ReactNode,
|
|
66
|
+
useCallback,
|
|
67
|
+
useEffect,
|
|
68
|
+
useImperativeHandle,
|
|
69
|
+
useMemo,
|
|
70
|
+
useRef,
|
|
71
|
+
useState,
|
|
72
|
+
} from "react";
|
|
73
|
+
|
|
74
|
+
import { Size } from "@noya-app/noya-geometry";
|
|
75
|
+
import { path } from "imfs";
|
|
76
|
+
import React from "react";
|
|
77
|
+
import { handleDataTransfer } from "./utils/handleFileDrop";
|
|
78
|
+
import {
|
|
79
|
+
deleteResources,
|
|
80
|
+
ExpandedMap,
|
|
81
|
+
FileKindFilter,
|
|
82
|
+
getDepthMap,
|
|
83
|
+
getVisibleItems,
|
|
84
|
+
moveMediaInsideFolder,
|
|
85
|
+
moveUpAFolder,
|
|
86
|
+
renameResourceAndDescendantPaths,
|
|
87
|
+
updateExpandedMap,
|
|
88
|
+
validateResourceRename,
|
|
89
|
+
} from "./utils/resourceUtils";
|
|
90
|
+
|
|
91
|
+
export const gridThumbnailDimension = {
|
|
92
|
+
width: 800,
|
|
93
|
+
height: 800,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const ResourceThumbnail = memoGeneric(
|
|
97
|
+
({
|
|
98
|
+
item,
|
|
99
|
+
selected,
|
|
100
|
+
size,
|
|
101
|
+
path: pathProp,
|
|
102
|
+
renderThumbnailIcon,
|
|
103
|
+
className,
|
|
104
|
+
url: urlProp,
|
|
105
|
+
contentType: contentTypeProp,
|
|
106
|
+
viewType,
|
|
107
|
+
}: CollectionThumbnailProps<Resource> & {
|
|
108
|
+
path?: string;
|
|
109
|
+
renderThumbnailIcon?: MediaThumbnailProps["renderThumbnailIcon"];
|
|
110
|
+
className?: string;
|
|
111
|
+
url?: string;
|
|
112
|
+
contentType?: string;
|
|
113
|
+
viewType?: CollectionViewType;
|
|
114
|
+
}) => {
|
|
115
|
+
const asset = useAsset(item.type === "asset" ? item.assetId : undefined);
|
|
116
|
+
const isRoot = item.id === rootResource.id;
|
|
117
|
+
const isFolder = item.type === "directory";
|
|
118
|
+
|
|
119
|
+
let contentType: string | undefined;
|
|
120
|
+
let url: string | undefined;
|
|
121
|
+
// let width: number | undefined;
|
|
122
|
+
// let height: number | undefined;
|
|
123
|
+
|
|
124
|
+
if (asset) {
|
|
125
|
+
contentType = asset.contentType;
|
|
126
|
+
// if (contentType?.startsWith("image/")) {
|
|
127
|
+
url = asset.url;
|
|
128
|
+
// }
|
|
129
|
+
// width = asset.width ?? undefined;
|
|
130
|
+
// height = asset.height ?? undefined;
|
|
131
|
+
} else if (urlProp) {
|
|
132
|
+
url = urlProp;
|
|
133
|
+
contentType = contentTypeProp;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const fileName = pathProp ? path.basename(pathProp) : undefined;
|
|
137
|
+
// const dimensions = width && height ? { width, height } : undefined;
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<MediaThumbnail
|
|
141
|
+
contentType={contentType}
|
|
142
|
+
iconName={isRoot ? "HomeIcon" : isFolder ? "FolderIcon" : undefined}
|
|
143
|
+
url={url}
|
|
144
|
+
selected={selected}
|
|
145
|
+
size={size}
|
|
146
|
+
fileName={fileName}
|
|
147
|
+
renderThumbnailIcon={renderThumbnailIcon}
|
|
148
|
+
dimensions={viewType === "grid" ? gridThumbnailDimension : undefined}
|
|
149
|
+
className={className}
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
type MenuAction =
|
|
156
|
+
| "preview"
|
|
157
|
+
| "download"
|
|
158
|
+
| "upload"
|
|
159
|
+
| "rename"
|
|
160
|
+
| "replace"
|
|
161
|
+
| "delete"
|
|
162
|
+
| "move"
|
|
163
|
+
| "addFolder";
|
|
164
|
+
|
|
165
|
+
export type ResourceExplorerRef = {
|
|
166
|
+
upload: (selectedId: string) => void;
|
|
167
|
+
delete: (selectedIds: string[]) => void;
|
|
168
|
+
download: (selectedItems: Resource[]) => void;
|
|
169
|
+
rename: (selectedItemId: string) => void;
|
|
170
|
+
addFolder: (currentFolderId: string) => void;
|
|
171
|
+
moveUpAFolder: (selectedIds: string[]) => void;
|
|
172
|
+
replace: (selectedItem: Resource) => void;
|
|
173
|
+
preview: (selectedItems: Resource[]) => void;
|
|
174
|
+
getItemAtIndex: (index: number) => Resource | undefined;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
type ResourceExplorerProps = {
|
|
178
|
+
parentFileId: string;
|
|
179
|
+
onSelectionChange?: (
|
|
180
|
+
selectedIds: string[],
|
|
181
|
+
event?: ListView.ClickInfo
|
|
182
|
+
) => void;
|
|
183
|
+
selectedIds?: string[];
|
|
184
|
+
media: ResourceMap;
|
|
185
|
+
setMedia?: (
|
|
186
|
+
metadata: Partial<MultiplayerPatchMetadata>,
|
|
187
|
+
media: ResourceCreateMap
|
|
188
|
+
) => void;
|
|
189
|
+
/** @default false */
|
|
190
|
+
readOnly?: boolean;
|
|
191
|
+
/** @default "list" */
|
|
192
|
+
viewType?: CollectionViewType;
|
|
193
|
+
/**
|
|
194
|
+
* Whether to show assets or directories or all media items
|
|
195
|
+
*
|
|
196
|
+
* @default "all"
|
|
197
|
+
* */
|
|
198
|
+
fileKindFilter?: FileKindFilter;
|
|
199
|
+
/**
|
|
200
|
+
* Whether to show the root item
|
|
201
|
+
*
|
|
202
|
+
* @default false
|
|
203
|
+
* */
|
|
204
|
+
showRootItem?: boolean;
|
|
205
|
+
/** Whether to expand all directories by default */
|
|
206
|
+
initialExpanded?: ExpandedMap;
|
|
207
|
+
/**
|
|
208
|
+
* Callback for when an item is double-clicked
|
|
209
|
+
*/
|
|
210
|
+
onDoubleClickItem?: (resourceItemId: string) => void;
|
|
211
|
+
/**
|
|
212
|
+
* If provided, only show items that are descendants of this folder
|
|
213
|
+
*/
|
|
214
|
+
rootItemId?: string;
|
|
215
|
+
title?: ReactNode;
|
|
216
|
+
right?: ReactNode;
|
|
217
|
+
className?: string;
|
|
218
|
+
showUploadButton?: boolean;
|
|
219
|
+
/**
|
|
220
|
+
* If true, show all descendants of all directories within a collection
|
|
221
|
+
* @default true
|
|
222
|
+
* */
|
|
223
|
+
showAllDescendants?: boolean;
|
|
224
|
+
renderAction?: Exclude<
|
|
225
|
+
CollectionProps<Resource, MenuAction>["renderAction"],
|
|
226
|
+
"menu"
|
|
227
|
+
>;
|
|
228
|
+
/** @default false */
|
|
229
|
+
sortable?: boolean;
|
|
230
|
+
renderThumbnailIcon?: MediaThumbnailProps["renderThumbnailIcon"];
|
|
231
|
+
onDidDeleteItems?: (items: [string, Resource][]) => void;
|
|
232
|
+
onAssetsUploaded?: (mediaMap: Record<string, AssetResourceCreate>) => void;
|
|
233
|
+
|
|
234
|
+
publishedResources?: Record<string, Resource>;
|
|
235
|
+
virtualized?: boolean;
|
|
236
|
+
|
|
237
|
+
renderUser?: (resource: Resource) => ReactNode;
|
|
238
|
+
} & Pick<
|
|
239
|
+
CollectionProps<Resource, MenuAction>,
|
|
240
|
+
| "sortableId"
|
|
241
|
+
| "size"
|
|
242
|
+
| "expandable"
|
|
243
|
+
| "renamable"
|
|
244
|
+
| "scrollable"
|
|
245
|
+
| "renderEmptyState"
|
|
246
|
+
| "sharedDragProps"
|
|
247
|
+
| "onClickItem"
|
|
248
|
+
| "itemClassName"
|
|
249
|
+
| "itemStyle"
|
|
250
|
+
>;
|
|
251
|
+
|
|
252
|
+
export const ResourceExplorer = memo(
|
|
253
|
+
forwardRef<ResourceExplorerRef, ResourceExplorerProps>(
|
|
254
|
+
function ResourceExplorer(
|
|
255
|
+
{
|
|
256
|
+
parentFileId,
|
|
257
|
+
sortableId,
|
|
258
|
+
onSelectionChange,
|
|
259
|
+
selectedIds: selectedIdsProp,
|
|
260
|
+
media,
|
|
261
|
+
setMedia: setMediaProp,
|
|
262
|
+
readOnly = false,
|
|
263
|
+
viewType = "list",
|
|
264
|
+
fileKindFilter = "all",
|
|
265
|
+
showRootItem = false,
|
|
266
|
+
initialExpanded,
|
|
267
|
+
expandable = true,
|
|
268
|
+
renamable = true,
|
|
269
|
+
onDoubleClickItem,
|
|
270
|
+
rootItemId = rootResource.id,
|
|
271
|
+
title,
|
|
272
|
+
size = "medium",
|
|
273
|
+
right,
|
|
274
|
+
renderAction: renderActionProp,
|
|
275
|
+
className,
|
|
276
|
+
showUploadButton = true,
|
|
277
|
+
showAllDescendants = true,
|
|
278
|
+
scrollable = false,
|
|
279
|
+
sortable = false,
|
|
280
|
+
renderEmptyState,
|
|
281
|
+
sharedDragProps,
|
|
282
|
+
onClickItem,
|
|
283
|
+
renderThumbnailIcon,
|
|
284
|
+
onDidDeleteItems,
|
|
285
|
+
onAssetsUploaded,
|
|
286
|
+
itemClassName,
|
|
287
|
+
itemStyle,
|
|
288
|
+
publishedResources,
|
|
289
|
+
virtualized = false,
|
|
290
|
+
renderUser,
|
|
291
|
+
},
|
|
292
|
+
ref
|
|
293
|
+
) {
|
|
294
|
+
const setMedia = useCallback(
|
|
295
|
+
(...args: Parameters<Extract<typeof setMediaProp, Function>>) => {
|
|
296
|
+
setMediaProp?.(...args);
|
|
297
|
+
},
|
|
298
|
+
[setMediaProp]
|
|
299
|
+
);
|
|
300
|
+
const tree = useMemo(() => createResourceTree(media), [media]);
|
|
301
|
+
const [tempItem, setTempItem] = useState<[string, Resource] | undefined>(
|
|
302
|
+
undefined
|
|
303
|
+
);
|
|
304
|
+
const mediaWithTempItem = useMemo(
|
|
305
|
+
() => ({
|
|
306
|
+
...media,
|
|
307
|
+
...(tempItem ? { [tempItem[0]]: tempItem[1] } : {}),
|
|
308
|
+
}),
|
|
309
|
+
[media, tempItem]
|
|
310
|
+
);
|
|
311
|
+
const treeWithTempItem = useMemo(
|
|
312
|
+
() => createResourceTree(mediaWithTempItem),
|
|
313
|
+
[mediaWithTempItem]
|
|
314
|
+
);
|
|
315
|
+
const temp = useMemo(
|
|
316
|
+
() => ({
|
|
317
|
+
media: mediaWithTempItem,
|
|
318
|
+
tree: treeWithTempItem,
|
|
319
|
+
}),
|
|
320
|
+
[mediaWithTempItem, treeWithTempItem]
|
|
321
|
+
);
|
|
322
|
+
const [selectedIds, setSelectedIds] = useControlledOrUncontrolled<
|
|
323
|
+
string[]
|
|
324
|
+
>({
|
|
325
|
+
defaultValue: [],
|
|
326
|
+
value: selectedIdsProp,
|
|
327
|
+
onChange: onSelectionChange,
|
|
328
|
+
});
|
|
329
|
+
const assetManager = useAssetManager();
|
|
330
|
+
const assets = useAssets();
|
|
331
|
+
const [expandedMap, setExpandedMap] = useState<ExpandedMap>({});
|
|
332
|
+
const visibleItems = useMemo(
|
|
333
|
+
() =>
|
|
334
|
+
getVisibleItems({
|
|
335
|
+
expandedMap,
|
|
336
|
+
fileKindFilter: fileKindFilter,
|
|
337
|
+
rootItemId,
|
|
338
|
+
tree: treeWithTempItem,
|
|
339
|
+
showAllDescendants,
|
|
340
|
+
showRootItem,
|
|
341
|
+
}),
|
|
342
|
+
[
|
|
343
|
+
expandedMap,
|
|
344
|
+
fileKindFilter,
|
|
345
|
+
rootItemId,
|
|
346
|
+
treeWithTempItem,
|
|
347
|
+
showAllDescendants,
|
|
348
|
+
showRootItem,
|
|
349
|
+
]
|
|
350
|
+
);
|
|
351
|
+
const depthMap = useMemo(
|
|
352
|
+
() => getDepthMap(rootResource, treeWithTempItem, showAllDescendants),
|
|
353
|
+
[treeWithTempItem, showAllDescendants]
|
|
354
|
+
);
|
|
355
|
+
const collectionRef = useRef<CollectionRef>(null);
|
|
356
|
+
const selectedResources = useMemo(
|
|
357
|
+
() =>
|
|
358
|
+
treeWithTempItem.resourcesWithRoot.filter((item) =>
|
|
359
|
+
selectedIds.includes(item.id)
|
|
360
|
+
),
|
|
361
|
+
[treeWithTempItem, selectedIds]
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const groupedItems = groupBy(selectedResources, (item) => item.type);
|
|
365
|
+
const selectedAssetItems = groupedItems.asset ?? [];
|
|
366
|
+
const selectedFolderItems = groupedItems.folder ?? [];
|
|
367
|
+
|
|
368
|
+
const singleItemSelected = selectedResources.length === 1;
|
|
369
|
+
const onlyAssetsSelected =
|
|
370
|
+
selectedAssetItems.length > 0 &&
|
|
371
|
+
selectedAssetItems.length === selectedResources.length;
|
|
372
|
+
const onlyFoldersSelected =
|
|
373
|
+
selectedFolderItems.length > 0 &&
|
|
374
|
+
selectedFolderItems.length === selectedResources.length;
|
|
375
|
+
const onlySingleFolderSelected =
|
|
376
|
+
onlyFoldersSelected && selectedFolderItems.length === 1;
|
|
377
|
+
const onlySingleAssetSelected =
|
|
378
|
+
onlyAssetsSelected && selectedAssetItems.length === 1;
|
|
379
|
+
const rootSelected = selectedIds.includes(rootResource.id);
|
|
380
|
+
const sameParentSelected = selectedResources.every((item) => {
|
|
381
|
+
const itemPath = tree.idToPathMap.get(item.id);
|
|
382
|
+
const firstSelectedPath = tree.idToPathMap.get(selectedIds[0]);
|
|
383
|
+
if (!itemPath || !firstSelectedPath) return false;
|
|
384
|
+
return itemPath.startsWith(path.dirname(firstSelectedPath));
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
if (initialExpanded) {
|
|
389
|
+
setExpandedMap(initialExpanded);
|
|
390
|
+
}
|
|
391
|
+
}, [initialExpanded]);
|
|
392
|
+
|
|
393
|
+
const getExpanded = useCallback(
|
|
394
|
+
(item: Resource) => {
|
|
395
|
+
if (!expandable) return undefined;
|
|
396
|
+
if (item.type !== "directory") return undefined;
|
|
397
|
+
if (item.id === rootResource.id) return undefined;
|
|
398
|
+
return expandedMap[item.id] ?? false;
|
|
399
|
+
},
|
|
400
|
+
[expandedMap, expandable]
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
const openConfirmationDialog = useOpenConfirmationDialog();
|
|
404
|
+
|
|
405
|
+
const handleDelete = useCallback(
|
|
406
|
+
async (selectedIds: string[]) => {
|
|
407
|
+
const ok = await openConfirmationDialog({
|
|
408
|
+
title: "Delete items",
|
|
409
|
+
description:
|
|
410
|
+
"Are you sure you want to delete these items? This action cannot be undone.",
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
if (!ok) return;
|
|
414
|
+
|
|
415
|
+
const deletedItems = Object.entries(media).flatMap(
|
|
416
|
+
([path, item]): [string, Resource][] => {
|
|
417
|
+
if (selectedIds.includes(item.id)) {
|
|
418
|
+
return [[path, item]];
|
|
419
|
+
}
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
const newMedia = deleteResources({
|
|
425
|
+
selectedIds,
|
|
426
|
+
media,
|
|
427
|
+
tree,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
setSelectedIds([rootResource.id]);
|
|
431
|
+
setMedia({ name: "Delete items", timestamp: Date.now() }, newMedia);
|
|
432
|
+
|
|
433
|
+
onDidDeleteItems?.(deletedItems);
|
|
434
|
+
},
|
|
435
|
+
[
|
|
436
|
+
media,
|
|
437
|
+
setMedia,
|
|
438
|
+
setSelectedIds,
|
|
439
|
+
tree,
|
|
440
|
+
onDidDeleteItems,
|
|
441
|
+
openConfirmationDialog,
|
|
442
|
+
]
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const onRename = useCallback(
|
|
446
|
+
(selectedItem: Resource, newName: string) => {
|
|
447
|
+
if (!renamable) return;
|
|
448
|
+
const selectedItemPath = treeWithTempItem.idToPathMap.get(
|
|
449
|
+
selectedItem.id
|
|
450
|
+
);
|
|
451
|
+
if (!selectedItemPath) return;
|
|
452
|
+
const renameIsValid = validateResourceRename({
|
|
453
|
+
basename: newName,
|
|
454
|
+
selectedItemPath,
|
|
455
|
+
media: temp.media,
|
|
456
|
+
});
|
|
457
|
+
if (!renameIsValid) {
|
|
458
|
+
setTempItem(undefined);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const mediaWithRenamedDescendantPaths =
|
|
462
|
+
renameResourceAndDescendantPaths({
|
|
463
|
+
newName,
|
|
464
|
+
selectedItemPath,
|
|
465
|
+
media: temp.media,
|
|
466
|
+
tree: temp.tree,
|
|
467
|
+
});
|
|
468
|
+
setMedia(
|
|
469
|
+
{ name: "Rename media item", timestamp: Date.now() },
|
|
470
|
+
mediaWithRenamedDescendantPaths
|
|
471
|
+
);
|
|
472
|
+
setTempItem(undefined);
|
|
473
|
+
},
|
|
474
|
+
[
|
|
475
|
+
renamable,
|
|
476
|
+
setMedia,
|
|
477
|
+
temp.media,
|
|
478
|
+
temp.tree,
|
|
479
|
+
treeWithTempItem.idToPathMap,
|
|
480
|
+
]
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
const handleAddFolder = useCallback(
|
|
484
|
+
(currentFolderId: string) => {
|
|
485
|
+
const currentFolderPath = tree.idToPathMap.get(currentFolderId);
|
|
486
|
+
if (!currentFolderPath) return;
|
|
487
|
+
const newFolderPath = path.join(
|
|
488
|
+
currentFolderPath,
|
|
489
|
+
PLACEHOLDER_ITEM_NAME
|
|
490
|
+
);
|
|
491
|
+
const newFolder = createDirectoryResource({
|
|
492
|
+
path: newFolderPath,
|
|
493
|
+
accessibleByFileId: parentFileId,
|
|
494
|
+
id: newFolderPath,
|
|
495
|
+
stableId: uuid(),
|
|
496
|
+
});
|
|
497
|
+
setTempItem([newFolderPath, newFolder]);
|
|
498
|
+
setTimeout(() => {
|
|
499
|
+
collectionRef.current?.editName(newFolder.id);
|
|
500
|
+
}, 50);
|
|
501
|
+
},
|
|
502
|
+
[parentFileId, tree]
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
const handleMoveUpAFolder = useCallback(
|
|
506
|
+
(selectedIds: string[]) => {
|
|
507
|
+
const newMedia = moveUpAFolder({
|
|
508
|
+
tree,
|
|
509
|
+
media,
|
|
510
|
+
selectedIds,
|
|
511
|
+
});
|
|
512
|
+
if (!newMedia) return;
|
|
513
|
+
setMedia({ name: "Move items", timestamp: Date.now() }, newMedia);
|
|
514
|
+
},
|
|
515
|
+
[media, tree, setMedia]
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
519
|
+
|
|
520
|
+
const handleUpload = useCallback(
|
|
521
|
+
async (selectedId: string) => {
|
|
522
|
+
try {
|
|
523
|
+
const files = await fileOpen({ multiple: true });
|
|
524
|
+
|
|
525
|
+
if (!files || !Array.isArray(files) || files.length === 0) return;
|
|
526
|
+
|
|
527
|
+
const parentPath = tree.idToPathMap.get(selectedId);
|
|
528
|
+
|
|
529
|
+
if (!parentPath) return;
|
|
530
|
+
|
|
531
|
+
const uploadPromises = files.map(async (file) => {
|
|
532
|
+
const assetPath = path.join(parentPath, path.basename(file.name));
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
assetPath,
|
|
536
|
+
asset: createAssetResource({
|
|
537
|
+
id: uuid(),
|
|
538
|
+
asset: {
|
|
539
|
+
content: Base64.encode(await file.arrayBuffer()),
|
|
540
|
+
contentType: file.type,
|
|
541
|
+
encoding: "base64",
|
|
542
|
+
},
|
|
543
|
+
path: assetPath,
|
|
544
|
+
accessibleByFileId: parentFileId,
|
|
545
|
+
stableId: uuid(),
|
|
546
|
+
}),
|
|
547
|
+
};
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
setIsUploading(true);
|
|
551
|
+
|
|
552
|
+
const uploadedAssets = await Promise.all(uploadPromises);
|
|
553
|
+
const newMediaMap = Object.fromEntries(
|
|
554
|
+
uploadedAssets.map(({ assetPath, asset }) => [assetPath, asset])
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
setMedia(
|
|
558
|
+
{ name: "Add media items", timestamp: Date.now() },
|
|
559
|
+
{
|
|
560
|
+
...media,
|
|
561
|
+
...newMediaMap,
|
|
562
|
+
}
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
onAssetsUploaded?.(newMediaMap);
|
|
566
|
+
} catch (error) {
|
|
567
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
568
|
+
// Ignore user-cancelled file picker
|
|
569
|
+
} else {
|
|
570
|
+
console.error("Failed to upload files:", error);
|
|
571
|
+
}
|
|
572
|
+
} finally {
|
|
573
|
+
setIsUploading(false);
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
[tree, setMedia, media, onAssetsUploaded, parentFileId]
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
const handleDownload = useCallback(
|
|
580
|
+
async (selectedItems: Resource[]) => {
|
|
581
|
+
const downloadPromises = selectedItems
|
|
582
|
+
.filter((item) => item.type === "asset")
|
|
583
|
+
.map(async (item) => {
|
|
584
|
+
const asset = assets.find((a) => a.id === item.assetId);
|
|
585
|
+
if (!asset?.url) return;
|
|
586
|
+
return downloadUrl(asset.url, tree.getNameForId(item.id));
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
await Promise.all(downloadPromises);
|
|
590
|
+
},
|
|
591
|
+
[assets, tree]
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
const handlePreview = useCallback(
|
|
595
|
+
async (selectedItems: Resource[]) => {
|
|
596
|
+
const previewPromises = selectedItems
|
|
597
|
+
.filter((item) => item.type === "asset")
|
|
598
|
+
.map(async (item) => {
|
|
599
|
+
const asset = assets.find((a) => a.id === item.assetId);
|
|
600
|
+
if (!asset?.url) return;
|
|
601
|
+
return window?.open(asset.url, "_blank");
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
await Promise.all(previewPromises);
|
|
605
|
+
},
|
|
606
|
+
[assets]
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
const handleReplace = useCallback(
|
|
610
|
+
async (selectedItem: Resource) => {
|
|
611
|
+
try {
|
|
612
|
+
const file = await fileOpen();
|
|
613
|
+
if (!file) return;
|
|
614
|
+
// Create the asset
|
|
615
|
+
const asset = await assetManager.create(file);
|
|
616
|
+
const oldFile = selectedItem;
|
|
617
|
+
const oldFilePath = tree.idToPathMap.get(oldFile.id);
|
|
618
|
+
if (!oldFilePath || oldFile.type !== "asset") return;
|
|
619
|
+
setMedia(
|
|
620
|
+
{ name: "Replace media file", timestamp: Date.now() },
|
|
621
|
+
{
|
|
622
|
+
...media,
|
|
623
|
+
[oldFilePath]: createAssetResource({
|
|
624
|
+
...oldFile,
|
|
625
|
+
assetId: asset.id,
|
|
626
|
+
}),
|
|
627
|
+
}
|
|
628
|
+
);
|
|
629
|
+
} catch (error) {
|
|
630
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
631
|
+
// Ignore user-cancelled file picker
|
|
632
|
+
} else {
|
|
633
|
+
console.error("Failed to upload files:", error);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
[media, setMedia, assetManager, tree]
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
const handleRename = useCallback(
|
|
641
|
+
(selectedItemId: string) => {
|
|
642
|
+
collectionRef.current?.editName(selectedItemId);
|
|
643
|
+
},
|
|
644
|
+
[collectionRef]
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
const handleMoveMediaInsideFolder = useCallback(
|
|
648
|
+
(sourceItems: Resource[], targetItem: Resource) => {
|
|
649
|
+
const newMedia = moveMediaInsideFolder({
|
|
650
|
+
sourceItemIds: sourceItems.map((item) => item.id),
|
|
651
|
+
targetItemId: targetItem.id,
|
|
652
|
+
media,
|
|
653
|
+
tree,
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
setMedia(
|
|
657
|
+
{
|
|
658
|
+
name: "Move media file inside folder",
|
|
659
|
+
timestamp: Date.now(),
|
|
660
|
+
},
|
|
661
|
+
newMedia
|
|
662
|
+
);
|
|
663
|
+
},
|
|
664
|
+
[media, setMedia, tree]
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
const assetContextMenuItems = useMemo(() => {
|
|
668
|
+
return createSectionedMenu<MenuAction>(
|
|
669
|
+
[
|
|
670
|
+
!rootSelected &&
|
|
671
|
+
singleItemSelected && {
|
|
672
|
+
title: "Rename",
|
|
673
|
+
value: "rename",
|
|
674
|
+
icon: <InputIcon />,
|
|
675
|
+
},
|
|
676
|
+
onlySingleAssetSelected && {
|
|
677
|
+
title: "Replace",
|
|
678
|
+
value: "replace",
|
|
679
|
+
icon: <UpdateIcon />,
|
|
680
|
+
},
|
|
681
|
+
!rootSelected && {
|
|
682
|
+
title: "Delete",
|
|
683
|
+
value: "delete",
|
|
684
|
+
icon: <TrashIcon />,
|
|
685
|
+
},
|
|
686
|
+
],
|
|
687
|
+
[
|
|
688
|
+
onlySingleFolderSelected && {
|
|
689
|
+
title: "Upload Files",
|
|
690
|
+
value: "upload",
|
|
691
|
+
icon: <UploadIcon />,
|
|
692
|
+
},
|
|
693
|
+
onlySingleFolderSelected && {
|
|
694
|
+
title: "Add Folder",
|
|
695
|
+
value: "addFolder",
|
|
696
|
+
icon: <FolderIcon />,
|
|
697
|
+
},
|
|
698
|
+
onlyAssetsSelected &&
|
|
699
|
+
tree.resourcesWithRoot.length > 0 && {
|
|
700
|
+
title: "Download",
|
|
701
|
+
value: "download",
|
|
702
|
+
icon: <DownloadIcon />,
|
|
703
|
+
},
|
|
704
|
+
onlySingleAssetSelected && {
|
|
705
|
+
title: "Preview",
|
|
706
|
+
value: "preview",
|
|
707
|
+
icon: <OpenInNewWindowIcon />,
|
|
708
|
+
},
|
|
709
|
+
],
|
|
710
|
+
[
|
|
711
|
+
!rootSelected &&
|
|
712
|
+
sameParentSelected && {
|
|
713
|
+
title: "Move up a folder",
|
|
714
|
+
value: "move",
|
|
715
|
+
icon: <ResetIcon />,
|
|
716
|
+
},
|
|
717
|
+
]
|
|
718
|
+
);
|
|
719
|
+
}, [
|
|
720
|
+
rootSelected,
|
|
721
|
+
singleItemSelected,
|
|
722
|
+
onlySingleAssetSelected,
|
|
723
|
+
onlySingleFolderSelected,
|
|
724
|
+
onlyAssetsSelected,
|
|
725
|
+
tree.resourcesWithRoot.length,
|
|
726
|
+
sameParentSelected,
|
|
727
|
+
]);
|
|
728
|
+
|
|
729
|
+
const handleMenuAction = useCallback(
|
|
730
|
+
async (action: MenuAction, selectedItems: Resource[]) => {
|
|
731
|
+
if (selectedItems.length === 0) return;
|
|
732
|
+
|
|
733
|
+
switch (action) {
|
|
734
|
+
case "rename":
|
|
735
|
+
handleRename(selectedItems[0].id);
|
|
736
|
+
return;
|
|
737
|
+
case "delete":
|
|
738
|
+
handleDelete(selectedIds);
|
|
739
|
+
return;
|
|
740
|
+
case "replace":
|
|
741
|
+
handleReplace(selectedItems[0]);
|
|
742
|
+
return;
|
|
743
|
+
case "upload":
|
|
744
|
+
handleUpload(selectedItems[0].id);
|
|
745
|
+
return;
|
|
746
|
+
case "addFolder":
|
|
747
|
+
handleAddFolder(selectedItems[0].id);
|
|
748
|
+
return;
|
|
749
|
+
case "preview":
|
|
750
|
+
handlePreview(selectedItems);
|
|
751
|
+
return;
|
|
752
|
+
case "download":
|
|
753
|
+
handleDownload(selectedItems);
|
|
754
|
+
return;
|
|
755
|
+
case "move":
|
|
756
|
+
handleMoveUpAFolder(selectedIds);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
},
|
|
760
|
+
[
|
|
761
|
+
handleRename,
|
|
762
|
+
handleDelete,
|
|
763
|
+
selectedIds,
|
|
764
|
+
handleReplace,
|
|
765
|
+
handleUpload,
|
|
766
|
+
handleAddFolder,
|
|
767
|
+
handlePreview,
|
|
768
|
+
handleDownload,
|
|
769
|
+
handleMoveUpAFolder,
|
|
770
|
+
]
|
|
771
|
+
);
|
|
772
|
+
|
|
773
|
+
const handleSetExpanded = useCallback(
|
|
774
|
+
(item: Resource, expanded: boolean) => {
|
|
775
|
+
setExpandedMap((prev) =>
|
|
776
|
+
updateExpandedMap({
|
|
777
|
+
item,
|
|
778
|
+
expanded,
|
|
779
|
+
expandable,
|
|
780
|
+
expandedMap: prev,
|
|
781
|
+
tree,
|
|
782
|
+
})
|
|
783
|
+
);
|
|
784
|
+
},
|
|
785
|
+
[expandable, tree]
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
const renderAction = useMemo(() => {
|
|
789
|
+
if (renderActionProp) return renderActionProp;
|
|
790
|
+
|
|
791
|
+
return ({
|
|
792
|
+
selected,
|
|
793
|
+
onOpenChange,
|
|
794
|
+
}: CollectionRenderActionProps<Resource>) => (
|
|
795
|
+
<div
|
|
796
|
+
style={{
|
|
797
|
+
minWidth: "75px",
|
|
798
|
+
display: "flex",
|
|
799
|
+
justifyContent: "flex-end",
|
|
800
|
+
}}
|
|
801
|
+
>
|
|
802
|
+
<ActionMenu
|
|
803
|
+
menuItems={assetContextMenuItems}
|
|
804
|
+
onSelect={(action) => handleMenuAction(action, selectedResources)}
|
|
805
|
+
selected={selected}
|
|
806
|
+
onOpenChange={onOpenChange}
|
|
807
|
+
variant={viewType === "grid" ? "normal" : "bare"}
|
|
808
|
+
style={{
|
|
809
|
+
backgroundColor: selected
|
|
810
|
+
? cssVars.colors.selectedListItemBackground
|
|
811
|
+
: "transparent",
|
|
812
|
+
color: selected
|
|
813
|
+
? cssVars.colors.selectedListItemText
|
|
814
|
+
: undefined,
|
|
815
|
+
}}
|
|
816
|
+
/>
|
|
817
|
+
</div>
|
|
818
|
+
);
|
|
819
|
+
}, [
|
|
820
|
+
assetContextMenuItems,
|
|
821
|
+
handleMenuAction,
|
|
822
|
+
renderActionProp,
|
|
823
|
+
selectedResources,
|
|
824
|
+
viewType,
|
|
825
|
+
]);
|
|
826
|
+
|
|
827
|
+
useImperativeHandle(ref, () => ({
|
|
828
|
+
upload: (selectedId: string) => handleUpload(selectedId),
|
|
829
|
+
delete: handleDelete,
|
|
830
|
+
download: handleDownload,
|
|
831
|
+
rename: handleRename,
|
|
832
|
+
addFolder: handleAddFolder,
|
|
833
|
+
moveUpAFolder: handleMoveUpAFolder,
|
|
834
|
+
replace: handleReplace,
|
|
835
|
+
preview: handlePreview,
|
|
836
|
+
moveMediaInsideFolder: handleMoveMediaInsideFolder,
|
|
837
|
+
getItemAtIndex: (index) => visibleItems[index],
|
|
838
|
+
}));
|
|
839
|
+
|
|
840
|
+
const diffResources = useMemo(() => {
|
|
841
|
+
if (!publishedResources) return;
|
|
842
|
+
|
|
843
|
+
const diff = diffResourceMaps(publishedResources, media, "stableId");
|
|
844
|
+
|
|
845
|
+
return {
|
|
846
|
+
added: new Set(
|
|
847
|
+
diff.addedResources.map((resource) => resource.stableId)
|
|
848
|
+
),
|
|
849
|
+
removed: new Set(
|
|
850
|
+
diff.removedResources.map((resource) => resource.stableId)
|
|
851
|
+
),
|
|
852
|
+
modified: new Set(
|
|
853
|
+
diff.modifiedResources.map((resource) => resource.stableId)
|
|
854
|
+
),
|
|
855
|
+
};
|
|
856
|
+
}, [media, publishedResources]);
|
|
857
|
+
|
|
858
|
+
const keyExtractor = useCallback((item: Resource) => item.id, []);
|
|
859
|
+
|
|
860
|
+
const renderCollection = (virtualizedSize: Size | undefined) => (
|
|
861
|
+
<FileExplorerCollection<Resource, MenuAction>
|
|
862
|
+
ref={collectionRef}
|
|
863
|
+
sortableId={sortableId}
|
|
864
|
+
scrollable={scrollable}
|
|
865
|
+
sharedDragProps={sharedDragProps}
|
|
866
|
+
items={visibleItems}
|
|
867
|
+
viewType={viewType}
|
|
868
|
+
size={size}
|
|
869
|
+
getId={keyExtractor}
|
|
870
|
+
getName={(file) => {
|
|
871
|
+
if (file.id === tempItem?.[1].id) {
|
|
872
|
+
return "";
|
|
873
|
+
}
|
|
874
|
+
return treeWithTempItem.getNameForId(file.id);
|
|
875
|
+
}}
|
|
876
|
+
expandable={expandable}
|
|
877
|
+
sortable={sortable}
|
|
878
|
+
getPlaceholder={(item) => {
|
|
879
|
+
switch (item.type) {
|
|
880
|
+
case "directory":
|
|
881
|
+
return "Enter folder name";
|
|
882
|
+
case "resource":
|
|
883
|
+
case "asset":
|
|
884
|
+
case "file":
|
|
885
|
+
return "Enter file name";
|
|
886
|
+
}
|
|
887
|
+
}}
|
|
888
|
+
getExpanded={getExpanded}
|
|
889
|
+
setExpanded={handleSetExpanded}
|
|
890
|
+
getRenamable={(item) => {
|
|
891
|
+
if (item.id === rootResource.id) return false;
|
|
892
|
+
return true;
|
|
893
|
+
}}
|
|
894
|
+
getDepth={(item) => depthMap[item.id]}
|
|
895
|
+
menuItems={assetContextMenuItems}
|
|
896
|
+
onSelectMenuItem={handleMenuAction}
|
|
897
|
+
onSelectionChange={setSelectedIds}
|
|
898
|
+
onClickItem={onClickItem}
|
|
899
|
+
onClick={() => {
|
|
900
|
+
setSelectedIds([]);
|
|
901
|
+
}}
|
|
902
|
+
onDoubleClickItem={onDoubleClickItem}
|
|
903
|
+
onRename={onRename}
|
|
904
|
+
renamable={renamable}
|
|
905
|
+
selectedIds={selectedIds}
|
|
906
|
+
itemClassName={itemClassName}
|
|
907
|
+
itemStyle={itemStyle}
|
|
908
|
+
virtualized={virtualizedSize}
|
|
909
|
+
renderThumbnail={(props) => (
|
|
910
|
+
<ResourceThumbnail
|
|
911
|
+
{...props}
|
|
912
|
+
path={tree.idToPathMap.get(props.item.id)}
|
|
913
|
+
renderThumbnailIcon={renderThumbnailIcon}
|
|
914
|
+
viewType={viewType}
|
|
915
|
+
/>
|
|
916
|
+
)}
|
|
917
|
+
renderAction={renderAction}
|
|
918
|
+
renderRight={(file) => {
|
|
919
|
+
if (file.type !== "asset") return null;
|
|
920
|
+
|
|
921
|
+
const publishStatus = getPublishStatus({
|
|
922
|
+
resource: file,
|
|
923
|
+
diff: diffResources,
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
return (
|
|
927
|
+
<div
|
|
928
|
+
style={{ display: "flex", alignItems: "center", gap: "1.5px" }}
|
|
929
|
+
>
|
|
930
|
+
{publishStatus && (
|
|
931
|
+
<Chip
|
|
932
|
+
colorScheme={
|
|
933
|
+
publishStatus === "Modified"
|
|
934
|
+
? "info"
|
|
935
|
+
: publishStatus === "Added"
|
|
936
|
+
? "secondary"
|
|
937
|
+
: "primary"
|
|
938
|
+
}
|
|
939
|
+
>
|
|
940
|
+
{publishStatus}
|
|
941
|
+
</Chip>
|
|
942
|
+
)}
|
|
943
|
+
{renderUser?.(file)}
|
|
944
|
+
</div>
|
|
945
|
+
);
|
|
946
|
+
}}
|
|
947
|
+
renderDetail={(file, selected) => {
|
|
948
|
+
if (file.type !== "asset") return null;
|
|
949
|
+
|
|
950
|
+
const asset = assets.find((a) => a.id === file.assetId);
|
|
951
|
+
|
|
952
|
+
if (!asset) return null;
|
|
953
|
+
|
|
954
|
+
return (
|
|
955
|
+
<FileExplorerDetail
|
|
956
|
+
selected={selected}
|
|
957
|
+
size={size}
|
|
958
|
+
style={{ minWidth: "75px", textAlign: "right" }}
|
|
959
|
+
>
|
|
960
|
+
{formatByteSize(asset.size)}
|
|
961
|
+
</FileExplorerDetail>
|
|
962
|
+
);
|
|
963
|
+
}}
|
|
964
|
+
renderEmptyState={() =>
|
|
965
|
+
renderEmptyState?.() ?? <FileExplorerEmptyState />
|
|
966
|
+
}
|
|
967
|
+
itemRoleDescription="clickable file item"
|
|
968
|
+
getDropTargetParentIndex={(overIndex) => {
|
|
969
|
+
const item = visibleItems[overIndex];
|
|
970
|
+
const parentIndex = visibleItems.findIndex(
|
|
971
|
+
(i) => i.id === tree.getParentIdForId(item.id)
|
|
972
|
+
);
|
|
973
|
+
return parentIndex === -1 ? undefined : parentIndex;
|
|
974
|
+
}}
|
|
975
|
+
acceptsDrop={({
|
|
976
|
+
sourceIndex,
|
|
977
|
+
targetIndex,
|
|
978
|
+
position,
|
|
979
|
+
sourceListId,
|
|
980
|
+
targetListId,
|
|
981
|
+
}) => {
|
|
982
|
+
if (sourceListId !== targetListId) {
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (sourceListId !== sortableId || targetListId !== sortableId) {
|
|
987
|
+
return false;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const sourceItem = visibleItems[sourceIndex];
|
|
991
|
+
const targetItem = visibleItems[targetIndex];
|
|
992
|
+
|
|
993
|
+
return acceptsResourceDrop({
|
|
994
|
+
position,
|
|
995
|
+
sourceItem,
|
|
996
|
+
targetItem,
|
|
997
|
+
tree,
|
|
998
|
+
});
|
|
999
|
+
}}
|
|
1000
|
+
onMoveItem={({
|
|
1001
|
+
sourceListId,
|
|
1002
|
+
sourceIndex,
|
|
1003
|
+
targetListId,
|
|
1004
|
+
targetIndex,
|
|
1005
|
+
position,
|
|
1006
|
+
}) => {
|
|
1007
|
+
if (
|
|
1008
|
+
sourceListId !== sortableId ||
|
|
1009
|
+
targetListId !== sortableId ||
|
|
1010
|
+
position !== "inside"
|
|
1011
|
+
) {
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const selectedItems = visibleItems.filter((item) =>
|
|
1016
|
+
selectedIds.includes(item.id)
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
const sourceItem = visibleItems[sourceIndex];
|
|
1020
|
+
|
|
1021
|
+
// if source is selected, move the selected items, otherwise move the source item
|
|
1022
|
+
const itemsToMove = selectedIds.includes(sourceItem.id)
|
|
1023
|
+
? selectedItems
|
|
1024
|
+
: [sourceItem];
|
|
1025
|
+
|
|
1026
|
+
const targetItem = visibleItems[targetIndex];
|
|
1027
|
+
|
|
1028
|
+
handleMoveMediaInsideFolder(itemsToMove, targetItem);
|
|
1029
|
+
}}
|
|
1030
|
+
onFilesDrop={async (event: React.DragEvent<Element>) => {
|
|
1031
|
+
event.preventDefault();
|
|
1032
|
+
|
|
1033
|
+
const rootItemPath = tree.idToPathMap.get(rootItemId);
|
|
1034
|
+
|
|
1035
|
+
if (!rootItemPath) return;
|
|
1036
|
+
|
|
1037
|
+
const newMedia = await handleDataTransfer({
|
|
1038
|
+
dataTransfer: event.dataTransfer,
|
|
1039
|
+
rootItemPath,
|
|
1040
|
+
resourceMap: media,
|
|
1041
|
+
accessibleByFileId: parentFileId,
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
if (!newMedia) return;
|
|
1045
|
+
|
|
1046
|
+
setMedia(
|
|
1047
|
+
{ name: "Add media files", timestamp: Date.now() },
|
|
1048
|
+
newMedia
|
|
1049
|
+
);
|
|
1050
|
+
}}
|
|
1051
|
+
/>
|
|
1052
|
+
);
|
|
1053
|
+
|
|
1054
|
+
return (
|
|
1055
|
+
<FileExplorerLayout
|
|
1056
|
+
title={title ?? rootResourceName}
|
|
1057
|
+
right={
|
|
1058
|
+
!readOnly && (
|
|
1059
|
+
<FileExplorerUploadButton
|
|
1060
|
+
showUploadButton={showUploadButton}
|
|
1061
|
+
onUpload={() => handleUpload(rootResource.id)}
|
|
1062
|
+
isUploading={isUploading}
|
|
1063
|
+
>
|
|
1064
|
+
{right}
|
|
1065
|
+
</FileExplorerUploadButton>
|
|
1066
|
+
)
|
|
1067
|
+
}
|
|
1068
|
+
className={className}
|
|
1069
|
+
>
|
|
1070
|
+
{virtualized ? (
|
|
1071
|
+
<AutoSizer>
|
|
1072
|
+
{({ width, height }) =>
|
|
1073
|
+
// add 24px to account for the -mx-3 on the Collection component
|
|
1074
|
+
renderCollection({ width: width + 24, height })
|
|
1075
|
+
}
|
|
1076
|
+
</AutoSizer>
|
|
1077
|
+
) : (
|
|
1078
|
+
renderCollection(undefined)
|
|
1079
|
+
)}
|
|
1080
|
+
</FileExplorerLayout>
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
)
|
|
1084
|
+
);
|
|
1085
|
+
|
|
1086
|
+
export function acceptsResourceDrop(parameters: {
|
|
1087
|
+
position: RelativeDropPosition;
|
|
1088
|
+
sourceItem: Resource;
|
|
1089
|
+
targetItem: Resource;
|
|
1090
|
+
tree: ResourceTree;
|
|
1091
|
+
}) {
|
|
1092
|
+
const { position, sourceItem, targetItem, tree } = parameters;
|
|
1093
|
+
|
|
1094
|
+
if (position !== "inside" || targetItem.type === "asset") {
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
const sourcePath = tree.findPath(
|
|
1098
|
+
rootResource,
|
|
1099
|
+
(item) => item.id === sourceItem.id
|
|
1100
|
+
);
|
|
1101
|
+
const targetPath = tree.findPath(
|
|
1102
|
+
rootResource,
|
|
1103
|
+
(item) => item.id === targetItem.id
|
|
1104
|
+
);
|
|
1105
|
+
|
|
1106
|
+
// Don't allow dragging into a descendant
|
|
1107
|
+
if (!sourcePath || !targetPath) return false;
|
|
1108
|
+
|
|
1109
|
+
if (isDeepEqual(sourcePath, targetPath.slice(0, sourcePath.length))) {
|
|
1110
|
+
return false;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
return true;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function getPublishStatus({
|
|
1117
|
+
resource,
|
|
1118
|
+
diff,
|
|
1119
|
+
}: {
|
|
1120
|
+
resource: Resource;
|
|
1121
|
+
diff:
|
|
1122
|
+
| {
|
|
1123
|
+
modified: Set<string>;
|
|
1124
|
+
added: Set<string>;
|
|
1125
|
+
removed: Set<string>;
|
|
1126
|
+
}
|
|
1127
|
+
| undefined;
|
|
1128
|
+
}) {
|
|
1129
|
+
// If no diff, nothing has been published, so we don't need to show status
|
|
1130
|
+
if (!diff) return undefined;
|
|
1131
|
+
|
|
1132
|
+
return diff.modified.has(resource.stableId)
|
|
1133
|
+
? "Modified"
|
|
1134
|
+
: diff.added.has(resource.stableId)
|
|
1135
|
+
? "Added"
|
|
1136
|
+
: diff.removed.has(resource.stableId)
|
|
1137
|
+
? "Removed"
|
|
1138
|
+
: // We don't have to check for the specific ID, since if anything had been published
|
|
1139
|
+
// but removed, it will be in the removed set
|
|
1140
|
+
"Published";
|
|
1141
|
+
}
|