@rebasepro/studio 0.22.0 → 0.24.0

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.
Files changed (62) hide show
  1. package/dist/{ApiExplorer-D-jLWAaY.js → ApiExplorer-CCtczj3F.js} +16 -32
  2. package/dist/ApiExplorer-CCtczj3F.js.map +1 -0
  3. package/dist/ApiKeysView-B66TdzhH.js +1738 -0
  4. package/dist/ApiKeysView-B66TdzhH.js.map +1 -0
  5. package/dist/BackupsView-DmSMzAbT.js +368 -0
  6. package/dist/BackupsView-DmSMzAbT.js.map +1 -0
  7. package/dist/{BranchesView-D6Sz1a5f.js → BranchesView-DP1a87E-.js} +12 -8
  8. package/dist/BranchesView-DP1a87E-.js.map +1 -0
  9. package/dist/{CronJobsView-JwRjQCde.js → CronJobsView-TDErHTuK.js} +143 -68
  10. package/dist/CronJobsView-TDErHTuK.js.map +1 -0
  11. package/dist/{JSEditor-qiuwmcvk.js → JSEditor-B59pwg4c.js} +120 -74
  12. package/dist/JSEditor-B59pwg4c.js.map +1 -0
  13. package/dist/{LogsExplorer-Xq8K4oo6.js → LogsExplorer-BKM4yfP8.js} +48 -22
  14. package/dist/LogsExplorer-BKM4yfP8.js.map +1 -0
  15. package/dist/{MonacoEditor-D0_QljcX.js → MonacoEditor-CUxy5_k5.js} +11 -9
  16. package/dist/MonacoEditor-CUxy5_k5.js.map +1 -0
  17. package/dist/{RLSEditor-B-qrgdHy.js → RLSEditor-BlLLV6xs.js} +350 -98
  18. package/dist/RLSEditor-BlLLV6xs.js.map +1 -0
  19. package/dist/{SQLEditor-sdtoZv5F.js → SQLEditor-1An7w0j6.js} +496 -224
  20. package/dist/SQLEditor-1An7w0j6.js.map +1 -0
  21. package/dist/{SchemaVisualizer-DtDzIUNy.js → SchemaVisualizer-DCLVfA5t.js} +20 -24
  22. package/dist/SchemaVisualizer-DCLVfA5t.js.map +1 -0
  23. package/dist/{StorageView-C52dSZNf.js → StorageView-CbsHuCBf.js} +382 -228
  24. package/dist/StorageView-CbsHuCBf.js.map +1 -0
  25. package/dist/components/ApiExplorer/TryItPanel.d.ts +6 -1
  26. package/dist/components/ApiKeys/CreateApiKeyDialog.d.ts +21 -2
  27. package/dist/components/ApiKeys/scope-words.d.ts +37 -0
  28. package/dist/components/ApiKeys/scopes.d.ts +113 -0
  29. package/dist/components/AuthSimulationSelector.d.ts +5 -1
  30. package/dist/components/LogsExplorer/useLogTail.d.ts +9 -0
  31. package/dist/components/RLSEditor/policyListing.d.ts +44 -0
  32. package/dist/components/RLSEditor/policyRules.d.ts +49 -0
  33. package/dist/components/SQLEditor/MonacoEditor.d.ts +13 -1
  34. package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +0 -1
  35. package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +6 -0
  36. package/dist/components/load-failure.d.ts +1 -1
  37. package/dist/components/useRunAsUsers.d.ts +25 -0
  38. package/dist/index.es.js +11 -11
  39. package/dist/{load-failure-C6OMJSGC.js → load-failure-Bd8lFIO7.js} +2 -2
  40. package/dist/load-failure-Bd8lFIO7.js.map +1 -0
  41. package/dist/useRunAsUsers-CLaAtXt6.js +122 -0
  42. package/dist/useRunAsUsers-CLaAtXt6.js.map +1 -0
  43. package/dist/utils/sql_utils.d.ts +126 -36
  44. package/package.json +9 -9
  45. package/dist/ApiExplorer-D-jLWAaY.js.map +0 -1
  46. package/dist/ApiKeysView-OfJLH2x7.js +0 -1239
  47. package/dist/ApiKeysView-OfJLH2x7.js.map +0 -1
  48. package/dist/AuthSimulationSelector-BewmhMBw.js +0 -56
  49. package/dist/AuthSimulationSelector-BewmhMBw.js.map +0 -1
  50. package/dist/BackupsView-DYTAD6aW.js +0 -253
  51. package/dist/BackupsView-DYTAD6aW.js.map +0 -1
  52. package/dist/BranchesView-D6Sz1a5f.js.map +0 -1
  53. package/dist/CronJobsView-JwRjQCde.js.map +0 -1
  54. package/dist/JSEditor-qiuwmcvk.js.map +0 -1
  55. package/dist/LogsExplorer-Xq8K4oo6.js.map +0 -1
  56. package/dist/MonacoEditor-D0_QljcX.js.map +0 -1
  57. package/dist/RLSEditor-B-qrgdHy.js.map +0 -1
  58. package/dist/SQLEditor-sdtoZv5F.js.map +0 -1
  59. package/dist/SchemaVisualizer-DtDzIUNy.js.map +0 -1
  60. package/dist/StorageView-C52dSZNf.js.map +0 -1
  61. package/dist/components/ApiKeys/permissions.d.ts +0 -66
  62. package/dist/load-failure-C6OMJSGC.js.map +0 -1
@@ -1,12 +1,25 @@
1
- import { t as classifyLoadFailure } from "./load-failure-C6OMJSGC.js";
2
- import { ErrorView, useApiBase, useApiConfig, useSnackbarController, useStorageSource, useStorageSources } from "@rebasepro/app";
1
+ import { t as classifyLoadFailure } from "./load-failure-Bd8lFIO7.js";
2
+ import { ErrorView, useApiBase, useApiConfig, useSnackbarController, useStorageSource, useStorageSources, useTranslation } from "@rebasepro/app";
3
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 } from "@rebasepro/ui";
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";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { useSearchParams } from "react-router";
7
- import { DEFAULT_STORAGE_SOURCE_KEY } from "@rebasepro/types";
7
+ import { DEFAULT_STORAGE_SOURCE_KEY, isPublicStorageKey } from "@rebasepro/types";
8
8
  import { useDropzone } from "react-dropzone";
9
9
  //#region src/components/StorageView/StorageView.tsx
10
+ function fileLinkOf(config, fullPath) {
11
+ if (!config.url) return null;
12
+ const metadata = config.metadata;
13
+ return {
14
+ url: config.url,
15
+ expiresInSeconds: metadata?.token && typeof metadata.tokenExpiresIn === "number" ? metadata.tokenExpiresIn : void 0,
16
+ isPublic: metadata ? metadata.public === true : isPublicStorageKey(fullPath)
17
+ };
18
+ }
19
+ /** A URL the browser can open from anywhere: relative ones are this page's origin's. */
20
+ function absoluteUrl(url) {
21
+ return url.startsWith("http") ? url : `${window.location.origin}${url.startsWith("/") ? "" : "/"}${url}`;
22
+ }
10
23
  /**
11
24
  * The query parameter holding the folder being browsed.
12
25
  *
@@ -32,14 +45,14 @@ function getExtension(name) {
32
45
  const parts = name.split(".");
33
46
  return parts.length > 1 ? parts[parts.length - 1].toUpperCase() : "";
34
47
  }
35
- function breadcrumbSegments(path) {
48
+ function breadcrumbSegments(path, rootLabel) {
36
49
  if (!path || path === "/") return [{
37
- label: "Root",
50
+ label: rootLabel,
38
51
  path: ""
39
52
  }];
40
53
  const parts = path.split("/").filter(Boolean);
41
54
  const segments = [{
42
- label: "Root",
55
+ label: rootLabel,
43
56
  path: ""
44
57
  }];
45
58
  let accumulated = "";
@@ -52,7 +65,43 @@ function breadcrumbSegments(path) {
52
65
  }
53
66
  return segments;
54
67
  }
68
+ /**
69
+ * Everything directly under `prefix`, across every page.
70
+ *
71
+ * A listing answers a page at a time — a thousand entries on the built-in
72
+ * controllers — and hands back a token for the next. Reading only the first
73
+ * showed a large folder cut short, and deleted the first page of it.
74
+ */
75
+ async function listAllPages(source, prefix) {
76
+ const items = [];
77
+ const prefixes = [];
78
+ let pageToken;
79
+ do {
80
+ const page = pageToken ? await source.listObjects(prefix, { pageToken }) : await source.listObjects(prefix);
81
+ items.push(...page.items ?? []);
82
+ prefixes.push(...page.prefixes ?? []);
83
+ pageToken = page.nextPageToken;
84
+ } while (pageToken);
85
+ return {
86
+ items,
87
+ prefixes
88
+ };
89
+ }
90
+ /**
91
+ * Delete a folder: every file in it, every folder below it, and the folder
92
+ * itself — the directory on local disk, or the zero-byte `name/` marker that
93
+ * "New folder" writes on S3 and GCS, which no listing shows as an entry.
94
+ */
95
+ async function deleteFolderRecursive(source, prefix) {
96
+ const { items, prefixes } = await listAllPages(source, prefix);
97
+ for (const item of items) await source.deleteObject(item.fullPath);
98
+ for (const sub of prefixes) await deleteFolderRecursive(source, sub.fullPath);
99
+ try {
100
+ await source.deleteObject(prefix.endsWith("/") ? prefix : `${prefix}/`);
101
+ } catch {}
102
+ }
55
103
  function UploadDialog({ open, currentPath, onClose, onUpload }) {
104
+ const { t } = useTranslation();
56
105
  const [uploading, setUploading] = useState(false);
57
106
  const [selectedFiles, setSelectedFiles] = useState([]);
58
107
  const [error, setError] = useState(null);
@@ -71,14 +120,15 @@ function UploadDialog({ open, currentPath, onClose, onUpload }) {
71
120
  setSelectedFiles([]);
72
121
  onClose();
73
122
  } catch (err) {
74
- setError(err instanceof Error ? err.message : "Upload failed");
123
+ setError(err instanceof Error ? err.message : t("studio_storage_upload_failed"));
75
124
  } finally {
76
125
  setUploading(false);
77
126
  }
78
127
  }, [
79
128
  selectedFiles,
80
129
  onUpload,
81
- onClose
130
+ onClose,
131
+ t
82
132
  ]);
