@rebasepro/studio 0.24.0 → 0.24.1-canary.gcb918fc
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/dist/{ApiExplorer-CCtczj3F.js → ApiExplorer-Bzt7Ye49.js} +5 -3
- package/dist/ApiExplorer-Bzt7Ye49.js.map +1 -0
- package/dist/{ApiKeysView-B66TdzhH.js → ApiKeysView-Bqf6XYR1.js} +2 -1
- package/dist/{ApiKeysView-B66TdzhH.js.map → ApiKeysView-Bqf6XYR1.js.map} +1 -1
- package/dist/{BackupsView-DmSMzAbT.js → BackupsView-BhOHB6iP.js} +6 -20
- package/dist/BackupsView-BhOHB6iP.js.map +1 -0
- package/dist/{BranchesView-DP1a87E-.js → BranchesView-BHfwGTYc.js} +5 -3
- package/dist/BranchesView-BHfwGTYc.js.map +1 -0
- package/dist/{CronJobsView-TDErHTuK.js → CronJobsView-CMNK7eWO.js} +52 -59
- package/dist/CronJobsView-CMNK7eWO.js.map +1 -0
- package/dist/{JSEditor-B59pwg4c.js → JSEditor-DIO2Mw8q.js} +115 -96
- package/dist/JSEditor-DIO2Mw8q.js.map +1 -0
- package/dist/{LogsExplorer-BKM4yfP8.js → LogsExplorer-C-bwDFIl.js} +19 -23
- package/dist/LogsExplorer-C-bwDFIl.js.map +1 -0
- package/dist/{RLSEditor-BlLLV6xs.js → RLSEditor-jFD_d06f.js} +10 -8
- package/dist/RLSEditor-jFD_d06f.js.map +1 -0
- package/dist/{SQLEditor-1An7w0j6.js → SQLEditor-D5Ci4pyW.js} +372 -545
- package/dist/SQLEditor-D5Ci4pyW.js.map +1 -0
- package/dist/{SchemaVisualizer-DCLVfA5t.js → SchemaVisualizer-D-rAqwul.js} +479 -206
- package/dist/SchemaVisualizer-D-rAqwul.js.map +1 -0
- package/dist/{StorageView-CbsHuCBf.js → StorageView-CPkDKbsX.js} +498 -241
- package/dist/StorageView-CPkDKbsX.js.map +1 -0
- package/dist/assets/dagre-layout.worker-BFnhAzuR.js.map +1 -0
- package/dist/components/SQLEditor/SQLEditor.d.ts +11 -0
- package/dist/components/SchemaVisualizer/dagre-layout.d.ts +32 -0
- package/dist/components/SchemaVisualizer/dagre-layout.worker.d.ts +13 -0
- package/dist/components/SchemaVisualizer/layout-runner.d.ts +23 -0
- package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +21 -10
- package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +38 -1
- package/dist/components/StorageView/useVisibleFileMetadata.d.ts +46 -0
- package/dist/dagre-layout.worker-j9NbH9Gb.js +27 -0
- package/dist/dagre-layout.worker-j9NbH9Gb.js.map +1 -0
- package/dist/index.es.js +16 -14
- package/dist/index.es.js.map +1 -1
- package/dist/sql_utils-BZD31xE3.js +478 -0
- package/dist/sql_utils-BZD31xE3.js.map +1 -0
- package/dist/utils/sql_utils.d.ts +60 -0
- package/package.json +9 -9
- package/dist/ApiExplorer-CCtczj3F.js.map +0 -1
- package/dist/BackupsView-DmSMzAbT.js.map +0 -1
- package/dist/BranchesView-DP1a87E-.js.map +0 -1
- package/dist/CronJobsView-TDErHTuK.js.map +0 -1
- package/dist/JSEditor-B59pwg4c.js.map +0 -1
- package/dist/LogsExplorer-BKM4yfP8.js.map +0 -1
- package/dist/RLSEditor-BlLLV6xs.js.map +0 -1
- package/dist/SQLEditor-1An7w0j6.js.map +0 -1
- package/dist/SchemaVisualizer-DCLVfA5t.js.map +0 -1
- package/dist/StorageView-CbsHuCBf.js.map +0 -1
|
@@ -1,11 +1,163 @@
|
|
|
1
1
|
import { t as classifyLoadFailure } from "./load-failure-Bd8lFIO7.js";
|
|
2
2
|
import { ErrorView, useApiBase, useApiConfig, useSnackbarController, useStorageSource, useStorageSources, useTranslation } from "@rebasepro/app";
|
|
3
|
-
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
-
import { ArrowLeftIcon, Button, CheckIcon, Checkbox, Chip, CircularProgress, CopyIcon, Dialog, DialogActions, DialogContent, DialogTitle, DownloadIcon, FileTextIcon, FileUpload, FolderIcon, FolderPlusIcon, IconButton, ImageIcon, LayoutGridIcon, ListIcon, LoadingButton, Music2Icon, PlusIcon, RefreshCwIcon, Select, SelectItem, TextField, Tooltip, Trash2Icon, Typography, UploadCloudIcon, VideoIcon, XIcon, cls, defaultBorderMixin, iconSize, isKeyHandled } from "@rebasepro/ui";
|
|
3
|
+
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { ArrowLeftIcon, Button, CheckIcon, Checkbox, Chip, CircularProgress, CopyIcon, Dialog, DialogActions, DialogContent, DialogTitle, DownloadIcon, FileTextIcon, FileUpload, FolderIcon, FolderPlusIcon, IconButton, ImageIcon, LayoutGridIcon, ListIcon, LoadingButton, Music2Icon, PlusIcon, RefreshCwIcon, Select, SelectItem, Table, TableBody, TableCell, TableHeader, TableRow, TextField, Tooltip, Trash2Icon, Typography, UploadCloudIcon, VideoIcon, XIcon, cls, defaultBorderMixin, iconSize, isKeyHandled } from "@rebasepro/ui";
|
|
5
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import { useSearchParams } from "react-router";
|
|
7
7
|
import { DEFAULT_STORAGE_SOURCE_KEY, isPublicStorageKey } from "@rebasepro/types";
|
|
8
8
|
import { useDropzone } from "react-dropzone";
|
|
9
|
+
//#region src/components/StorageView/useVisibleFileMetadata.ts
|
|
10
|
+
function fileMetadataOf(config) {
|
|
11
|
+
return {
|
|
12
|
+
size: config.metadata?.size,
|
|
13
|
+
contentType: config.metadata?.contentType,
|
|
14
|
+
downloadUrl: config.url ?? void 0
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
var NO_METADATA = /* @__PURE__ */ new Map();
|
|
18
|
+
/**
|
|
19
|
+
* The metadata asked for one listing: each path once, {@link METADATA_CONCURRENCY}
|
|
20
|
+
* at a time, through the source that listed it. Cancelled when the listing is
|
|
21
|
+
* replaced, so an answer for a folder or source the view has left is dropped.
|
|
22
|
+
*/
|
|
23
|
+
var MetadataRun = class {
|
|
24
|
+
source;
|
|
25
|
+
files;
|
|
26
|
+
publish;
|
|
27
|
+
wants;
|
|
28
|
+
known;
|
|
29
|
+
requested = /* @__PURE__ */ new Set();
|
|
30
|
+
queue = [];
|
|
31
|
+
inFlight = 0;
|
|
32
|
+
cancelled = false;
|
|
33
|
+
answered = [];
|
|
34
|
+
flushQueued = false;
|
|
35
|
+
constructor(source, files, publish, wants, known) {
|
|
36
|
+
this.source = source;
|
|
37
|
+
this.files = files;
|
|
38
|
+
this.publish = publish;
|
|
39
|
+
this.wants = wants;
|
|
40
|
+
this.known = known;
|
|
41
|
+
}
|
|
42
|
+
want(path) {
|
|
43
|
+
if (this.cancelled || !this.source || !path || this.requested.has(path) || this.known.has(path)) return;
|
|
44
|
+
const file = this.files.get(path);
|
|
45
|
+
if (!file || !this.wants(file)) return;
|
|
46
|
+
this.requested.add(path);
|
|
47
|
+
this.queue.push(path);
|
|
48
|
+
this.pump();
|
|
49
|
+
}
|
|
50
|
+
cancel() {
|
|
51
|
+
this.cancelled = true;
|
|
52
|
+
this.queue = [];
|
|
53
|
+
this.answered = [];
|
|
54
|
+
}
|
|
55
|
+
pump() {
|
|
56
|
+
const source = this.source;
|
|
57
|
+
if (!source) return;
|
|
58
|
+
while (!this.cancelled && this.inFlight < 6 && this.queue.length > 0) {
|
|
59
|
+
const path = this.queue.shift();
|
|
60
|
+
if (path === void 0) break;
|
|
61
|
+
this.inFlight++;
|
|
62
|
+
source.getSignedUrl(path).then((config) => this.deliver(path, fileMetadataOf(config))).catch(() => this.deliver(path, {})).finally(() => {
|
|
63
|
+
this.inFlight--;
|
|
64
|
+
this.pump();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Answers that arrive together are published as one update. */
|
|
69
|
+
deliver(path, metadata) {
|
|
70
|
+
if (this.cancelled) return;
|
|
71
|
+
this.answered.push([path, metadata]);
|
|
72
|
+
if (this.flushQueued) return;
|
|
73
|
+
this.flushQueued = true;
|
|
74
|
+
queueMicrotask(() => {
|
|
75
|
+
this.flushQueued = false;
|
|
76
|
+
if (this.cancelled || this.answered.length === 0) return;
|
|
77
|
+
const answered = this.answered;
|
|
78
|
+
this.answered = [];
|
|
79
|
+
this.publish(answered);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Metadata for the files of a listing, asked for as each comes on screen —
|
|
85
|
+
* and only for those `wants` says need it: the listing already describes most
|
|
86
|
+
* files (see `StorageReference.size`), so what is left is a file a source
|
|
87
|
+
* listed bare, and the link an image's thumbnail is drawn from.
|
|
88
|
+
*
|
|
89
|
+
* The browser used to ask for every file's metadata before it showed any of
|
|
90
|
+
* them: a folder of 1,000 files was 1,001 requests and seconds of a spinner.
|
|
91
|
+
* The listing is shown as it comes back; a file's metadata is asked for when
|
|
92
|
+
* its element — given {@link observe} as its `ref`, and its path as
|
|
93
|
+
* `data-storage-path` — scrolls into view, a few requests at a time.
|
|
94
|
+
*
|
|
95
|
+
* Kept per listing: keyed by path, and dropped when `files` or `source`
|
|
96
|
+
* changes — another folder, another source, or the same folder read again
|
|
97
|
+
* after an upload or a delete, whose files may have changed. Without an
|
|
98
|
+
* `IntersectionObserver` every file of the listing is asked for, still a few
|
|
99
|
+
* at a time and still after the listing is shown.
|
|
100
|
+
*/
|
|
101
|
+
function useVisibleFileMetadata(source, files, wants = () => true) {
|
|
102
|
+
const [store, setStore] = useState(() => ({
|
|
103
|
+
files,
|
|
104
|
+
entries: NO_METADATA
|
|
105
|
+
}));
|
|
106
|
+
const storeRef = useRef(store);
|
|
107
|
+
useLayoutEffect(() => {
|
|
108
|
+
storeRef.current = store;
|
|
109
|
+
});
|
|
110
|
+
const runRef = useRef(null);
|
|
111
|
+
const elementsRef = useRef(/* @__PURE__ */ new Set());
|
|
112
|
+
const observerRef = useRef(null);
|
|
113
|
+
const observer = useCallback(() => {
|
|
114
|
+
if (observerRef.current) return observerRef.current;
|
|
115
|
+
if (typeof IntersectionObserver !== "function") return null;
|
|
116
|
+
observerRef.current = new IntersectionObserver((entries) => {
|
|
117
|
+
for (const entry of entries) if (entry.isIntersecting && entry.target instanceof HTMLElement) runRef.current?.want(entry.target.dataset.storagePath);
|
|
118
|
+
}, { rootMargin: "200px" });
|
|
119
|
+
return observerRef.current;
|
|
120
|
+
}, []);
|
|
121
|
+
useLayoutEffect(() => {
|
|
122
|
+
const known = storeRef.current.files === files ? storeRef.current.entries : NO_METADATA;
|
|
123
|
+
const run = new MetadataRun(source, new Map(files.map((file) => [file.fullPath, file])), (answered) => setStore((previous) => ({
|
|
124
|
+
files,
|
|
125
|
+
entries: new Map([...previous.files === files ? previous.entries : NO_METADATA, ...answered])
|
|
126
|
+
})), wants, known);
|
|
127
|
+
runRef.current = run;
|
|
128
|
+
const io = observer();
|
|
129
|
+
for (const element of elementsRef.current) if (io) {
|
|
130
|
+
io.unobserve(element);
|
|
131
|
+
io.observe(element);
|
|
132
|
+
} else run.want(element.dataset.storagePath);
|
|
133
|
+
return () => run.cancel();
|
|
134
|
+
}, [
|
|
135
|
+
source,
|
|
136
|
+
files,
|
|
137
|
+
wants,
|
|
138
|
+
observer
|
|
139
|
+
]);
|
|
140
|
+
useEffect(() => () => {
|
|
141
|
+
observerRef.current?.disconnect();
|
|
142
|
+
observerRef.current = null;
|
|
143
|
+
}, []);
|
|
144
|
+
const observe = useCallback((element) => {
|
|
145
|
+
if (!element) return void 0;
|
|
146
|
+
elementsRef.current.add(element);
|
|
147
|
+
const io = observer();
|
|
148
|
+
if (io) io.observe(element);
|
|
149
|
+
else runRef.current?.want(element.dataset.storagePath);
|
|
150
|
+
return () => {
|
|
151
|
+
elementsRef.current.delete(element);
|
|
152
|
+
io?.unobserve(element);
|
|
153
|
+
};
|
|
154
|
+
}, [observer]);
|
|
155
|
+
return {
|
|
156
|
+
metadata: store.files === files ? store.entries : NO_METADATA,
|
|
157
|
+
observe
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
//#endregion
|
|
9
161
|
//#region src/components/StorageView/StorageView.tsx
|
|
10
162
|
function fileLinkOf(config, fullPath) {
|
|
11
163
|
if (!config.url) return null;
|
|
@@ -13,7 +165,17 @@ function fileLinkOf(config, fullPath) {
|
|
|
13
165
|
return {
|
|
14
166
|
url: config.url,
|
|
15
167
|
expiresInSeconds: metadata?.token && typeof metadata.tokenExpiresIn === "number" ? metadata.tokenExpiresIn : void 0,
|
|
16
|
-
isPublic: metadata ? metadata.public === true : isPublicStorageKey(fullPath)
|
|
168
|
+
isPublic: metadata ? metadata.public === true : isPublicStorageKey(fullPath),
|
|
169
|
+
size: metadata?.size,
|
|
170
|
+
contentType: metadata?.contentType
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/** `file`, with what the link minted for it says where the listing did not. */
|
|
174
|
+
function withLinkFacts(file, link) {
|
|
175
|
+
return {
|
|
176
|
+
...file,
|
|
177
|
+
size: file.size ?? link?.size,
|
|
178
|
+
contentType: file.contentType ?? link?.contentType
|
|
17
179
|
};
|
|
18
180
|
}
|
|
19
181
|
/** A URL the browser can open from anywhere: relative ones are this page's origin's. */
|
|
@@ -34,17 +196,45 @@ function formatFileSize(bytes) {
|
|
|
34
196
|
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
35
197
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
36
198
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
199
|
+
var IMAGE_EXTENSIONS = [
|
|
200
|
+
"jpg",
|
|
201
|
+
"jpeg",
|
|
202
|
+
"png",
|
|
203
|
+
"gif",
|
|
204
|
+
"webp",
|
|
205
|
+
"svg"
|
|
206
|
+
];
|
|
207
|
+
var VIDEO_EXTENSIONS = [
|
|
208
|
+
"mp4",
|
|
209
|
+
"webm",
|
|
210
|
+
"ogg",
|
|
211
|
+
"mov"
|
|
212
|
+
];
|
|
213
|
+
var AUDIO_EXTENSIONS = [
|
|
214
|
+
"mp3",
|
|
215
|
+
"wav",
|
|
216
|
+
"ogg",
|
|
217
|
+
"m4a"
|
|
218
|
+
];
|
|
219
|
+
/**
|
|
220
|
+
* The icon for a file: by its stored type, or — where the listing carried
|
|
221
|
+
* none, as an S3 listing does not — by its extension.
|
|
222
|
+
*/
|
|
223
|
+
function getFileIcon(contentType, name) {
|
|
224
|
+
const ext = getExtension(name).toLowerCase();
|
|
225
|
+
if (contentType ? contentType.startsWith("image/") : IMAGE_EXTENSIONS.includes(ext)) return ImageIcon;
|
|
226
|
+
if (contentType ? contentType.startsWith("video/") : VIDEO_EXTENSIONS.includes(ext)) return VideoIcon;
|
|
227
|
+
if (contentType ? contentType.startsWith("audio/") : AUDIO_EXTENSIONS.includes(ext)) return Music2Icon;
|
|
42
228
|
return FileTextIcon;
|
|
43
229
|
}
|
|
44
230
|
function getExtension(name) {
|
|
45
231
|
const parts = name.split(".");
|
|
46
232
|
return parts.length > 1 ? parts[parts.length - 1].toUpperCase() : "";
|
|
47
233
|
}
|
|
234
|
+
/** Whether the grid draws `file` as a thumbnail — which needs a link to it. */
|
|
235
|
+
function isImageFile(file) {
|
|
236
|
+
return file.contentType?.startsWith("image/") === true || IMAGE_EXTENSIONS.includes(getExtension(file.name).toLowerCase());
|
|
237
|
+
}
|
|
48
238
|
function breadcrumbSegments(path, rootLabel) {
|
|
49
239
|
if (!path || path === "/") return [{
|
|
50
240
|
label: rootLabel,
|
|
@@ -87,14 +277,39 @@ async function listAllPages(source, prefix) {
|
|
|
87
277
|
prefixes
|
|
88
278
|
};
|
|
89
279
|
}
|
|
280
|
+
/** How many deletes run at once: one at a time, a folder of 1,000 files took a minute. */
|
|
281
|
+
var DELETE_CONCURRENCY = 8;
|
|
282
|
+
/**
|
|
283
|
+
* `work` for each of `items`, at most `limit` at once. The first failure stops
|
|
284
|
+
* what has not started; what has started is let finish, then it is thrown.
|
|
285
|
+
*/
|
|
286
|
+
async function forEachBounded(items, limit, work) {
|
|
287
|
+
const run = { next: 0 };
|
|
288
|
+
const lane = async () => {
|
|
289
|
+
while (!run.failure && run.next < items.length) {
|
|
290
|
+
const item = items[run.next++];
|
|
291
|
+
try {
|
|
292
|
+
await work(item);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
run.failure ??= { error };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, lane));
|
|
299
|
+
if (run.failure) throw run.failure.error;
|
|
300
|
+
}
|
|
90
301
|
/**
|
|
91
302
|
* Delete a folder: every file in it, every folder below it, and the folder
|
|
92
303
|
* itself — the directory on local disk, or the zero-byte `name/` marker that
|
|
93
304
|
* "New folder" writes on S3 and GCS, which no listing shows as an entry.
|
|
305
|
+
*
|
|
306
|
+
* The files of a level go {@link DELETE_CONCURRENCY} at a time; the folders
|
|
307
|
+
* below it one after another, so no more than that are ever in flight, and a
|
|
308
|
+
* folder's own marker goes last, once everything in it has.
|
|
94
309
|
*/
|
|
95
310
|
async function deleteFolderRecursive(source, prefix) {
|
|
96
311
|
const { items, prefixes } = await listAllPages(source, prefix);
|
|
97
|
-
|
|
312
|
+
await forEachBounded(items, DELETE_CONCURRENCY, (item) => source.deleteObject(item.fullPath));
|
|
98
313
|
for (const sub of prefixes) await deleteFolderRecursive(source, sub.fullPath);
|
|
99
314
|
try {
|
|
100
315
|
await source.deleteObject(prefix.endsWith("/") ? prefix : `${prefix}/`);
|
|
@@ -116,9 +331,10 @@ function UploadDialog({ open, currentPath, onClose, onUpload }) {
|
|
|
116
331
|
setUploading(true);
|
|
117
332
|
setError(null);
|
|
118
333
|
try {
|
|
119
|
-
await onUpload(selectedFiles)
|
|
120
|
-
|
|
121
|
-
|
|
334
|
+
if (await onUpload(selectedFiles)) {
|
|
335
|
+
setSelectedFiles([]);
|
|
336
|
+
onClose();
|
|
337
|
+
}
|
|
122
338
|
} catch (err) {
|
|
123
339
|
setError(err instanceof Error ? err.message : t("studio_storage_upload_failed"));
|
|
124
340
|
} finally {
|
|
@@ -203,6 +419,7 @@ function UploadDialog({ open, currentPath, onClose, onUpload }) {
|
|
|
203
419
|
children: formatFileSize(file.size)
|
|
204
420
|
})]
|
|
205
421
|
}), /* @__PURE__ */ jsx(IconButton, {
|
|
422
|
+
"aria-label": t("remove"),
|
|
206
423
|
size: "small",
|
|
207
424
|
onClick: (e) => {
|
|
208
425
|
e.stopPropagation();
|
|
@@ -237,7 +454,7 @@ function FilePreviewPanel({ file, onClose, onDelete, link, mintLink }) {
|
|
|
237
454
|
file.contentType?.startsWith("image/");
|
|
238
455
|
file.contentType?.startsWith("video/");
|
|
239
456
|
file.contentType?.startsWith("audio/");
|
|
240
|
-
const FileIconComponent = getFileIcon(file.contentType);
|
|
457
|
+
const FileIconComponent = getFileIcon(file.contentType, file.name);
|
|
241
458
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
242
459
|
const [urlCopied, setUrlCopied] = useState(false);
|
|
243
460
|
const reportLinkError = (e) => {
|
|
@@ -291,6 +508,7 @@ function FilePreviewPanel({ file, onClose, onDelete, link, mintLink }) {
|
|
|
291
508
|
})
|
|
292
509
|
}),
|
|
293
510
|
/* @__PURE__ */ jsx(IconButton, {
|
|
511
|
+
"aria-label": t("close"),
|
|
294
512
|
size: "small",
|
|
295
513
|
onClick: onClose,
|
|
296
514
|
children: /* @__PURE__ */ jsx(XIcon, { size: iconSize.smallest })
|
|
@@ -304,26 +522,9 @@ function FilePreviewPanel({ file, onClose, onDelete, link, mintLink }) {
|
|
|
304
522
|
className: cls("flex flex-col items-center justify-center min-h-[200px] p-4 bg-surface-sheet border-b", defaultBorderMixin),
|
|
305
523
|
children: (() => {
|
|
306
524
|
const ext = getExtension(file.name)?.toLowerCase() || "";
|
|
307
|
-
const isImage = file.contentType?.startsWith("image/") ||
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
"png",
|
|
311
|
-
"gif",
|
|
312
|
-
"webp",
|
|
313
|
-
"svg"
|
|
314
|
-
].includes(ext);
|
|
315
|
-
const isVideo = file.contentType?.startsWith("video/") || [
|
|
316
|
-
"mp4",
|
|
317
|
-
"webm",
|
|
318
|
-
"ogg",
|
|
319
|
-
"mov"
|
|
320
|
-
].includes(ext);
|
|
321
|
-
const isAudio = file.contentType?.startsWith("audio/") || [
|
|
322
|
-
"mp3",
|
|
323
|
-
"wav",
|
|
324
|
-
"ogg",
|
|
325
|
-
"m4a"
|
|
326
|
-
].includes(ext);
|
|
525
|
+
const isImage = file.contentType?.startsWith("image/") || IMAGE_EXTENSIONS.includes(ext);
|
|
526
|
+
const isVideo = file.contentType?.startsWith("video/") || VIDEO_EXTENSIONS.includes(ext);
|
|
527
|
+
const isAudio = file.contentType?.startsWith("audio/") || AUDIO_EXTENSIONS.includes(ext);
|
|
327
528
|
const previewUrl = link?.url;
|
|
328
529
|
if (isImage && previewUrl) return /* @__PURE__ */ jsx("img", {
|
|
329
530
|
src: previewUrl,
|
|
@@ -358,8 +559,9 @@ function FilePreviewPanel({ file, onClose, onDelete, link, mintLink }) {
|
|
|
358
559
|
className: "p-4 space-y-3",
|
|
359
560
|
children: [
|
|
360
561
|
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Typography, {
|
|
361
|
-
variant: "
|
|
362
|
-
|
|
562
|
+
variant: "label",
|
|
563
|
+
color: "secondary",
|
|
564
|
+
className: "mb-1 block",
|
|
363
565
|
children: t("studio_storage_file_info")
|
|
364
566
|
}) }),
|
|
365
567
|
/* @__PURE__ */ jsxs("div", {
|
|
@@ -511,12 +713,26 @@ var StorageView = () => {
|
|
|
511
713
|
const [failure, setFailure] = useState(null);
|
|
512
714
|
const [folders, setFolders] = useState([]);
|
|
513
715
|
const [files, setFiles] = useState([]);
|
|
716
|
+
/** The source `files` were listed from: their metadata is read through it, whatever is selected since. */
|
|
717
|
+
const [listedFrom, setListedFrom] = useState(null);
|
|
718
|
+
const [viewMode, setViewMode] = useState("grid");
|
|
719
|
+
const { metadata, observe: observeFile } = useVisibleFileMetadata(listedFrom, files, useCallback((file) => file.size === void 0 || viewMode === "grid" && isImageFile(file), [viewMode]));
|
|
720
|
+
/** `file` with what its metadata has said so far, over what the listing said. */
|
|
721
|
+
const withMetadata = useCallback((file) => {
|
|
722
|
+
const read = metadata.get(file.fullPath);
|
|
723
|
+
if (!read) return file;
|
|
724
|
+
return {
|
|
725
|
+
...file,
|
|
726
|
+
size: read.size ?? file.size,
|
|
727
|
+
contentType: read.contentType ?? file.contentType,
|
|
728
|
+
downloadUrl: read.downloadUrl ?? file.downloadUrl
|
|
729
|
+
};
|
|
730
|
+
}, [metadata]);
|
|
514
731
|
const [selectedFile, setSelectedFile] = useState(null);
|
|
515
732
|
const [selectedLink, setSelectedLink] = useState(null);
|
|
516
733
|
/** The latest link asked for the preview: an earlier one answering later is dropped. */
|
|
517
734
|
const linkRequestRef = useRef(0);
|
|
518
735
|
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
|
|
519
|
-
const [viewMode, setViewMode] = useState("grid");
|
|
520
736
|
const [selectedPaths, setSelectedPaths] = useState(/* @__PURE__ */ new Set());
|
|
521
737
|
const lastClickedRef = useRef(null);
|
|
522
738
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
@@ -532,10 +748,11 @@ var StorageView = () => {
|
|
|
532
748
|
storageSourceRef.current = storageSource;
|
|
533
749
|
}, [storageSource]);
|
|
534
750
|
/**
|
|
535
|
-
* The latest listing asked for. Only its answer is shown: a listing
|
|
536
|
-
*
|
|
537
|
-
* view has since left — can answer after a later one, and
|
|
538
|
-
* source's files under the one selected, where their actions
|
|
751
|
+
* The latest listing asked for. Only its answer is shown: a listing of a
|
|
752
|
+
* large folder takes several pages, so an earlier one — of the source or
|
|
753
|
+
* folder the view has since left — can answer after a later one, and
|
|
754
|
+
* showed that source's files under the one selected, where their actions
|
|
755
|
+
* ran.
|
|
539
756
|
*/
|
|
540
757
|
const listingRequestRef = useRef(0);
|
|
541
758
|
const fetchContents = useCallback(async (path) => {
|
|
@@ -551,28 +768,17 @@ var StorageView = () => {
|
|
|
551
768
|
fullPath: ref.fullPath,
|
|
552
769
|
isFolder: true
|
|
553
770
|
}));
|
|
554
|
-
const fileItems =
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
isFolder: false,
|
|
561
|
-
size: downloadConfig.metadata?.size,
|
|
562
|
-
contentType: downloadConfig.metadata?.contentType,
|
|
563
|
-
downloadUrl: downloadConfig.url ?? void 0
|
|
564
|
-
};
|
|
565
|
-
} catch {
|
|
566
|
-
return {
|
|
567
|
-
name: ref.name,
|
|
568
|
-
fullPath: ref.fullPath,
|
|
569
|
-
isFolder: false
|
|
570
|
-
};
|
|
571
|
-
}
|
|
771
|
+
const fileItems = result.items.map((ref) => ({
|
|
772
|
+
name: ref.name,
|
|
773
|
+
fullPath: ref.fullPath,
|
|
774
|
+
isFolder: false,
|
|
775
|
+
size: ref.size,
|
|
776
|
+
contentType: ref.contentType
|
|
572
777
|
}));
|
|
573
778
|
if (!isCurrent()) return;
|
|
574
779
|
setFolders(folderItems);
|
|
575
780
|
setFiles(fileItems);
|
|
781
|
+
setListedFrom(source);
|
|
576
782
|
} catch (e) {
|
|
577
783
|
if (!isCurrent()) return;
|
|
578
784
|
console.error("Storage list error:", e);
|
|
@@ -670,23 +876,52 @@ var StorageView = () => {
|
|
|
670
876
|
if (item.isFolder) handleNavigate(item.fullPath);
|
|
671
877
|
else openPreview(item);
|
|
672
878
|
}, [handleNavigate, openPreview]);
|
|
879
|
+
/**
|
|
880
|
+
* Files an upload would write over, waiting for the person to say whether
|
|
881
|
+
* it should. `answer` settles the upload that asked.
|
|
882
|
+
*/
|
|
883
|
+
const [replacePrompt, setReplacePrompt] = useState(null);
|
|
884
|
+
/**
|
|
885
|
+
* Whether `uploadFiles` may go ahead: at once when none of them has the
|
|
886
|
+
* name of a file in the folder, and otherwise once the person says to
|
|
887
|
+
* replace those. An upload is a put, and a put over an existing key
|
|
888
|
+
* replaces the file without a trace.
|
|
889
|
+
*/
|
|
890
|
+
const mayReplace = useCallback((uploadFiles) => {
|
|
891
|
+
const listed = new Set(files.map((file) => file.name));
|
|
892
|
+
const names = [...new Set(uploadFiles.map((file) => file.name))].filter((name) => listed.has(name));
|
|
893
|
+
if (names.length === 0) return Promise.resolve(true);
|
|
894
|
+
return new Promise((resolve) => setReplacePrompt({
|
|
895
|
+
names,
|
|
896
|
+
answer: (replace) => {
|
|
897
|
+
setReplacePrompt(null);
|
|
898
|
+
resolve(replace);
|
|
899
|
+
}
|
|
900
|
+
}));
|
|
901
|
+
}, [files]);
|
|
673
902
|
const handleUpload = useCallback(async (uploadFiles) => {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
file
|
|
678
|
-
|
|
679
|
-
|
|
903
|
+
if (!await mayReplace(uploadFiles)) return false;
|
|
904
|
+
try {
|
|
905
|
+
for (const file of uploadFiles) {
|
|
906
|
+
const key = currentPath ? `${currentPath}/${file.name}` : file.name;
|
|
907
|
+
await storageSourceRef.current.putObject({
|
|
908
|
+
file,
|
|
909
|
+
key
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
} finally {
|
|
913
|
+
await fetchContents(currentPath);
|
|
680
914
|
}
|
|
681
915
|
snackbarController.open({
|
|
682
916
|
type: "success",
|
|
683
917
|
message: t("studio_storage_files_uploaded", { count: uploadFiles.length })
|
|
684
918
|
});
|
|
685
|
-
|
|
919
|
+
return true;
|
|
686
920
|
}, [
|
|
687
921
|
currentPath,
|
|
688
922
|
snackbarController,
|
|
689
923
|
fetchContents,
|
|
924
|
+
mayReplace,
|
|
690
925
|
t
|
|
691
926
|
]);
|
|
692
927
|
const handleCreateFolder = useCallback(async () => {
|
|
@@ -755,30 +990,14 @@ var StorageView = () => {
|
|
|
755
990
|
onDrop: useCallback(async (droppedFiles) => {
|
|
756
991
|
if (droppedFiles.length === 0) return;
|
|
757
992
|
try {
|
|
758
|
-
|
|
759
|
-
const key = currentPath ? `${currentPath}/${file.name}` : file.name;
|
|
760
|
-
await storageSourceRef.current.putObject({
|
|
761
|
-
file,
|
|
762
|
-
key
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
snackbarController.open({
|
|
766
|
-
type: "success",
|
|
767
|
-
message: t("studio_storage_files_uploaded", { count: droppedFiles.length })
|
|
768
|
-
});
|
|
769
|
-
await fetchContents(currentPath);
|
|
993
|
+
await handleUpload(droppedFiles);
|
|
770
994
|
} catch (e) {
|
|
771
995
|
snackbarController.open({
|
|
772
996
|
type: "error",
|
|
773
997
|
message: e instanceof Error ? e.message : String(e)
|
|
774
998
|
});
|
|
775
999
|
}
|
|
776
|
-
}, [
|
|
777
|
-
currentPath,
|
|
778
|
-
snackbarController,
|
|
779
|
-
fetchContents,
|
|
780
|
-
t
|
|
781
|
-
]),
|
|
1000
|
+
}, [handleUpload, snackbarController]),
|
|
782
1001
|
noClick: true,
|
|
783
1002
|
noKeyboard: true,
|
|
784
1003
|
noDragEventsBubbling: true
|
|
@@ -795,8 +1014,8 @@ var StorageView = () => {
|
|
|
795
1014
|
*/
|
|
796
1015
|
const deleteItems = useCallback(async (items, folder) => {
|
|
797
1016
|
const source = storageSourceRef.current;
|
|
1017
|
+
await forEachBounded(items.filter((item) => !item.isFolder), DELETE_CONCURRENCY, (item) => source.deleteObject(item.fullPath));
|
|
798
1018
|
for (const item of items) if (item.isFolder) await deleteFolderRecursive(source, item.fullPath);
|
|
799
|
-
else await source.deleteObject(item.fullPath);
|
|
800
1019
|
const listed = await listAllPages(source, folder);
|
|
801
1020
|
const stillListed = new Set([...listed.items, ...listed.prefixes].map((ref) => ref.fullPath));
|
|
802
1021
|
return items.filter((item) => stillListed.has(item.fullPath));
|
|
@@ -826,12 +1045,13 @@ var StorageView = () => {
|
|
|
826
1045
|
next.delete(file.fullPath);
|
|
827
1046
|
return next;
|
|
828
1047
|
});
|
|
829
|
-
fetchContents(currentPath);
|
|
830
1048
|
} catch (e) {
|
|
831
1049
|
snackbarController.open({
|
|
832
1050
|
type: "error",
|
|
833
1051
|
message: e instanceof Error ? e.message : String(e)
|
|
834
1052
|
});
|
|
1053
|
+
} finally {
|
|
1054
|
+
fetchContents(currentPath);
|
|
835
1055
|
}
|
|
836
1056
|
}, [
|
|
837
1057
|
currentPath,
|
|
@@ -854,13 +1074,13 @@ var StorageView = () => {
|
|
|
854
1074
|
setSelectedPaths(/* @__PURE__ */ new Set());
|
|
855
1075
|
setSelectedFile(null);
|
|
856
1076
|
setSelectedLink(null);
|
|
857
|
-
await fetchContents(currentPath);
|
|
858
1077
|
} catch (e) {
|
|
859
1078
|
snackbarController.open({
|
|
860
1079
|
type: "error",
|
|
861
1080
|
message: e instanceof Error ? e.message : String(e)
|
|
862
1081
|
});
|
|
863
1082
|
} finally {
|
|
1083
|
+
await fetchContents(currentPath);
|
|
864
1084
|
setDeleting(false);
|
|
865
1085
|
setDeleteDialogOpen(false);
|
|
866
1086
|
setDeleteDialogTarget(null);
|
|
@@ -894,13 +1114,13 @@ var StorageView = () => {
|
|
|
894
1114
|
next.delete(deleteDialogTarget.fullPath);
|
|
895
1115
|
return next;
|
|
896
1116
|
});
|
|
897
|
-
await fetchContents(currentPath);
|
|
898
1117
|
} catch (e) {
|
|
899
1118
|
snackbarController.open({
|
|
900
1119
|
type: "error",
|
|
901
1120
|
message: e instanceof Error ? e.message : String(e)
|
|
902
1121
|
});
|
|
903
1122
|
} finally {
|
|
1123
|
+
await fetchContents(currentPath);
|
|
904
1124
|
setDeleting(false);
|
|
905
1125
|
setDeleteDialogOpen(false);
|
|
906
1126
|
setDeleteDialogTarget(null);
|
|
@@ -921,7 +1141,7 @@ var StorageView = () => {
|
|
|
921
1141
|
}, [allItems, selectedPaths]);
|
|
922
1142
|
useEffect(() => {
|
|
923
1143
|
const handler = (e) => {
|
|
924
|
-
if (deleteDialogOpen || uploadDialogOpen || newFolderDialogOpen) return;
|
|
1144
|
+
if (deleteDialogOpen || uploadDialogOpen || newFolderDialogOpen || replacePrompt) return;
|
|
925
1145
|
if (isKeyHandled(e)) return;
|
|
926
1146
|
if ((e.metaKey || e.ctrlKey) && e.key === "a") {
|
|
927
1147
|
e.preventDefault();
|
|
@@ -946,7 +1166,8 @@ var StorageView = () => {
|
|
|
946
1166
|
selectedPaths,
|
|
947
1167
|
deleteDialogOpen,
|
|
948
1168
|
uploadDialogOpen,
|
|
949
|
-
newFolderDialogOpen
|
|
1169
|
+
newFolderDialogOpen,
|
|
1170
|
+
replacePrompt
|
|
950
1171
|
]);
|
|
951
1172
|
const handleRefresh = useCallback(() => {
|
|
952
1173
|
fetchContents(currentPath);
|
|
@@ -1035,12 +1256,12 @@ var StorageView = () => {
|
|
|
1035
1256
|
});
|
|
1036
1257
|
if (viewMode === "list") return /* @__PURE__ */ jsx("div", {
|
|
1037
1258
|
className: "flex-grow overflow-auto",
|
|
1038
|
-
children: /* @__PURE__ */ jsxs(
|
|
1259
|
+
children: /* @__PURE__ */ jsxs(Table, {
|
|
1039
1260
|
className: "w-full",
|
|
1040
|
-
children: [/* @__PURE__ */
|
|
1041
|
-
className:
|
|
1261
|
+
children: [/* @__PURE__ */ jsxs(TableHeader, {
|
|
1262
|
+
className: "text-xs font-normal text-text-secondary dark:text-text-secondary-dark",
|
|
1042
1263
|
children: [
|
|
1043
|
-
/* @__PURE__ */ jsx(
|
|
1264
|
+
/* @__PURE__ */ jsx(TableCell, {
|
|
1044
1265
|
className: "pl-3 pr-0 py-2 w-8",
|
|
1045
1266
|
children: /* @__PURE__ */ jsx(Checkbox, {
|
|
1046
1267
|
size: "small",
|
|
@@ -1049,148 +1270,155 @@ var StorageView = () => {
|
|
|
1049
1270
|
onCheckedChange: handleSelectAll
|
|
1050
1271
|
})
|
|
1051
1272
|
}),
|
|
1052
|
-
/* @__PURE__ */ jsx(
|
|
1053
|
-
className: "px-2 py-2 font-
|
|
1273
|
+
/* @__PURE__ */ jsx(TableCell, {
|
|
1274
|
+
className: "px-2 py-2 font-medium",
|
|
1054
1275
|
children: t("studio_storage_name")
|
|
1055
1276
|
}),
|
|
1056
|
-
/* @__PURE__ */ jsx(
|
|
1057
|
-
className: "px-4 py-2 font-
|
|
1277
|
+
/* @__PURE__ */ jsx(TableCell, {
|
|
1278
|
+
className: "px-4 py-2 font-medium w-24",
|
|
1058
1279
|
children: t("studio_storage_type")
|
|
1059
1280
|
}),
|
|
1060
|
-
/* @__PURE__ */ jsx(
|
|
1061
|
-
className: "px-4 py-2 font-
|
|
1281
|
+
/* @__PURE__ */ jsx(TableCell, {
|
|
1282
|
+
className: "px-4 py-2 font-medium w-24 text-right",
|
|
1062
1283
|
children: t("studio_storage_size")
|
|
1063
1284
|
}),
|
|
1064
|
-
/* @__PURE__ */ jsx(
|
|
1285
|
+
/* @__PURE__ */ jsx(TableCell, { className: "px-2 py-2 w-10" })
|
|
1065
1286
|
]
|
|
1066
|
-
})
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
className: "
|
|
1094
|
-
children:
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1287
|
+
}), /* @__PURE__ */ jsxs(TableBody, {
|
|
1288
|
+
variant: "list",
|
|
1289
|
+
children: [folders.map((folder) => {
|
|
1290
|
+
const isChecked = selectedPaths.has(folder.fullPath);
|
|
1291
|
+
return /* @__PURE__ */ jsxs(TableRow, {
|
|
1292
|
+
"data-storage-item": true,
|
|
1293
|
+
className: cls("group", isChecked && "bg-primary/5 dark:bg-primary/10"),
|
|
1294
|
+
onClick: (e) => handleItemClick(folder, e),
|
|
1295
|
+
onDoubleClick: () => handleItemDoubleClick(folder),
|
|
1296
|
+
children: [
|
|
1297
|
+
/* @__PURE__ */ jsx(TableCell, {
|
|
1298
|
+
className: "pl-3 pr-0 py-2.5",
|
|
1299
|
+
onClick: (e) => e.stopPropagation(),
|
|
1300
|
+
children: /* @__PURE__ */ jsx(Checkbox, {
|
|
1301
|
+
size: "small",
|
|
1302
|
+
checked: isChecked,
|
|
1303
|
+
onCheckedChange: () => {
|
|
1304
|
+
setSelectedPaths((prev) => {
|
|
1305
|
+
const next = new Set(prev);
|
|
1306
|
+
if (next.has(folder.fullPath)) next.delete(folder.fullPath);
|
|
1307
|
+
else next.add(folder.fullPath);
|
|
1308
|
+
return next;
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
})
|
|
1312
|
+
}),
|
|
1313
|
+
/* @__PURE__ */ jsx(TableCell, {
|
|
1314
|
+
className: "px-2 py-2.5",
|
|
1315
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1316
|
+
className: "flex items-center gap-2",
|
|
1317
|
+
children: [/* @__PURE__ */ jsx(FolderIcon, {
|
|
1318
|
+
size: iconSize.smallest,
|
|
1319
|
+
className: "text-amber-500 dark:text-amber-400 shrink-0"
|
|
1320
|
+
}), /* @__PURE__ */ jsx(Typography, {
|
|
1321
|
+
variant: "body2",
|
|
1322
|
+
className: "text-[13px] font-medium truncate",
|
|
1323
|
+
children: folder.name
|
|
1324
|
+
})]
|
|
1325
|
+
})
|
|
1326
|
+
}),
|
|
1327
|
+
/* @__PURE__ */ jsx(TableCell, {
|
|
1328
|
+
className: "px-4 py-2.5",
|
|
1329
|
+
children: /* @__PURE__ */ jsx(Typography, {
|
|
1330
|
+
variant: "caption",
|
|
1331
|
+
className: "text-text-secondary dark:text-text-secondary-dark",
|
|
1332
|
+
children: t("studio_storage_folder")
|
|
1333
|
+
})
|
|
1334
|
+
}),
|
|
1335
|
+
/* @__PURE__ */ jsx(TableCell, {
|
|
1336
|
+
className: "px-4 py-2.5 text-right",
|
|
1337
|
+
children: /* @__PURE__ */ jsx(Typography, {
|
|
1338
|
+
variant: "caption",
|
|
1339
|
+
className: "text-text-disabled dark:text-text-disabled-dark",
|
|
1340
|
+
children: "—"
|
|
1341
|
+
})
|
|
1342
|
+
}),
|
|
1343
|
+
/* @__PURE__ */ jsx(TableCell, { className: "px-2 py-2.5" })
|
|
1344
|
+
]
|
|
1345
|
+
}, folder.fullPath);
|
|
1346
|
+
}), files.map((listed) => {
|
|
1347
|
+
const file = withMetadata(listed);
|
|
1348
|
+
const FileIconComp = getFileIcon(file.contentType, file.name);
|
|
1349
|
+
const isChecked = selectedPaths.has(file.fullPath);
|
|
1350
|
+
return /* @__PURE__ */ jsxs(TableRow, {
|
|
1351
|
+
ref: observeFile,
|
|
1352
|
+
"data-storage-path": file.fullPath,
|
|
1353
|
+
"data-storage-item": true,
|
|
1354
|
+
className: cls("group", isChecked && "bg-primary/5 dark:bg-primary/10"),
|
|
1355
|
+
onClick: (e) => handleItemClick(file, e),
|
|
1356
|
+
onDoubleClick: () => handleItemDoubleClick(file),
|
|
1357
|
+
children: [
|
|
1358
|
+
/* @__PURE__ */ jsx(TableCell, {
|
|
1359
|
+
className: "pl-3 pr-0 py-2.5",
|
|
1360
|
+
onClick: (e) => e.stopPropagation(),
|
|
1361
|
+
children: /* @__PURE__ */ jsx(Checkbox, {
|
|
1362
|
+
size: "small",
|
|
1363
|
+
checked: isChecked,
|
|
1364
|
+
onCheckedChange: () => {
|
|
1365
|
+
setSelectedPaths((prev) => {
|
|
1366
|
+
const next = new Set(prev);
|
|
1367
|
+
if (next.has(file.fullPath)) next.delete(file.fullPath);
|
|
1368
|
+
else next.add(file.fullPath);
|
|
1369
|
+
return next;
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
})
|
|
1373
|
+
}),
|
|
1374
|
+
/* @__PURE__ */ jsx(TableCell, {
|
|
1375
|
+
className: "px-2 py-2.5",
|
|
1376
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1377
|
+
className: "flex items-center gap-2",
|
|
1378
|
+
children: [/* @__PURE__ */ jsx(FileIconComp, {
|
|
1379
|
+
size: iconSize.smallest,
|
|
1380
|
+
className: "text-surface-accent-400 shrink-0"
|
|
1381
|
+
}), /* @__PURE__ */ jsx(Typography, {
|
|
1382
|
+
variant: "body2",
|
|
1383
|
+
className: "text-[13px] truncate",
|
|
1384
|
+
children: file.name
|
|
1385
|
+
})]
|
|
1386
|
+
})
|
|
1387
|
+
}),
|
|
1388
|
+
/* @__PURE__ */ jsx(TableCell, {
|
|
1389
|
+
className: "px-4 py-2.5",
|
|
1390
|
+
children: /* @__PURE__ */ jsx(Typography, {
|
|
1391
|
+
variant: "caption",
|
|
1392
|
+
className: "text-text-secondary dark:text-text-secondary-dark",
|
|
1393
|
+
children: getExtension(file.name) || file.contentType?.split("/")[1]?.toUpperCase() || "—"
|
|
1394
|
+
})
|
|
1395
|
+
}),
|
|
1396
|
+
/* @__PURE__ */ jsx(TableCell, {
|
|
1397
|
+
className: "px-4 py-2.5 text-right",
|
|
1398
|
+
children: /* @__PURE__ */ jsx(Typography, {
|
|
1399
|
+
variant: "caption",
|
|
1400
|
+
className: "text-text-secondary dark:text-text-secondary-dark font-mono text-[11px]",
|
|
1401
|
+
children: file.size !== void 0 ? formatFileSize(file.size) : "—"
|
|
1402
|
+
})
|
|
1403
|
+
}),
|
|
1404
|
+
/* @__PURE__ */ jsx(TableCell, {
|
|
1405
|
+
className: "px-2 py-2.5",
|
|
1406
|
+
onClick: (e) => e.stopPropagation(),
|
|
1407
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
1408
|
+
"aria-label": t("studio_storage_delete"),
|
|
1409
|
+
size: "smallest",
|
|
1410
|
+
className: "opacity-0 group-hover:opacity-100 transition-opacity",
|
|
1411
|
+
onClick: () => {
|
|
1412
|
+
setDeleteDialogTarget(file);
|
|
1413
|
+
setDeleteDialogOpen(true);
|
|
1414
|
+
},
|
|
1415
|
+
children: /* @__PURE__ */ jsx(Trash2Icon, { size: 14 })
|
|
1416
|
+
})
|
|
1189
1417
|
})
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
}
|
|
1193
|
-
})]
|
|
1418
|
+
]
|
|
1419
|
+
}, file.fullPath);
|
|
1420
|
+
})]
|
|
1421
|
+
})]
|
|
1194
1422
|
})
|
|
1195
1423
|
});
|
|
1196
1424
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -1226,20 +1454,16 @@ var StorageView = () => {
|
|
|
1226
1454
|
children: t("studio_storage_files_heading", { count: files.length })
|
|
1227
1455
|
}), /* @__PURE__ */ jsx("div", {
|
|
1228
1456
|
className: "grid gap-3 grid-cols-[repeat(auto-fill,minmax(140px,1fr))]",
|
|
1229
|
-
children: files.map((
|
|
1230
|
-
const
|
|
1231
|
-
const
|
|
1232
|
-
const isImage = file
|
|
1233
|
-
|
|
1234
|
-
"jpeg",
|
|
1235
|
-
"png",
|
|
1236
|
-
"gif",
|
|
1237
|
-
"webp",
|
|
1238
|
-
"svg"
|
|
1239
|
-
].includes(ext);
|
|
1457
|
+
children: files.map((listed) => {
|
|
1458
|
+
const file = withMetadata(listed);
|
|
1459
|
+
const FileIconComp = getFileIcon(file.contentType, file.name);
|
|
1460
|
+
const isImage = isImageFile(file);
|
|
1461
|
+
const isChecked = selectedPaths.has(file.fullPath);
|
|
1240
1462
|
return /* @__PURE__ */ jsxs("div", {
|
|
1463
|
+
ref: observeFile,
|
|
1464
|
+
"data-storage-path": file.fullPath,
|
|
1241
1465
|
"data-storage-item": true,
|
|
1242
|
-
className: cls("rounded-lg overflow-hidden cursor-pointer border", "transition-shadow duration-150", defaultBorderMixin, "hover:shadow-md",
|
|
1466
|
+
className: cls("rounded-lg overflow-hidden cursor-pointer border", "transition-shadow duration-150", defaultBorderMixin, "hover:shadow-md", isChecked && "ring-2 ring-primary"),
|
|
1243
1467
|
onClick: (e) => handleItemClick(file, e),
|
|
1244
1468
|
onDoubleClick: () => handleItemDoubleClick(file),
|
|
1245
1469
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
@@ -1489,7 +1713,7 @@ var StorageView = () => {
|
|
|
1489
1713
|
}), selectedFile && /* @__PURE__ */ jsx("div", {
|
|
1490
1714
|
className: "w-80 lg:w-96 shrink-0",
|
|
1491
1715
|
children: /* @__PURE__ */ jsx(FilePreviewPanel, {
|
|
1492
|
-
file: selectedFile,
|
|
1716
|
+
file: withLinkFacts(withMetadata(selectedFile), selectedLink),
|
|
1493
1717
|
link: selectedLink,
|
|
1494
1718
|
mintLink: refreshPreviewLink,
|
|
1495
1719
|
onClose: () => {
|
|
@@ -1506,6 +1730,39 @@ var StorageView = () => {
|
|
|
1506
1730
|
onClose: () => setUploadDialogOpen(false),
|
|
1507
1731
|
onUpload: handleUpload
|
|
1508
1732
|
}),
|
|
1733
|
+
/* @__PURE__ */ jsxs(Dialog, {
|
|
1734
|
+
open: replacePrompt !== null,
|
|
1735
|
+
onOpenChange: (open) => {
|
|
1736
|
+
if (!open) replacePrompt?.answer(false);
|
|
1737
|
+
},
|
|
1738
|
+
children: [
|
|
1739
|
+
/* @__PURE__ */ jsx(DialogTitle, {
|
|
1740
|
+
hidden: true,
|
|
1741
|
+
children: t("studio_storage_replace_title")
|
|
1742
|
+
}),
|
|
1743
|
+
/* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsx(Typography, {
|
|
1744
|
+
variant: "subtitle1",
|
|
1745
|
+
className: "font-semibold mb-2",
|
|
1746
|
+
children: t("studio_storage_replace_question", { count: replacePrompt?.names.length ?? 0 })
|
|
1747
|
+
}), /* @__PURE__ */ jsx(Typography, {
|
|
1748
|
+
variant: "body2",
|
|
1749
|
+
color: "secondary",
|
|
1750
|
+
children: t("studio_storage_replace_body", {
|
|
1751
|
+
count: replacePrompt?.names.length ?? 0,
|
|
1752
|
+
names: replacePrompt?.names.join(", ") ?? ""
|
|
1753
|
+
})
|
|
1754
|
+
})] }),
|
|
1755
|
+
/* @__PURE__ */ jsxs(DialogActions, { children: [/* @__PURE__ */ jsx(Button, {
|
|
1756
|
+
variant: "text",
|
|
1757
|
+
onClick: () => replacePrompt?.answer(false),
|
|
1758
|
+
children: t("studio_storage_cancel")
|
|
1759
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
1760
|
+
variant: "filled",
|
|
1761
|
+
onClick: () => replacePrompt?.answer(true),
|
|
1762
|
+
children: t("studio_storage_replace")
|
|
1763
|
+
})] })
|
|
1764
|
+
]
|
|
1765
|
+
}),
|
|
1509
1766
|
/* @__PURE__ */ jsxs(Dialog, {
|
|
1510
1767
|
open: deleteDialogOpen,
|
|
1511
1768
|
onOpenChange: (open) => {
|
|
@@ -1617,4 +1874,4 @@ var StorageView = () => {
|
|
|
1617
1874
|
//#endregion
|
|
1618
1875
|
export { StorageView };
|
|
1619
1876
|
|
|
1620
|
-
//# sourceMappingURL=StorageView-
|
|
1877
|
+
//# sourceMappingURL=StorageView-CPkDKbsX.js.map
|