83
133
  const handleClose = useCallback(() => {
84
134
  if (!uploading) {
@@ -92,13 +142,17 @@ function UploadDialog({ open, currentPath, onClose, onUpload }) {
92
142
  onOpenChange: (o) => !o && handleClose(),
93
143
  maxWidth: "md",
94
144
  children: [
95
- /* @__PURE__ */ jsxs(DialogTitle, { children: ["Upload Files", /* @__PURE__ */ jsxs(Typography, {
145
+ /* @__PURE__ */ jsxs(DialogTitle, { children: [t("studio_storage_upload_dialog_title"), /* @__PURE__ */ jsxs(Typography, {
96
146
  variant: "caption",
97
147
  className: "text-text-secondary dark:text-text-secondary-dark mt-0.5 block",
98
- children: ["to ", /* @__PURE__ */ jsxs("span", {
99
- className: "font-mono text-primary",
100
- children: ["/", currentPath || "root"]
101
- })]
148
+ children: [
149
+ t("studio_storage_upload_to"),
150
+ " ",
151
+ /* @__PURE__ */ jsxs("span", {
152
+ className: "font-mono text-primary",
153
+ children: ["/", currentPath || t("studio_storage_root_folder")]
154
+ })
155
+ ]
102
156
  })] }),
103
157
  /* @__PURE__ */ jsxs(DialogContent, {
104
158
  className: "space-y-4",
@@ -112,12 +166,12 @@ function UploadDialog({ open, currentPath, onClose, onUpload }) {
112
166
  /* @__PURE__ */ jsx(UploadCloudIcon, { className: "text-surface-accent-400 mb-2 w-8 h-8" }),
113
167
  /* @__PURE__ */ jsx(Typography, {
114
168
  variant: "label",
115
- children: "Drop files here or click to browse"
169
+ children: t("studio_storage_drop_or_browse")
116
170
  }),
117
171
  /* @__PURE__ */ jsx(Typography, {
118
172
  variant: "caption",
119
173
  color: "secondary",
120
- children: "Any file type supported"
174
+ children: t("studio_storage_any_file_type")
121
175
  })
122
176
  ]
123
177
  })
@@ -129,14 +183,10 @@ function UploadDialog({ open, currentPath, onClose, onUpload }) {
129
183
  }),
130
184
  selectedFiles.length > 0 && /* @__PURE__ */ jsxs("div", {
131
185
  className: "space-y-2",
132
- children: [/* @__PURE__ */ jsxs(Typography, {
186
+ children: [/* @__PURE__ */ jsx(Typography, {
133
187
  variant: "caption",
134
188
  color: "secondary",
135
- children: [
136
- "Selected files (",
137
- selectedFiles.length,
138
- ")"
139
- ]
189
+ children: t("studio_storage_selected_files", { count: selectedFiles.length })
140
190
  }), /* @__PURE__ */ jsx("div", {
141
191
  className: "max-h-40 overflow-auto space-y-1",
142
192
  children: selectedFiles.map((file, index) => /* @__PURE__ */ jsxs("div", {
@@ -170,24 +220,45 @@ function UploadDialog({ open, currentPath, onClose, onUpload }) {
170
220
  variant: "text",
171
221
  onClick: handleClose,
172
222
  disabled: uploading,
173
- children: "Cancel"
223
+ children: t("studio_storage_cancel")
174
224
  }), /* @__PURE__ */ jsx(Button, {
175
225
  variant: "filled",
176
226
  onClick: handleUpload,
177
227
  disabled: selectedFiles.length === 0 || uploading,
178
228
  startIcon: uploading ? /* @__PURE__ */ jsx(CircularProgress, { size: "smallest" }) : /* @__PURE__ */ jsx(UploadCloudIcon, { size: 14 }),
179
- children: uploading ? "Uploading..." : `Upload${selectedFiles.length > 0 ? ` (${selectedFiles.length})` : ""}`
229
+ children: uploading ? t("studio_storage_uploading") : selectedFiles.length > 0 ? t("studio_storage_upload_count", { count: selectedFiles.length }) : t("studio_storage_upload")
180
230
  })] })
181
231
  ]
182
232
  });
183
233
  }
184
- function FilePreviewPanel({ file, onClose, onDelete, downloadUrl }) {
234
+ function FilePreviewPanel({ file, onClose, onDelete, link, mintLink }) {
235
+ const { t } = useTranslation();
236
+ const snackbarController = useSnackbarController();
185
237
  file.contentType?.startsWith("image/");
186
238
  file.contentType?.startsWith("video/");
187
239
  file.contentType?.startsWith("audio/");
188
240
  const FileIconComponent = getFileIcon(file.contentType);
189
241
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
190
242
  const [urlCopied, setUrlCopied] = useState(false);
243
+ const reportLinkError = (e) => {
244
+ snackbarController.open({
245
+ type: "error",
246
+ message: e instanceof Error ? e.message : String(e)
247
+ });
248
+ };
249
+ const handleDownload = () => {
250
+ mintLink().then((fresh) => {
251
+ if (fresh) window.open(fresh.url, "_blank");
252
+ }).catch(reportLinkError);
253
+ };
254
+ const handleCopy = () => {
255
+ mintLink().then(async (fresh) => {
256
+ if (!fresh) return;
257
+ await navigator.clipboard.writeText(absoluteUrl(fresh.url));
258
+ setUrlCopied(true);
259
+ setTimeout(() => setUrlCopied(false), 2e3);
260
+ }).catch(reportLinkError);
261
+ };
191
262
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
192
263
  className: cls("flex flex-col h-full border-l", defaultBorderMixin, "bg-surface-card"),
193
264
  children: [
@@ -200,17 +271,19 @@ function FilePreviewPanel({ file, onClose, onDelete, downloadUrl }) {
200
271
  }), /* @__PURE__ */ jsxs("div", {
201
272
  className: "flex items-center gap-0.5",
202
273
  children: [
203
- downloadUrl && /* @__PURE__ */ jsx(Tooltip, {
204
- title: "Download",
274
+ link && /* @__PURE__ */ jsx(Tooltip, {
275
+ title: t("studio_storage_download"),
205
276
  children: /* @__PURE__ */ jsx(IconButton, {
277
+ "aria-label": t("studio_storage_download"),
206
278
  size: "small",
207
- onClick: () => window.open(downloadUrl, "_blank"),
279
+ onClick: handleDownload,
208
280
  children: /* @__PURE__ */ jsx(DownloadIcon, { size: iconSize.smallest })
209
281
  })
210
282
  }),
211
283
  /* @__PURE__ */ jsx(Tooltip, {
212
- title: "Delete",
284
+ title: t("studio_storage_delete"),
213
285
  children: /* @__PURE__ */ jsx(IconButton, {
286
+ "aria-label": t("studio_storage_delete"),
214
287
  size: "small",
215
288
  onClick: () => setDeleteDialogOpen(true),
216
289
  className: "text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20",
@@ -251,21 +324,21 @@ function FilePreviewPanel({ file, onClose, onDelete, downloadUrl }) {
251
324
  "ogg",
252
325
  "m4a"
253
326
  ].includes(ext);
254
- const downloadUrl = file.downloadUrl;
255
- if (isImage && downloadUrl) return /* @__PURE__ */ jsx("img", {
256
- src: downloadUrl,
327
+ const previewUrl = link?.url;
328
+ if (isImage && previewUrl) return /* @__PURE__ */ jsx("img", {
329
+ src: previewUrl,
257
330
  alt: file.name,
258
331
  className: "max-w-full max-h-[400px] object-contain rounded-md shadow-sm"
259
332
  });
260
- else if (isVideo && downloadUrl) return /* @__PURE__ */ jsx("video", {
261
- src: downloadUrl,
333
+ else if (isVideo && previewUrl) return /* @__PURE__ */ jsx("video", {
334
+ src: previewUrl,
262
335
  className: "max-w-full max-h-[400px] rounded-md",
263
336
  controls: true
264
337
  });
265
- else if (isAudio && downloadUrl) return /* @__PURE__ */ jsxs("div", {
338
+ else if (isAudio && previewUrl) return /* @__PURE__ */ jsxs("div", {
266
339
  className: "flex flex-col items-center gap-4",
267
340
  children: [/* @__PURE__ */ jsx(Music2Icon, { className: "text-surface-accent-400 w-10 h-10" }), /* @__PURE__ */ jsx("audio", {
268
- src: downloadUrl,
341
+ src: previewUrl,
269
342
  controls: true,
270
343
  className: "w-full max-w-xs"
271
344
  })]
@@ -275,7 +348,7 @@ function FilePreviewPanel({ file, onClose, onDelete, downloadUrl }) {
275
348
  children: [/* @__PURE__ */ jsx(FileIconComponent, { className: "w-10 h-10" }), /* @__PURE__ */ jsx(Typography, {
276
349
  variant: "caption",
277
350
  className: "text-text-disabled dark:text-text-disabled-dark",
278
- children: "No preview available"
351
+ children: t("studio_storage_no_preview")
279
352
  })]
280
353
  });
281
354
  })()
@@ -287,7 +360,7 @@ function FilePreviewPanel({ file, onClose, onDelete, downloadUrl }) {
287
360
  /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Typography, {
288
361
  variant: "caption",
289
362
  className: "text-text-disabled dark:text-text-disabled-dark text-[10px] uppercase tracking-wider font-semibold mb-1 block",
290
- children: "File Info"
363
+ children: t("studio_storage_file_info")
291
364
  }) }),
292
365
  /* @__PURE__ */ jsxs("div", {
293
366
  className: "grid grid-cols-2 gap-3",
@@ -295,7 +368,7 @@ function FilePreviewPanel({ file, onClose, onDelete, downloadUrl }) {
295
368
  /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Typography, {
296
369
  variant: "caption",
297
370
  className: "text-surface-accent-500 text-[11px]",
298
- children: "Name"
371
+ children: t("studio_storage_name")
299
372
  }), /* @__PURE__ */ jsx(Typography, {
300
373
  variant: "body2",
301
374
  className: "text-[13px] break-all",
@@ -304,16 +377,16 @@ function FilePreviewPanel({ file, onClose, onDelete, downloadUrl }) {
304
377
  /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Typography, {
305
378
  variant: "caption",
306
379
  className: "text-surface-accent-500 text-[11px]",
307
- children: "Type"
380
+ children: t("studio_storage_type")
308
381
  }), /* @__PURE__ */ jsx(Typography, {
309
382
  variant: "body2",
310
383
  className: "text-[13px]",
311
- children: file.contentType || "Unknown"
384
+ children: file.contentType || t("studio_storage_type_unknown")
312
385
  })] }),
313
386
  file.size !== void 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Typography, {
314
387
  variant: "caption",
315
388
  className: "text-surface-accent-500 text-[11px]",
316
- children: "Size"
389
+ children: t("studio_storage_size")
317
390
  }), /* @__PURE__ */ jsx(Typography, {
318
391
  variant: "body2",
319
392
  className: "text-[13px]",
@@ -322,7 +395,7 @@ function FilePreviewPanel({ file, onClose, onDelete, downloadUrl }) {
322
395
  /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Typography, {
323
396
  variant: "caption",
324
397
  className: "text-surface-accent-500 text-[11px]",
325
- children: "Extension"
398
+ children: t("studio_storage_extension")
326
399
  }), /* @__PURE__ */ jsx(Typography, {
327
400
  variant: "body2",
328
401
  className: "text-[13px] font-mono",
@@ -333,7 +406,7 @@ function FilePreviewPanel({ file, onClose, onDelete, downloadUrl }) {
333
406
  children: [/* @__PURE__ */ jsx(Typography, {
334
407
  variant: "caption",
335
408
  className: "text-surface-accent-500 text-[11px]",
336
- children: "Path"
409
+ children: t("studio_storage_path")
337
410
  }), /* @__PURE__ */ jsx(Typography, {
338
411
  variant: "body2",
339
412
  className: "text-[13px] font-mono break-all",
@@ -342,41 +415,47 @@ function FilePreviewPanel({ file, onClose, onDelete, downloadUrl }) {
342
415
  })
343
416
  ]
344
417
  }),
345
- downloadUrl && /* @__PURE__ */ jsxs("div", {
418
+ link && /* @__PURE__ */ jsxs("div", {
346
419
  className: "pt-2",
347
- children: [/* @__PURE__ */ jsx(Typography, {
348
- variant: "caption",
349
- className: "text-surface-accent-500 text-[11px] block mb-1",
350
- children: "URL"
351
- }), /* @__PURE__ */ jsxs("div", {
352
- className: cls("flex items-center gap-2 p-2 rounded cursor-pointer transition-colors", "bg-surface-raised hover:bg-surface-raised-hover"),
353
- onClick: () => {
354
- const fullUrl = downloadUrl.startsWith("http") ? downloadUrl : `${window.location.origin}${downloadUrl.startsWith("/") ? "" : "/"}${downloadUrl}`;
355
- navigator.clipboard.writeText(fullUrl).then(() => {
356
- setUrlCopied(true);
357
- setTimeout(() => setUrlCopied(false), 2e3);
358
- });
359
- },
360
- children: [/* @__PURE__ */ jsx(Typography, {
420
+ children: [
421
+ /* @__PURE__ */ jsx(Typography, {
361
422
  variant: "caption",
362
- className: "font-mono text-[11px] truncate flex-1 min-w-0 text-primary",
363
- children: (() => {
364
- return downloadUrl.startsWith("http") ? downloadUrl : `${window.location.origin}${downloadUrl.startsWith("/") ? "" : "/"}${downloadUrl}`;
365
- })()
366
- }), /* @__PURE__ */ jsx(Tooltip, {
367
- title: urlCopied ? "Copied!" : "Copy URL",
368
- children: /* @__PURE__ */ jsx("div", {
369
- className: "shrink-0",
370
- children: urlCopied ? /* @__PURE__ */ jsx(CheckIcon, {
371
- size: 14,
372
- className: "text-green-500"
373
- }) : /* @__PURE__ */ jsx(CopyIcon, {
374
- size: 14,
375
- className: "text-surface-accent-400"
423
+ className: "text-surface-accent-500 text-[11px] block mb-1",
424
+ children: t("studio_storage_url")
425
+ }),
426
+ /* @__PURE__ */ jsxs("div", {
427
+ className: cls("flex items-center gap-2 p-2 rounded cursor-pointer transition-colors", "bg-surface-raised hover:bg-surface-raised-hover"),
428
+ onClick: handleCopy,
429
+ children: [/* @__PURE__ */ jsx(Typography, {
430
+ variant: "caption",
431
+ className: "font-mono text-[11px] truncate flex-1 min-w-0 text-primary",
432
+ children: absoluteUrl(link.url)
433
+ }), /* @__PURE__ */ jsx(Tooltip, {
434
+ title: urlCopied ? t("studio_storage_url_copied") : t("studio_storage_copy_url"),
435
+ children: /* @__PURE__ */ jsx("div", {
436
+ className: "shrink-0",
437
+ children: urlCopied ? /* @__PURE__ */ jsx(CheckIcon, {
438
+ size: 14,
439
+ className: "text-green-500"
440
+ }) : /* @__PURE__ */ jsx(CopyIcon, {
441
+ size: 14,
442
+ className: "text-surface-accent-400"
443
+ })
376
444
  })
377
- })
378
- })]
379
- })]
445
+ })]
446
+ }),
447
+ link.isPublic ? /* @__PURE__ */ jsx(Typography, {
448
+ variant: "caption",
449
+ color: "secondary",
450
+ className: "block mt-1 text-[11px]",
451
+ children: t("studio_storage_url_public")
452
+ }) : link.expiresInSeconds !== void 0 ? /* @__PURE__ */ jsx(Typography, {
453
+ variant: "caption",
454
+ color: "secondary",
455
+ className: "block mt-1 text-[11px]",
456
+ children: t("studio_storage_url_temporary", { minutes: Math.max(1, Math.round(link.expiresInSeconds / 60)) })
457
+ }) : null
458
+ ]
380
459
  })
381
460
  ]
382
461
  })
@@ -387,24 +466,20 @@ function FilePreviewPanel({ file, onClose, onDelete, downloadUrl }) {
387
466
  children: [
388
467
  /* @__PURE__ */ jsx(DialogTitle, {
389
468
  hidden: true,
390
- children: "Delete File"
469
+ children: t("studio_storage_delete_file_title")
391
470
  }),
392
471
  /* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsx(Typography, {
393
472
  variant: "subtitle1",
394
473
  className: "mb-2",
395
- children: "Delete File?"
396
- }), /* @__PURE__ */ jsxs(Typography, {
474
+ children: t("studio_storage_delete_file_question")
475
+ }), /* @__PURE__ */ jsx(Typography, {
397
476
  className: "text-surface-accent-600 dark:text-surface-accent-400",
398
- children: [
399
- "Are you sure you want to delete \"",
400
- file.name,
401
- "\"? This action cannot be undone."
402
- ]
477
+ children: t("studio_storage_delete_file_body", { name: file.name })
403
478
  })] }),
404
479
  /* @__PURE__ */ jsxs(DialogActions, { children: [/* @__PURE__ */ jsx(Button, {
405
480
  variant: "text",
406
481
  onClick: () => setDeleteDialogOpen(false),
407
- children: "Cancel"
482
+ children: t("studio_storage_cancel")
408
483
  }), /* @__PURE__ */ jsx(Button, {
409
484
  variant: "filled",
410
485
  color: "error",
@@ -412,12 +487,13 @@ function FilePreviewPanel({ file, onClose, onDelete, downloadUrl }) {
412
487
  setDeleteDialogOpen(false);
413
488
  onDelete();
414
489
  },
415
- children: "Delete"
490
+ children: t("studio_storage_delete")
416
491
  })] })
417
492
  ]
418
493
  })] });
419
494
  }
420
495
  var StorageView = () => {
496
+ const { t } = useTranslation();
421
497
  const defaultStorageSource = useStorageSource();
422
498
  const storageSources = useStorageSources();
423
499
  const snackbarController = useSnackbarController();
@@ -436,7 +512,9 @@ var StorageView = () => {
436
512
  const [folders, setFolders] = useState([]);
437
513
  const [files, setFiles] = useState([]);
438
514
  const [selectedFile, setSelectedFile] = useState(null);
439
- const [selectedDownloadUrl, setSelectedDownloadUrl] = useState(null);
515
+ const [selectedLink, setSelectedLink] = useState(null);
516
+ /** The latest link asked for the preview: an earlier one answering later is dropped. */
517
+ const linkRequestRef = useRef(0);
440
518
  const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
441
519
  const [viewMode, setViewMode] = useState("grid");
442
520
  const [selectedPaths, setSelectedPaths] = useState(/* @__PURE__ */ new Set());
@@ -453,19 +531,29 @@ var StorageView = () => {
453
531
  useEffect(() => {
454
532
  storageSourceRef.current = storageSource;
455
533
  }, [storageSource]);
534
+ /**
535
+ * The latest listing asked for. Only its answer is shown: a listing awaits
536
+ * every file's metadata, so an earlier one — of the source or folder the
537
+ * view has since left — can answer after a later one, and showed that
538
+ * source's files under the one selected, where their actions ran.
539
+ */
540
+ const listingRequestRef = useRef(0);
456
541
  const fetchContents = useCallback(async (path) => {
542
+ const request = ++listingRequestRef.current;
543
+ const isCurrent = () => request === listingRequestRef.current;
544
+ const source = storageSourceRef.current;
457
545
  setLoading(true);
458
546
  setFailure(null);
459
547
  try {
460
- const result = await storageSourceRef.current.listObjects(path);
461
- const folderItems = (result.prefixes ?? []).map((ref) => ({
548
+ const result = await listAllPages(source, path);
549
+ const folderItems = result.prefixes.map((ref) => ({
462
550
  name: ref.name,
463
551
  fullPath: ref.fullPath,
464
552
  isFolder: true
465
553
  }));
466
- const fileItems = await Promise.all((result.items ?? []).map(async (ref) => {
554
+ const fileItems = await Promise.all(result.items.map(async (ref) => {
467
555
  try {
468
- const downloadConfig = await storageSourceRef.current.getSignedUrl(ref.fullPath);
556
+ const downloadConfig = await source.getSignedUrl(ref.fullPath);
469
557
  return {
470
558
  name: ref.name,
471
559
  fullPath: ref.fullPath,
@@ -482,13 +570,15 @@ var StorageView = () => {
482
570
  };
483
571
  }
484
572
  }));
573
+ if (!isCurrent()) return;
485
574
  setFolders(folderItems);
486
575
  setFiles(fileItems);
487
576
  } catch (e) {
577
+ if (!isCurrent()) return;
488
578
  console.error("Storage list error:", e);
489
579
  setFailure(classifyLoadFailure(e));
490
580
  } finally {
491
- setLoading(false);
581
+ if (isCurrent()) setLoading(false);
492
582
  }
493
583
  }, []);
494
584
  useEffect(() => {
@@ -498,6 +588,33 @@ var StorageView = () => {
498
588
  fetchContents,
499
589
  selectedSourceKey
500
590
  ]);
591
+ /** A link to `file`, minted now through the source being browsed — see {@link FileLink}. */
592
+ const mintFileLink = useCallback(async (file) => fileLinkOf(await storageSourceRef.current.getSignedUrl(file.fullPath), file.fullPath), []);
593
+ /** Open `file` in the preview, with a link minted for it now. */
594
+ const openPreview = useCallback((file) => {
595
+ const request = ++linkRequestRef.current;
596
+ setSelectedFile(file);
597
+ setSelectedLink(null);
598
+ mintFileLink(file).then((link) => {
599
+ if (request === linkRequestRef.current) setSelectedLink(link);
600
+ }).catch(() => {
601
+ if (request === linkRequestRef.current) setSelectedLink(null);
602
+ });
603
+ }, [mintFileLink]);
604
+ /** A fresh link to the previewed file, for a download or a copy. It replaces the one shown. */
605
+ const refreshPreviewLink = useCallback(async () => {
606
+ if (!selectedFile) return null;
607
+ const request = ++linkRequestRef.current;
608
+ const link = await mintFileLink(selectedFile);
609
+ if (request === linkRequestRef.current) setSelectedLink(link);
610
+ return link;
611
+ }, [selectedFile, mintFileLink]);
612
+ const clearSelection = useCallback(() => {
613
+ setSelectedFile(null);
614
+ setSelectedLink(null);
615
+ setSelectedPaths(/* @__PURE__ */ new Set());
616
+ lastClickedRef.current = null;
617
+ }, []);
501
618
  const handleNavigate = useCallback((path) => {
502
619
  setSearchParams((prev) => {
503
620
  const next = new URLSearchParams(prev);
@@ -505,11 +622,12 @@ var StorageView = () => {
505
622
  else next.delete(STORAGE_PATH_PARAM);
506
623
  return next;
507
624
  }, { replace: true });
508
- setSelectedFile(null);
509
- setSelectedDownloadUrl(null);
510
- setSelectedPaths(/* @__PURE__ */ new Set());
511
- lastClickedRef.current = null;
512
- }, [setSearchParams]);
625
+ clearSelection();
626
+ }, [setSearchParams, clearSelection]);
627
+ const handleSourceChange = useCallback((key) => {
628
+ setSelectedSourceKey(key);
629
+ clearSelection();
630
+ }, [clearSelection]);
513
631
  const handleNavigateUp = useCallback(() => {
514
632
  const parts = currentPath.split("/").filter(Boolean);
515
633
  parts.pop();
@@ -541,24 +659,17 @@ var StorageView = () => {
541
659
  } else {
542
660
  setSelectedPaths(/* @__PURE__ */ new Set([path]));
543
661
  lastClickedRef.current = path;
544
- if (!item.isFolder) {
545
- setSelectedFile(item);
546
- if (item.downloadUrl) setSelectedDownloadUrl(item.downloadUrl);
547
- else storageSourceRef.current.getSignedUrl(item.fullPath).then((config) => setSelectedDownloadUrl(config.url)).catch(() => setSelectedDownloadUrl(null));
548
- } else {
662
+ if (!item.isFolder) openPreview(item);
663
+ else {
549
664
  setSelectedFile(null);
550
- setSelectedDownloadUrl(null);
665
+ setSelectedLink(null);
551
666
  }
552
667
  }
553
- }, [allItems]);
668
+ }, [allItems, openPreview]);
554
669
  const handleItemDoubleClick = useCallback((item) => {
555
670
  if (item.isFolder) handleNavigate(item.fullPath);
556
- else {
557
- setSelectedFile(item);
558
- if (item.downloadUrl) setSelectedDownloadUrl(item.downloadUrl);
559
- else storageSourceRef.current.getSignedUrl(item.fullPath).then((config) => setSelectedDownloadUrl(config.url)).catch(() => setSelectedDownloadUrl(null));
560
- }
561
- }, [handleNavigate]);
671
+ else openPreview(item);
672
+ }, [handleNavigate, openPreview]);
562
673
  const handleUpload = useCallback(async (uploadFiles) => {
563
674
  for (const file of uploadFiles) {
564
675
  const key = currentPath ? `${currentPath}/${file.name}` : file.name;
@@ -569,13 +680,14 @@ var StorageView = () => {
569
680
  }
570
681
  snackbarController.open({
571
682
  type: "success",
572
- message: `${uploadFiles.length} file${uploadFiles.length > 1 ? "s" : ""} uploaded successfully`
683
+ message: t("studio_storage_files_uploaded", { count: uploadFiles.length })
573
684
  });
574
685
  await fetchContents(currentPath);
575
686
  }, [
576
687
  currentPath,
577
688
  snackbarController,
578
- fetchContents
689
+ fetchContents,
690
+ t
579
691
  ]);
580
692
  const handleCreateFolder = useCallback(async () => {
581
693
  if (!newFolderName.trim() || !apiConfig?.apiUrl) return;
@@ -583,14 +695,14 @@ var StorageView = () => {
583
695
  if (name.includes("/") || name.includes("\\")) {
584
696
  snackbarController.open({
585
697
  type: "error",
586
- message: "Folder name cannot contain slashes"
698
+ message: t("studio_storage_folder_name_has_slash")
587
699
  });
588
700
  return;
589
701
  }
590
702
  if (folders.find((f) => f.name === name)) {
591
703
  snackbarController.open({
592
704
  type: "error",
593
- message: `Folder "${name}" already exists`
705
+ message: t("studio_storage_folder_exists", { name })
594
706
  });
595
707
  return;
596
708
  }
@@ -604,15 +716,18 @@ var StorageView = () => {
604
716
  "Content-Type": "application/json",
605
717
  ...token ? { "Authorization": `Bearer ${token}` } : {}
606
718
  },
607
- body: JSON.stringify({ path: folderPath })
719
+ body: JSON.stringify({
720
+ path: folderPath,
721
+ ...selectedSourceKey !== DEFAULT_STORAGE_SOURCE_KEY ? { storageId: selectedSourceKey } : {}
722
+ })
608
723
  });
609
724
  if (!response.ok) {
610
- const err = await response.json().catch(() => ({ error: "Failed to create folder" }));
611
- throw new Error(err.error || "Failed to create folder");
725
+ const err = await response.json().catch(() => ({ error: t("studio_storage_create_folder_failed") }));
726
+ throw new Error(err.error || t("studio_storage_create_folder_failed"));
612
727
  }
613
728
  snackbarController.open({
614
729
  type: "success",
615
- message: `Folder "${name}" created`
730
+ message: t("studio_storage_folder_created", { name })
616
731
  });
617
732
  setNewFolderDialogOpen(false);
618
733
  setNewFolderName("");
@@ -632,7 +747,9 @@ var StorageView = () => {
632
747
  apiBase,
633
748
  snackbarController,
634
749
  fetchContents,
635
- folders
750
+ folders,
751
+ selectedSourceKey,
752
+ t
636
753
  ]);
637
754
  const { getRootProps: getDropRootProps, getInputProps: getDropInputProps, isDragActive } = useDropzone({
638
755
  onDrop: useCallback(async (droppedFiles) => {
@@ -647,7 +764,7 @@ var StorageView = () => {
647
764
  }
648
765
  snackbarController.open({
649
766
  type: "success",
650
- message: `${droppedFiles.length} file${droppedFiles.length > 1 ? "s" : ""} uploaded successfully`
767
+ message: t("studio_storage_files_uploaded", { count: droppedFiles.length })
651
768
  });
652
769
  await fetchContents(currentPath);
653
770
  } catch (e) {
@@ -659,30 +776,51 @@ var StorageView = () => {
659
776
  }, [
660
777
  currentPath,
661
778
  snackbarController,
662
- fetchContents
779
+ fetchContents,
780
+ t
663
781
  ]),
664
782
  noClick: true,
665
783
  noKeyboard: true,
666
784
  noDragEventsBubbling: true
667
785
  });
668
- const deleteFolderRecursive = useCallback(async (prefix) => {
669
- const result = await storageSourceRef.current.listObjects(prefix);
670
- for (const item of result.items ?? []) await storageSourceRef.current.deleteObject(item.fullPath);
671
- for (const sub of result.prefixes ?? []) await deleteFolderRecursive(sub.fullPath);
672
- try {
673
- await storageSourceRef.current.deleteObject(prefix);
674
- } catch {}
786
+ /**
787
+ * Delete files and folders from the source being browsed, then read back
788
+ * the folder they were listed in and return the ones still there.
789
+ *
790
+ * Nothing is reported deleted on the strength of the requests alone: a
791
+ * delete can answer success and leave the object behind — a directory that
792
+ * is not empty yet, a folder marker, a hook — and the folder listing is
793
+ * what the user sees next. One source for the whole operation, taken when
794
+ * it starts.
795
+ */
796
+ const deleteItems = useCallback(async (items, folder) => {
797
+ const source = storageSourceRef.current;
798
+ for (const item of items) if (item.isFolder) await deleteFolderRecursive(source, item.fullPath);
799
+ else await source.deleteObject(item.fullPath);
800
+ const listed = await listAllPages(source, folder);
801
+ const stillListed = new Set([...listed.items, ...listed.prefixes].map((ref) => ref.fullPath));
802
+ return items.filter((item) => stillListed.has(item.fullPath));
675
803
  }, []);
804
+ /** What a delete left behind, said as the error it is rather than a success. */
805
+ const reportStillThere = useCallback((remaining) => {
806
+ snackbarController.open({
807
+ type: "error",
808
+ message: t("studio_storage_delete_incomplete", {
809
+ count: remaining.length,
810
+ names: remaining.map((item) => item.name).join(", ")
811
+ })
812
+ });
813
+ }, [snackbarController, t]);
676
814
  const handleDeleteFile = useCallback(async (file) => {
677
815
  try {
678
- if (file.isFolder) await deleteFolderRecursive(file.fullPath);
679
- else await storageSourceRef.current.deleteObject(file.fullPath);
680
- snackbarController.open({
816
+ const remaining = await deleteItems([file], currentPath);
817
+ if (remaining.length > 0) reportStillThere(remaining);
818
+ else snackbarController.open({
681
819
  type: "success",
682
- message: `"${file.name}" deleted`
820
+ message: t("studio_storage_file_deleted", { name: file.name })
683
821
  });
684
822
  setSelectedFile(null);
685
- setSelectedDownloadUrl(null);
823
+ setSelectedLink(null);
686
824
  setSelectedPaths((prev) => {
687
825
  const next = new Set(prev);
688
826
  next.delete(file.fullPath);
@@ -699,21 +837,23 @@ var StorageView = () => {
699
837
  currentPath,
700
838
  snackbarController,
701
839
  fetchContents,
702
- deleteFolderRecursive
840
+ deleteItems,
841
+ reportStillThere,
842
+ t
703
843
  ]);
704
844
  const handleBulkDelete = useCallback(async () => {
705
845
  setDeleting(true);
706
846
  try {
707
847
  const items = allItems.filter((i) => selectedPaths.has(i.fullPath));
708
- for (const item of items) if (item.isFolder) await deleteFolderRecursive(item.fullPath);
709
- else await storageSourceRef.current.deleteObject(item.fullPath);
710
- snackbarController.open({
848
+ const remaining = await deleteItems(items, currentPath);
849
+ if (remaining.length > 0) reportStillThere(remaining);
850
+ else snackbarController.open({
711
851
  type: "success",
712
- message: `${items.length} item${items.length !== 1 ? "s" : ""} deleted`
852
+ message: t("studio_storage_items_deleted", { count: items.length })
713
853
  });
714
854
  setSelectedPaths(/* @__PURE__ */ new Set());
715
855
  setSelectedFile(null);
716
- setSelectedDownloadUrl(null);
856
+ setSelectedLink(null);
717
857
  await fetchContents(currentPath);
718
858
  } catch (e) {
719
859
  snackbarController.open({
@@ -731,17 +871,24 @@ var StorageView = () => {
731
871
  currentPath,
732
872
  snackbarController,
733
873
  fetchContents,
734
- deleteFolderRecursive
874
+ deleteItems,
875
+ reportStillThere,
876
+ t
735
877
  ]);
736
- const handleConfirmDeleteFolder = useCallback(async () => {
878
+ const handleConfirmDeleteItem = useCallback(async () => {
737
879
  if (!deleteDialogTarget || deleteDialogTarget === "selection") return;
738
880
  setDeleting(true);
739
881
  try {
740
- await deleteFolderRecursive(deleteDialogTarget.fullPath);
741
- snackbarController.open({
882
+ const remaining = await deleteItems([deleteDialogTarget], currentPath);
883
+ if (remaining.length > 0) reportStillThere(remaining);
884
+ else snackbarController.open({
742
885
  type: "success",
743
- message: `Folder "${deleteDialogTarget.name}" deleted`
886
+ message: deleteDialogTarget.isFolder ? t("studio_storage_folder_deleted", { name: deleteDialogTarget.name }) : t("studio_storage_file_deleted", { name: deleteDialogTarget.name })
744
887
  });
888
+ if (!deleteDialogTarget.isFolder && selectedFile?.fullPath === deleteDialogTarget.fullPath) {
889
+ setSelectedFile(null);
890
+ setSelectedLink(null);
891
+ }
745
892
  setSelectedPaths((prev) => {
746
893
  const next = new Set(prev);
747
894
  next.delete(deleteDialogTarget.fullPath);
@@ -760,10 +907,13 @@ var StorageView = () => {
760
907
  }
761
908
  }, [
762
909
  deleteDialogTarget,
910
+ selectedFile,
763
911
  currentPath,
764
912
  snackbarController,
765
913
  fetchContents,
766
- deleteFolderRecursive
914
+ deleteItems,
915
+ reportStillThere,
916
+ t
767
917
  ]);
768
918
  const handleSelectAll = useCallback(() => {
769
919
  if (selectedPaths.size === allItems.length) setSelectedPaths(/* @__PURE__ */ new Set());
@@ -772,6 +922,7 @@ var StorageView = () => {
772
922
  useEffect(() => {
773
923
  const handler = (e) => {
774
924
  if (deleteDialogOpen || uploadDialogOpen || newFolderDialogOpen) return;
925
+ if (isKeyHandled(e)) return;
775
926
  if ((e.metaKey || e.ctrlKey) && e.key === "a") {
776
927
  e.preventDefault();
777
928
  handleSelectAll();
@@ -779,7 +930,7 @@ var StorageView = () => {
779
930
  if (e.key === "Escape") {
780
931
  setSelectedPaths(/* @__PURE__ */ new Set());
781
932
  setSelectedFile(null);
782
- setSelectedDownloadUrl(null);
933
+ setSelectedLink(null);
783
934
  }
784
935
  if ((e.key === "Delete" || e.key === "Backspace") && selectedPaths.size > 0 && !e.metaKey && !e.ctrlKey) {
785
936
  if (e.target?.tagName === "INPUT" || e.target?.tagName === "TEXTAREA") return;
@@ -800,7 +951,7 @@ var StorageView = () => {
800
951
  const handleRefresh = useCallback(() => {
801
952
  fetchContents(currentPath);
802
953
  }, [currentPath, fetchContents]);
803
- const segments = breadcrumbSegments(currentPath);
954
+ const segments = breadcrumbSegments(currentPath, t("studio_storage_root"));
804
955
  const renderContents = () => {
805
956
  if (loading) return /* @__PURE__ */ jsx("div", {
806
957
  className: "flex-grow flex items-center justify-center",
@@ -809,7 +960,7 @@ var StorageView = () => {
809
960
  children: [/* @__PURE__ */ jsx(CircularProgress, { size: "medium" }), /* @__PURE__ */ jsx(Typography, {
810
961
  variant: "body2",
811
962
  className: "mt-4 text-text-secondary dark:text-text-secondary-dark font-mono tracking-tight animate-pulse",
812
- children: "Loading..."
963
+ children: t("studio_storage_loading")
813
964
  })]
814
965
  })
815
966
  });
@@ -821,16 +972,12 @@ var StorageView = () => {
821
972
  /* @__PURE__ */ jsx(Typography, {
822
973
  variant: "subtitle2",
823
974
  className: "block",
824
- children: "This project's storage rules refused this listing"
975
+ children: t("studio_storage_denied_title")
825
976
  }),
826
- /* @__PURE__ */ jsxs(Typography, {
977
+ /* @__PURE__ */ jsx(Typography, {
827
978
  variant: "body2",
828
979
  className: "text-text-secondary dark:text-text-secondary-dark block mt-2",
829
- children: [
830
- "Nothing is wrong with the project. Its ",
831
- /* @__PURE__ */ jsx("code", { children: "storageAuthorize" }),
832
- " hook decides who may read which keys, and it declined this path for the signed-in account — commonly because a listing must name a prefix the rule recognises rather than the bucket root."
833
- ]
980
+ children: t("studio_storage_denied_hint")
834
981
  }),
835
982
  /* @__PURE__ */ jsx(Typography, {
836
983
  variant: "caption",
@@ -843,7 +990,7 @@ var StorageView = () => {
843
990
  if (failure) return /* @__PURE__ */ jsx("div", {
844
991
  className: "flex-grow flex items-center justify-center p-6 overflow-auto",
845
992
  children: /* @__PURE__ */ jsx(ErrorView, {
846
- title: "Could not read this project's storage",
993
+ title: t("studio_storage_read_failed"),
847
994
  error: failure.detail,
848
995
  onRetry: failure.retryable ? handleRefresh : void 0
849
996
  })
@@ -867,7 +1014,7 @@ var StorageView = () => {
867
1014
  }),
868
1015
  /* @__PURE__ */ jsx(Typography, {
869
1016
  variant: "body2",
870
- children: "This folder is empty"
1017
+ children: t("studio_storage_empty")
871
1018
  }),
872
1019
  /* @__PURE__ */ jsxs("div", {
873
1020
  className: "flex items-center gap-2 mt-3",
@@ -877,10 +1024,10 @@ var StorageView = () => {
877
1024
  setNewFolderName("");
878
1025
  setNewFolderDialogOpen(true);
879
1026
  },
880
- children: [/* @__PURE__ */ jsx(FolderPlusIcon, { size: iconSize.smallest }), "New folder"]
1027
+ children: [/* @__PURE__ */ jsx(FolderPlusIcon, { size: iconSize.smallest }), t("studio_storage_new_folder")]
881
1028
  }), /* @__PURE__ */ jsxs(Button, {
882
1029
  onClick: () => setUploadDialogOpen(true),
883
- children: [/* @__PURE__ */ jsx(PlusIcon, { size: iconSize.smallest }), "Upload files"]
1030
+ children: [/* @__PURE__ */ jsx(PlusIcon, { size: iconSize.smallest }), t("studio_storage_upload_files")]
884
1031
  })]
885
1032
  })
886
1033
  ]
@@ -904,15 +1051,15 @@ var StorageView = () => {
904
1051
  }),
905
1052
  /* @__PURE__ */ jsx("th", {
906
1053
  className: "px-2 py-2 font-semibold",
907
- children: "Name"
1054
+ children: t("studio_storage_name")
908
1055
  }),
909
1056
  /* @__PURE__ */ jsx("th", {
910
1057
  className: "px-4 py-2 font-semibold w-24",
911
- children: "Type"
1058
+ children: t("studio_storage_type")
912
1059
  }),
913
1060
  /* @__PURE__ */ jsx("th", {
914
1061
  className: "px-4 py-2 font-semibold w-24 text-right",
915
- children: "Size"
1062
+ children: t("studio_storage_size")
916
1063
  }),
917
1064
  /* @__PURE__ */ jsx("th", { className: "px-2 py-2 w-10" })
918
1065
  ]
@@ -959,7 +1106,7 @@ var StorageView = () => {
959
1106
  children: /* @__PURE__ */ jsx(Typography, {
960
1107
  variant: "caption",
961
1108
  className: "text-text-secondary dark:text-text-secondary-dark",
962
- children: "Folder"
1109
+ children: t("studio_storage_folder")
963
1110
  })
964
1111
  }),
965
1112
  /* @__PURE__ */ jsx("td", {
@@ -1034,7 +1181,10 @@ var StorageView = () => {
1034
1181
  children: /* @__PURE__ */ jsx(IconButton, {
1035
1182
  size: "smallest",
1036
1183
  className: "opacity-0 group-hover:opacity-100 transition-opacity",
1037
- onClick: () => handleDeleteFile(file),
1184
+ onClick: () => {
1185
+ setDeleteDialogTarget(file);
1186
+ setDeleteDialogOpen(true);
1187
+ },
1038
1188
  children: /* @__PURE__ */ jsx(Trash2Icon, { size: 14 })
1039
1189
  })
1040
1190
  })
@@ -1050,7 +1200,7 @@ var StorageView = () => {
1050
1200
  children: [/* @__PURE__ */ jsx(Typography, {
1051
1201
  variant: "caption",
1052
1202
  className: "text-[10px] uppercase tracking-wider font-semibold text-text-disabled dark:text-text-disabled-dark mb-2 block",
1053
- children: "Folders"
1203
+ children: t("studio_storage_folders")
1054
1204
  }), /* @__PURE__ */ jsx("div", {
1055
1205
  className: "grid gap-3 grid-cols-[repeat(auto-fill,minmax(140px,1fr))]",
1056
1206
  children: folders.map((folder) => {
@@ -1070,14 +1220,10 @@ var StorageView = () => {
1070
1220
  }, folder.fullPath);
1071
1221
  })
1072
1222
  })]
1073
- }), files.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs(Typography, {
1223
+ }), files.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Typography, {
1074
1224
  variant: "caption",
1075
1225
  className: "text-[10px] uppercase tracking-wider font-semibold text-text-disabled dark:text-text-disabled-dark mb-2 block",
1076
- children: [
1077
- "Files (",
1078
- files.length,
1079
- ")"
1080
- ]
1226
+ children: t("studio_storage_files_heading", { count: files.length })
1081
1227
  }), /* @__PURE__ */ jsx("div", {
1082
1228
  className: "grid gap-3 grid-cols-[repeat(auto-fill,minmax(140px,1fr))]",
1083
1229
  children: files.map((file) => {
@@ -1139,8 +1285,9 @@ var StorageView = () => {
1139
1285
  className: "flex items-center gap-1.5 flex-grow overflow-hidden px-3 py-2",
1140
1286
  children: [
1141
1287
  currentPath && /* @__PURE__ */ jsx(Tooltip, {
1142
- title: "Go up",
1288
+ title: t("studio_storage_go_up"),
1143
1289
  children: /* @__PURE__ */ jsx(IconButton, {
1290
+ "aria-label": t("studio_storage_go_up"),
1144
1291
  size: "small",
1145
1292
  onClick: handleNavigateUp,
1146
1293
  children: /* @__PURE__ */ jsx(ArrowLeftIcon, { size: iconSize.smallest })
@@ -1164,10 +1311,10 @@ var StorageView = () => {
1164
1311
  selectedPaths.size > 0 ? /* @__PURE__ */ jsxs("div", {
1165
1312
  className: "flex items-center gap-1.5 shrink-0",
1166
1313
  children: [
1167
- /* @__PURE__ */ jsxs(Typography, {
1314
+ /* @__PURE__ */ jsx(Typography, {
1168
1315
  variant: "body2",
1169
1316
  className: "text-[13px] font-medium whitespace-nowrap",
1170
- children: [selectedPaths.size, " selected"]
1317
+ children: t("studio_storage_selected_count", { count: selectedPaths.size })
1171
1318
  }),
1172
1319
  /* @__PURE__ */ jsxs(Button, {
1173
1320
  size: "small",
@@ -1179,7 +1326,7 @@ var StorageView = () => {
1179
1326
  children: [/* @__PURE__ */ jsx(Trash2Icon, {
1180
1327
  size: 14,
1181
1328
  className: "mr-1"
1182
- }), "Delete"]
1329
+ }), t("studio_storage_delete")]
1183
1330
  }),
1184
1331
  /* @__PURE__ */ jsxs(Button, {
1185
1332
  size: "small",
@@ -1187,23 +1334,18 @@ var StorageView = () => {
1187
1334
  onClick: () => {
1188
1335
  setSelectedPaths(/* @__PURE__ */ new Set());
1189
1336
  setSelectedFile(null);
1190
- setSelectedDownloadUrl(null);
1337
+ setSelectedLink(null);
1191
1338
  },
1192
1339
  children: [/* @__PURE__ */ jsx(XIcon, {
1193
1340
  size: 14,
1194
1341
  className: "mr-1"
1195
- }), "Deselect"]
1342
+ }), t("studio_storage_deselect")]
1196
1343
  })
1197
1344
  ]
1198
1345
  }) : !loading ? /* @__PURE__ */ jsxs(Chip, {
1199
1346
  size: "small",
1200
1347
  className: "shrink-0 text-[10px]",
1201
- children: [
1202
- files.length,
1203
- " file",
1204
- files.length !== 1 ? "s" : "",
1205
- folders.length > 0 ? `, ${folders.length} folder${folders.length !== 1 ? "s" : ""}` : ""
1206
- ]
1348
+ children: [t("studio_storage_file_count", { count: files.length }), folders.length > 0 ? `, ${t("studio_storage_folder_count", { count: folders.length })}` : ""]
1207
1349
  }) : null
1208
1350
  ]
1209
1351
  }), /* @__PURE__ */ jsxs("div", {
@@ -1214,19 +1356,20 @@ var StorageView = () => {
1214
1356
  position: "item-aligned",
1215
1357
  value: selectedSourceKey,
1216
1358
  onValueChange: (value) => {
1217
- if (value) setSelectedSourceKey(value);
1359
+ if (value && value !== selectedSourceKey) handleSourceChange(value);
1218
1360
  },
1219
1361
  renderValue: (key) => {
1220
- return storageSources.registry[key]?.label ?? (key === DEFAULT_STORAGE_SOURCE_KEY ? "Default" : key);
1362
+ return storageSources.registry[key]?.label ?? (key === DEFAULT_STORAGE_SOURCE_KEY ? t("studio_storage_default_source") : key);
1221
1363
  },
1222
1364
  children: sourceKeys.map((key) => /* @__PURE__ */ jsx(SelectItem, {
1223
1365
  value: key,
1224
- children: storageSources.registry[key]?.label ?? (key === DEFAULT_STORAGE_SOURCE_KEY ? "Default" : key)
1366
+ children: storageSources.registry[key]?.label ?? (key === DEFAULT_STORAGE_SOURCE_KEY ? t("studio_storage_default_source") : key)
1225
1367
  }, key))
1226
1368
  }),
1227
1369
  /* @__PURE__ */ jsx(Tooltip, {
1228
- title: "Grid view",
1370
+ title: t("studio_storage_grid_view"),
1229
1371
  children: /* @__PURE__ */ jsx(IconButton, {
1372
+ "aria-label": t("studio_storage_grid_view"),
1230
1373
  size: "small",
1231
1374
  onClick: () => setViewMode("grid"),
1232
1375
  className: cls(viewMode === "grid" && "bg-surface-raised"),
@@ -1234,8 +1377,9 @@ var StorageView = () => {
1234
1377
  })
1235
1378
  }),
1236
1379
  /* @__PURE__ */ jsx(Tooltip, {
1237
- title: "List view",
1380
+ title: t("studio_storage_list_view"),
1238
1381
  children: /* @__PURE__ */ jsx(IconButton, {
1382
+ "aria-label": t("studio_storage_list_view"),
1239
1383
  size: "small",
1240
1384
  onClick: () => setViewMode("list"),
1241
1385
  className: cls(viewMode === "list" && "bg-surface-raised"),
@@ -1244,8 +1388,9 @@ var StorageView = () => {
1244
1388
  }),
1245
1389
  /* @__PURE__ */ jsx("div", { className: cls("h-4 w-px mx-0.5", defaultBorderMixin, "bg-surface-raised") }),
1246
1390
  /* @__PURE__ */ jsx(Tooltip, {
1247
- title: "Refresh",
1391
+ title: t("studio_storage_refresh"),
1248
1392
  children: /* @__PURE__ */ jsx(IconButton, {
1393
+ "aria-label": t("studio_storage_refresh"),
1249
1394
  size: "small",
1250
1395
  onClick: handleRefresh,
1251
1396
  disabled: loading,
@@ -1253,8 +1398,9 @@ var StorageView = () => {
1253
1398
  })
1254
1399
  }),
1255
1400
  /* @__PURE__ */ jsx(Tooltip, {
1256
- title: "New folder",
1401
+ title: t("studio_storage_new_folder"),
1257
1402
  children: /* @__PURE__ */ jsx(IconButton, {
1403
+ "aria-label": t("studio_storage_new_folder"),
1258
1404
  size: "small",
1259
1405
  onClick: () => {
1260
1406
  setNewFolderName("");
@@ -1270,7 +1416,7 @@ var StorageView = () => {
1270
1416
  children: [/* @__PURE__ */ jsx(UploadCloudIcon, {
1271
1417
  size: iconSize.smallest,
1272
1418
  className: "mr-1"
1273
- }), "Upload"]
1419
+ }), t("studio_storage_upload")]
1274
1420
  })
1275
1421
  ]
1276
1422
  })]
@@ -1282,7 +1428,7 @@ var StorageView = () => {
1282
1428
  if (!e.target.closest("[data-storage-item]") && selectedPaths.size > 0) {
1283
1429
  setSelectedPaths(/* @__PURE__ */ new Set());
1284
1430
  setSelectedFile(null);
1285
- setSelectedDownloadUrl(null);
1431
+ setSelectedLink(null);
1286
1432
  }
1287
1433
  },
1288
1434
  children: [
@@ -1297,12 +1443,16 @@ var StorageView = () => {
1297
1443
  /* @__PURE__ */ jsx(Typography, {
1298
1444
  variant: "subtitle2",
1299
1445
  className: "text-primary font-semibold",
1300
- children: "Drop files to upload"
1446
+ children: t("studio_storage_drop_to_upload")
1301
1447
  }),
1302
1448
  /* @__PURE__ */ jsxs(Typography, {
1303
1449
  variant: "caption",
1304
1450
  color: "secondary",
1305
- children: ["to /", currentPath || "root"]
1451
+ children: [
1452
+ t("studio_storage_upload_to"),
1453
+ " /",
1454
+ currentPath || t("studio_storage_root_folder")
1455
+ ]
1306
1456
  })
1307
1457
  ]
1308
1458
  })
@@ -1315,25 +1465,24 @@ var StorageView = () => {
1315
1465
  className: "flex items-center gap-4 text-[11px]",
1316
1466
  children: [/* @__PURE__ */ jsx("span", {
1317
1467
  className: "text-text-disabled dark:text-text-disabled-dark font-semibold uppercase tracking-tighter",
1318
- children: "Path"
1468
+ children: t("studio_storage_path")
1319
1469
  }), /* @__PURE__ */ jsxs("span", {
1320
1470
  className: "font-mono text-text-secondary dark:text-text-secondary-dark",
1321
1471
  children: ["/", currentPath || ""]
1322
1472
  })]
1323
- }), selectedPaths.size > 0 ? /* @__PURE__ */ jsxs("div", {
1473
+ }), selectedPaths.size > 0 ? /* @__PURE__ */ jsx("div", {
1324
1474
  className: "text-[11px] text-text-secondary dark:text-text-secondary-dark",
1325
- children: [
1326
- selectedPaths.size,
1327
- " item",
1328
- selectedPaths.size !== 1 ? "s" : "",
1329
- " selected"
1330
- ]
1475
+ children: t("studio_storage_items_selected", { count: selectedPaths.size })
1331
1476
  }) : selectedFile ? /* @__PURE__ */ jsxs("div", {
1332
1477
  className: "text-[11px] text-text-secondary dark:text-text-secondary-dark",
1333
- children: ["Selected: ", /* @__PURE__ */ jsx("span", {
1334
- className: "font-mono",
1335
- children: selectedFile.name
1336
- })]
1478
+ children: [
1479
+ t("studio_storage_selected_label"),
1480
+ " ",
1481
+ /* @__PURE__ */ jsx("span", {
1482
+ className: "font-mono",
1483
+ children: selectedFile.name
1484
+ })
1485
+ ]
1337
1486
  }) : null]
1338
1487
  })
1339
1488
  ]
@@ -1341,10 +1490,11 @@ var StorageView = () => {
1341
1490
  className: "w-80 lg:w-96 shrink-0",
1342
1491
  children: /* @__PURE__ */ jsx(FilePreviewPanel, {
1343
1492
  file: selectedFile,
1344
- downloadUrl: selectedDownloadUrl,
1493
+ link: selectedLink,
1494
+ mintLink: refreshPreviewLink,
1345
1495
  onClose: () => {
1346
1496
  setSelectedFile(null);
1347
- setSelectedDownloadUrl(null);
1497
+ setSelectedLink(null);
1348
1498
  },
1349
1499
  onDelete: () => handleDeleteFile(selectedFile)
1350
1500
  })
@@ -1367,16 +1517,16 @@ var StorageView = () => {
1367
1517
  children: [
1368
1518
  /* @__PURE__ */ jsx(DialogTitle, {
1369
1519
  hidden: true,
1370
- children: "Delete Confirmation"
1520
+ children: t("studio_storage_delete_confirmation_title")
1371
1521
  }),
1372
1522
  /* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsx(Typography, {
1373
1523
  variant: "subtitle1",
1374
1524
  className: "font-semibold mb-2",
1375
- children: deleteDialogTarget === "selection" ? `Delete ${selectedPaths.size} item${selectedPaths.size !== 1 ? "s" : ""}?` : deleteDialogTarget ? `Delete folder "${deleteDialogTarget.name}"?` : "Delete?"
1525
+ children: deleteDialogTarget === "selection" ? t("studio_storage_delete_items_question", { count: selectedPaths.size }) : deleteDialogTarget?.isFolder ? t("studio_storage_delete_folder_question", { name: deleteDialogTarget.name }) : deleteDialogTarget ? t("studio_storage_delete_file_question") : t("studio_storage_delete_question")
1376
1526
  }), /* @__PURE__ */ jsx(Typography, {
1377
1527
  variant: "body2",
1378
1528
  color: "secondary",
1379
- children: deleteDialogTarget === "selection" ? "This will permanently delete all selected files and folders, including their contents. This action cannot be undone." : "This will permanently delete the folder and all of its contents. This action cannot be undone."
1529
+ children: deleteDialogTarget === "selection" ? t("studio_storage_delete_selection_body") : deleteDialogTarget && !deleteDialogTarget.isFolder ? t("studio_storage_delete_file_body", { name: deleteDialogTarget.name }) : t("studio_storage_delete_folder_body")
1380
1530
  })] }),
1381
1531
  /* @__PURE__ */ jsxs(DialogActions, { children: [/* @__PURE__ */ jsx(Button, {
1382
1532
  variant: "text",
@@ -1385,15 +1535,15 @@ var StorageView = () => {
1385
1535
  setDeleteDialogTarget(null);
1386
1536
  },
1387
1537
  disabled: deleting,
1388
- children: "Cancel"
1538
+ children: t("studio_storage_cancel")
1389
1539
  }), /* @__PURE__ */ jsxs(LoadingButton, {
1390
1540
  color: "error",
1391
1541
  loading: deleting,
1392
- onClick: deleteDialogTarget === "selection" ? handleBulkDelete : handleConfirmDeleteFolder,
1542
+ onClick: deleteDialogTarget === "selection" ? handleBulkDelete : handleConfirmDeleteItem,
1393
1543
  children: [/* @__PURE__ */ jsx(Trash2Icon, {
1394
1544
  size: 14,
1395
1545
  className: "mr-1"
1396
- }), "Delete"]
1546
+ }), t("studio_storage_delete")]
1397
1547
  })] })
1398
1548
  ]
1399
1549
  }),
@@ -1408,12 +1558,12 @@ var StorageView = () => {
1408
1558
  children: [
1409
1559
  /* @__PURE__ */ jsx(DialogTitle, {
1410
1560
  hidden: true,
1411
- children: "New Folder"
1561
+ children: t("studio_storage_new_folder_title")
1412
1562
  }),
1413
1563
  /* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsx(TextField, {
1414
1564
  autoFocus: true,
1415
1565
  size: "small",
1416
- label: "Folder name",
1566
+ label: t("studio_storage_folder_name"),
1417
1567
  value: newFolderName,
1418
1568
  onChange: (e) => setNewFolderName(e.target.value),
1419
1569
  onKeyDown: (e) => {
@@ -1423,19 +1573,23 @@ var StorageView = () => {
1423
1573
  }
1424
1574
  },
1425
1575
  disabled: creatingFolder,
1426
- placeholder: "Enter folder name"
1576
+ placeholder: t("studio_storage_folder_name_placeholder")
1427
1577
  }), currentPath && /* @__PURE__ */ jsxs(Typography, {
1428
1578
  variant: "caption",
1429
1579
  color: "secondary",
1430
1580
  className: "mt-2",
1431
- children: ["Will be created in ", /* @__PURE__ */ jsxs("span", {
1432
- className: "font-mono",
1433
- children: [
1434
- "/",
1435
- currentPath,
1436
- "/"
1437
- ]
1438
- })]
1581
+ children: [
1582
+ t("studio_storage_created_in"),
1583
+ " ",
1584
+ /* @__PURE__ */ jsxs("span", {
1585
+ className: "font-mono",
1586
+ children: [
1587
+ "/",
1588
+ currentPath,
1589
+ "/"
1590
+ ]
1591
+ })
1592
+ ]
1439
1593
  })] }),
1440
1594
  /* @__PURE__ */ jsxs(DialogActions, { children: [/* @__PURE__ */ jsx(Button, {
1441
1595
  variant: "text",
@@ -1444,7 +1598,7 @@ var StorageView = () => {
1444
1598
  setNewFolderName("");
1445
1599
  },
1446
1600
  disabled: creatingFolder,
1447
- children: "Cancel"
1601
+ children: t("studio_storage_cancel")
1448
1602
  }), /* @__PURE__ */ jsxs(LoadingButton, {
1449
1603
  color: "primary",
1450
1604
  loading: creatingFolder,
@@ -1453,7 +1607,7 @@ var StorageView = () => {
1453
1607
  children: [/* @__PURE__ */ jsx(FolderPlusIcon, {
1454
1608
  size: 14,
1455
1609
  className: "mr-1"
1456
- }), "Create"]
1610
+ }), t("studio_storage_create")]
1457
1611
  })] })
1458
1612
  ]
1459
1613
  })
@@ -1463,4 +1617,4 @@ var StorageView = () => {
1463
1617
  //#endregion
1464
1618
  export { StorageView };
1465
1619
 
1466
- //# sourceMappingURL=StorageView-C52dSZNf.js.map
1620
+ //# sourceMappingURL=StorageView-CbsHuCBf.js.map