@rebasepro/studio 0.0.1-canary.eae7889 → 0.1.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.
- package/dist/{ApiExplorer-gMJt5JrS.js → ApiExplorer-DHVmWYfK.js} +13 -13
- package/dist/ApiExplorer-DHVmWYfK.js.map +1 -0
- package/dist/AuthSimulationSelector-CM488Eei.js +106 -0
- package/dist/AuthSimulationSelector-CM488Eei.js.map +1 -0
- package/dist/{JSEditor-D8nVp3Lp.js → JSEditor-CSHA0t_O.js} +2 -2
- package/dist/{JSEditor-D8nVp3Lp.js.map → JSEditor-CSHA0t_O.js.map} +1 -1
- package/dist/{RLSEditor-DBH09u9v.js → RLSEditor-BzDjqo6w.js} +46 -5
- package/dist/RLSEditor-BzDjqo6w.js.map +1 -0
- package/dist/{SQLEditor-CkVx9vgr.js → SQLEditor-Cr9Kg_Qg.js} +63 -75
- package/dist/SQLEditor-Cr9Kg_Qg.js.map +1 -0
- package/dist/{SchemaVisualizer-BgD5Zb77.js → SchemaVisualizer-BGpmzyXT.js} +5 -5
- package/dist/SchemaVisualizer-BGpmzyXT.js.map +1 -0
- package/dist/StorageView-BYoslzBR.js +870 -0
- package/dist/StorageView-BYoslzBR.js.map +1 -0
- package/dist/core/src/components/BootstrapAdminBanner.d.ts +4 -0
- package/dist/core/src/components/LoginView/LoginView.d.ts +22 -0
- package/dist/core/src/components/common/useDataTableController.d.ts +3 -3
- package/dist/core/src/components/index.d.ts +1 -0
- package/dist/core/src/hooks/data/useRelationSelector.d.ts +2 -2
- package/dist/core/src/hooks/index.d.ts +1 -0
- package/dist/core/src/hooks/useCollapsedGroups.d.ts +16 -1
- package/dist/core/src/hooks/useResolvedComponent.d.ts +47 -0
- package/dist/index.es.js +79 -71
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +821 -834
- package/dist/index.umd.js.map +1 -1
- package/dist/types/src/controllers/auth.d.ts +8 -2
- package/dist/types/src/controllers/client.d.ts +13 -0
- package/dist/types/src/controllers/collection_registry.d.ts +2 -1
- package/dist/types/src/controllers/data_driver.d.ts +36 -1
- package/dist/types/src/controllers/navigation.d.ts +18 -6
- package/dist/types/src/controllers/registry.d.ts +9 -1
- package/dist/types/src/controllers/side_entity_controller.d.ts +7 -0
- package/dist/types/src/rebase_context.d.ts +17 -0
- package/dist/types/src/types/backend_hooks.d.ts +187 -0
- package/dist/types/src/types/collections.d.ts +31 -11
- package/dist/types/src/types/component_ref.d.ts +47 -0
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +6 -7
- package/dist/types/src/types/formex.d.ts +40 -0
- package/dist/types/src/types/index.d.ts +3 -0
- package/dist/types/src/types/plugins.d.ts +6 -3
- package/dist/types/src/types/properties.d.ts +72 -88
- package/dist/types/src/types/slots.d.ts +20 -10
- package/dist/types/src/types/translations.d.ts +6 -0
- package/dist/ui/src/components/FileUpload.d.ts +1 -1
- package/dist/ui/src/components/SearchBar.d.ts +5 -1
- package/dist/ui/src/styles.d.ts +2 -2
- package/package.json +10 -10
- package/src/components/ApiExplorer/ApiExplorer.tsx +7 -5
- package/src/components/ApiExplorer/TryItPanel.tsx +29 -53
- package/src/components/AuthSimulationSelector.tsx +13 -17
- package/src/components/RLSEditor/RLSEditor.tsx +82 -3
- package/src/components/SQLEditor/SQLEditor.tsx +16 -18
- package/src/components/SQLEditor/SchemaBrowser.tsx +6 -8
- package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +20 -22
- package/src/components/StorageView/StorageView.tsx +719 -304
- package/src/components/StudioHomePage.tsx +4 -1
- package/dist/ApiExplorer-gMJt5JrS.js.map +0 -1
- package/dist/AuthSimulationSelector-BF4rkRGp.js +0 -118
- package/dist/AuthSimulationSelector-BF4rkRGp.js.map +0 -1
- package/dist/RLSEditor-DBH09u9v.js.map +0 -1
- package/dist/SQLEditor-CkVx9vgr.js.map +0 -1
- package/dist/SchemaVisualizer-BgD5Zb77.js.map +0 -1
- package/dist/StorageView-CTqGFhY9.js +0 -907
- package/dist/StorageView-CTqGFhY9.js.map +0 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
|
|
2
|
-
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
|
3
|
-
import { Typography, cls, defaultBorderMixin, Button, IconButton, Tooltip, CircularProgress, ResizablePanels, Chip, Dialog, DialogContent, DialogActions, FileUpload
|
|
4
|
-
import { VideoIcon, Music2Icon, RefreshCwIcon, Trash2Icon, XIcon, PlusIcon, DownloadIcon, UploadCloudIcon, FolderIcon, FileTextIcon, ImageIcon, ArrowLeftIcon, FileIcon, HomeIcon, LayoutGridIcon, ListIcon } from "lucide-react";
|
|
5
|
-
import { useStorageSource, useSnackbarController, ErrorView } from "@rebasepro/core";
|
|
2
|
+
import React, { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
|
3
|
+
import { Typography, cls, defaultBorderMixin, Button, IconButton, Tooltip, CircularProgress, ResizablePanels, Chip, Dialog, DialogTitle, DialogContent, DialogActions, FileUpload, iconSize, Checkbox, LoadingButton, TextField } from "@rebasepro/ui";
|
|
4
|
+
import { VideoIcon, Music2Icon, RefreshCwIcon, Trash2Icon, XIcon, PlusIcon, DownloadIcon, UploadCloudIcon, FolderIcon, FolderPlusIcon, FileTextIcon, ImageIcon, ArrowLeftIcon, FileIcon, HomeIcon, LayoutGridIcon, ListIcon, CopyIcon, CheckIcon } from "lucide-react";
|
|
5
|
+
import { useStorageSource, useSnackbarController, ErrorView, useApiConfig } from "@rebasepro/core";
|
|
6
6
|
import type { StorageListResult } from "@rebasepro/types";
|
|
7
|
+
import { useSearchParams } from "react-router-dom";
|
|
8
|
+
import { useDropzone } from "react-dropzone";
|
|
7
9
|
|
|
8
10
|
// ──────────────────────────────────────────────
|
|
9
11
|
// Types
|
|
@@ -110,78 +112,69 @@ function UploadDialog({
|
|
|
110
112
|
|
|
111
113
|
return (
|
|
112
114
|
<Dialog open={open} onOpenChange={(o) => !o && handleClose()} maxWidth="md">
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
115
|
+
<DialogTitle>
|
|
116
|
+
Upload Files
|
|
117
|
+
<Typography variant="caption" className="text-text-secondary dark:text-text-secondary-dark mt-0.5 block">
|
|
118
|
+
to <span className="font-mono text-primary">/{currentPath || "root"}</span>
|
|
119
|
+
</Typography>
|
|
120
|
+
</DialogTitle>
|
|
121
|
+
<DialogContent className="space-y-4">
|
|
122
|
+
<FileUpload
|
|
123
|
+
onFilesAdded={handleFilesAdded}
|
|
124
|
+
size="large"
|
|
125
|
+
uploadDescription={
|
|
126
|
+
<div className="flex flex-col items-center justify-center pointer-events-none">
|
|
127
|
+
<UploadCloudIcon className="text-surface-accent-400 mb-2 w-8 h-8"/>
|
|
128
|
+
<Typography variant="label">
|
|
129
|
+
Drop files here or click to browse
|
|
130
|
+
</Typography>
|
|
131
|
+
<Typography variant="caption" color="secondary">
|
|
132
|
+
Any file type supported
|
|
133
|
+
</Typography>
|
|
134
|
+
</div>
|
|
135
|
+
}
|
|
136
|
+
/>
|
|
120
137
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
size="large"
|
|
127
|
-
uploadDescription={
|
|
128
|
-
<div className="flex flex-col items-center justify-center pointer-events-none mt-2">
|
|
129
|
-
<UploadCloudIcon className="text-surface-accent-400 mb-2 w-8 h-8"/>
|
|
130
|
-
<Typography variant="h6" className="font-bold">
|
|
131
|
-
Drop files here or click to browse
|
|
132
|
-
</Typography>
|
|
133
|
-
<Typography variant="caption" className="text-surface-accent-500 font-medium">
|
|
134
|
-
Any file type supported
|
|
135
|
-
</Typography>
|
|
136
|
-
</div>
|
|
137
|
-
}
|
|
138
|
-
/>
|
|
138
|
+
{error && (
|
|
139
|
+
<Typography variant="caption" className="text-red-500 block whitespace-pre-line">
|
|
140
|
+
{error}
|
|
141
|
+
</Typography>
|
|
142
|
+
)}
|
|
139
143
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
144
|
+
{selectedFiles.length > 0 && (
|
|
145
|
+
<div className="space-y-2">
|
|
146
|
+
<Typography variant="caption" color="secondary">
|
|
147
|
+
Selected files ({selectedFiles.length})
|
|
143
148
|
</Typography>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
"bg-surface-accent-50 dark:bg-surface-accent-800"
|
|
158
|
-
)}
|
|
159
|
-
>
|
|
160
|
-
<div className="flex-1 min-w-0 mr-2">
|
|
161
|
-
<Typography variant="body2" className="truncate">
|
|
162
|
-
{file.name}
|
|
163
|
-
</Typography>
|
|
164
|
-
<Typography variant="caption" className="text-surface-accent-500">
|
|
165
|
-
{formatFileSize(file.size)}
|
|
166
|
-
</Typography>
|
|
167
|
-
</div>
|
|
168
|
-
<Button
|
|
169
|
-
variant="text"
|
|
170
|
-
size="small"
|
|
171
|
-
onClick={(e) => {
|
|
172
|
-
e.stopPropagation();
|
|
173
|
-
handleRemoveFile(index);
|
|
174
|
-
}}
|
|
175
|
-
disabled={uploading}
|
|
176
|
-
>
|
|
177
|
-
Remove
|
|
178
|
-
</Button>
|
|
149
|
+
<div className="max-h-40 overflow-auto space-y-1">
|
|
150
|
+
{selectedFiles.map((file, index) => (
|
|
151
|
+
<div
|
|
152
|
+
key={`${file.name}-${index}`}
|
|
153
|
+
className="flex items-center justify-between p-2 rounded bg-surface-100 dark:bg-surface-800"
|
|
154
|
+
>
|
|
155
|
+
<div className="flex-1 min-w-0 mr-2">
|
|
156
|
+
<Typography variant="body2" className="truncate">
|
|
157
|
+
{file.name}
|
|
158
|
+
</Typography>
|
|
159
|
+
<Typography variant="caption" color="secondary">
|
|
160
|
+
{formatFileSize(file.size)}
|
|
161
|
+
</Typography>
|
|
179
162
|
</div>
|
|
180
|
-
|
|
181
|
-
|
|
163
|
+
<IconButton
|
|
164
|
+
size="small"
|
|
165
|
+
onClick={(e) => {
|
|
166
|
+
e.stopPropagation();
|
|
167
|
+
handleRemoveFile(index);
|
|
168
|
+
}}
|
|
169
|
+
disabled={uploading}
|
|
170
|
+
>
|
|
171
|
+
<XIcon size={14}/>
|
|
172
|
+
</IconButton>
|
|
173
|
+
</div>
|
|
174
|
+
))}
|
|
182
175
|
</div>
|
|
183
|
-
|
|
184
|
-
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
185
178
|
</DialogContent>
|
|
186
179
|
|
|
187
180
|
<DialogActions>
|
|
@@ -192,15 +185,9 @@ function UploadDialog({
|
|
|
192
185
|
variant="filled"
|
|
193
186
|
onClick={handleUpload}
|
|
194
187
|
disabled={selectedFiles.length === 0 || uploading}
|
|
188
|
+
startIcon={uploading ? <CircularProgress size="smallest"/> : <UploadCloudIcon size={14}/>}
|
|
195
189
|
>
|
|
196
|
-
{uploading ? (
|
|
197
|
-
<>
|
|
198
|
-
<CircularProgress size="smallest"/>
|
|
199
|
-
Uploading...
|
|
200
|
-
</>
|
|
201
|
-
) : (
|
|
202
|
-
`Upload ${selectedFiles.length > 0 ? `(${selectedFiles.length})` : ""}`
|
|
203
|
-
)}
|
|
190
|
+
{uploading ? "Uploading..." : `Upload${selectedFiles.length > 0 ? ` (${selectedFiles.length})` : ""}`}
|
|
204
191
|
</Button>
|
|
205
192
|
</DialogActions>
|
|
206
193
|
</Dialog>
|
|
@@ -227,13 +214,14 @@ function FilePreviewPanel({
|
|
|
227
214
|
const isAudio = file.contentType?.startsWith("audio/");
|
|
228
215
|
const FileIconComponent = getFileIcon(file.contentType);
|
|
229
216
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
217
|
+
const [urlCopied, setUrlCopied] = useState(false);
|
|
230
218
|
|
|
231
219
|
return (
|
|
232
220
|
<>
|
|
233
221
|
<div className={cls(
|
|
234
222
|
"flex flex-col h-full border-l",
|
|
235
223
|
defaultBorderMixin,
|
|
236
|
-
"bg-white dark:bg-surface-
|
|
224
|
+
"bg-white dark:bg-surface-800"
|
|
237
225
|
)}>
|
|
238
226
|
{/* Header */}
|
|
239
227
|
<div className={cls("flex items-center justify-between p-3 border-b shrink-0", defaultBorderMixin)}>
|
|
@@ -268,7 +256,7 @@ function FilePreviewPanel({
|
|
|
268
256
|
|
|
269
257
|
{/* Preview */}
|
|
270
258
|
<div className="flex-1 overflow-auto">
|
|
271
|
-
<div className="flex flex-col items-center justify-center min-h-[200px] p-4 bg-surface-50 dark:bg-surface-
|
|
259
|
+
<div className={cls("flex flex-col items-center justify-center min-h-[200px] p-4 bg-surface-50 dark:bg-surface-800 border-b", defaultBorderMixin)}>
|
|
272
260
|
{(() => {
|
|
273
261
|
const ext = getExtension(file.name)?.toLowerCase() || "";
|
|
274
262
|
const isImage = file.contentType?.startsWith("image/") || ["jpg", "jpeg", "png", "gif", "webp", "svg"].includes(ext);
|
|
@@ -371,17 +359,36 @@ function FilePreviewPanel({
|
|
|
371
359
|
URL
|
|
372
360
|
</Typography>
|
|
373
361
|
<div
|
|
374
|
-
className=
|
|
362
|
+
className={cls(
|
|
363
|
+
"flex items-center gap-2 p-2 rounded cursor-pointer transition-colors",
|
|
364
|
+
"bg-surface-100 dark:bg-surface-800 hover:bg-surface-200 dark:hover:bg-surface-700"
|
|
365
|
+
)}
|
|
375
366
|
onClick={() => {
|
|
376
|
-
|
|
367
|
+
const fullUrl = downloadUrl.startsWith("http")
|
|
368
|
+
? downloadUrl
|
|
369
|
+
: `${window.location.origin}${downloadUrl.startsWith("/") ? "" : "/"}${downloadUrl}`;
|
|
370
|
+
navigator.clipboard.writeText(fullUrl).then(() => {
|
|
371
|
+
setUrlCopied(true);
|
|
372
|
+
setTimeout(() => setUrlCopied(false), 2000);
|
|
373
|
+
});
|
|
377
374
|
}}
|
|
378
375
|
>
|
|
379
|
-
<Typography variant="caption" className="font-mono text-[11px]
|
|
380
|
-
{
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
376
|
+
<Typography variant="caption" className="font-mono text-[11px] truncate flex-1 min-w-0 text-primary">
|
|
377
|
+
{(() => {
|
|
378
|
+
const fullUrl = downloadUrl.startsWith("http")
|
|
379
|
+
? downloadUrl
|
|
380
|
+
: `${window.location.origin}${downloadUrl.startsWith("/") ? "" : "/"}${downloadUrl}`;
|
|
381
|
+
return fullUrl;
|
|
382
|
+
})()}
|
|
384
383
|
</Typography>
|
|
384
|
+
<Tooltip title={urlCopied ? "Copied!" : "Copy URL"}>
|
|
385
|
+
<div className="shrink-0">
|
|
386
|
+
{urlCopied
|
|
387
|
+
? <CheckIcon size={14} className="text-green-500"/>
|
|
388
|
+
: <CopyIcon size={14} className="text-surface-accent-400"/>
|
|
389
|
+
}
|
|
390
|
+
</div>
|
|
391
|
+
</Tooltip>
|
|
385
392
|
</div>
|
|
386
393
|
</div>
|
|
387
394
|
)}
|
|
@@ -419,79 +426,6 @@ function FilePreviewPanel({
|
|
|
419
426
|
);
|
|
420
427
|
}
|
|
421
428
|
|
|
422
|
-
// ──────────────────────────────────────────────
|
|
423
|
-
// Sidebar (folder tree)
|
|
424
|
-
// ──────────────────────────────────────────────
|
|
425
|
-
|
|
426
|
-
function StorageSidebar({
|
|
427
|
-
folders,
|
|
428
|
-
currentPath,
|
|
429
|
-
onNavigate,
|
|
430
|
-
loading
|
|
431
|
-
}: {
|
|
432
|
-
folders: StorageFile[];
|
|
433
|
-
currentPath: string;
|
|
434
|
-
onNavigate: (path: string) => void;
|
|
435
|
-
loading: boolean;
|
|
436
|
-
}) {
|
|
437
|
-
const segments = breadcrumbSegments(currentPath);
|
|
438
|
-
|
|
439
|
-
return (
|
|
440
|
-
<div className={cls("flex flex-col h-full w-full bg-white dark:bg-surface-950 border-r", defaultBorderMixin)}>
|
|
441
|
-
<div className={cls("p-3 border-b flex justify-between items-center bg-surface-50 dark:bg-surface-900 shrink-0", defaultBorderMixin)}>
|
|
442
|
-
<Typography variant="caption" className="font-bold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark">
|
|
443
|
-
Folders
|
|
444
|
-
</Typography>
|
|
445
|
-
</div>
|
|
446
|
-
|
|
447
|
-
<div className="flex-grow overflow-y-auto no-scrollbar p-1">
|
|
448
|
-
{/* Folder tree */}
|
|
449
|
-
<div
|
|
450
|
-
className={cls(
|
|
451
|
-
"flex items-center p-1.5 cursor-pointer rounded transition-colors",
|
|
452
|
-
currentPath === "" || !currentPath
|
|
453
|
-
? "bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary-light"
|
|
454
|
-
: "hover:bg-surface-100 dark:hover:bg-surface-950 text-text-secondary dark:text-text-secondary-dark"
|
|
455
|
-
)}
|
|
456
|
-
onClick={() => onNavigate("")}
|
|
457
|
-
>
|
|
458
|
-
<HomeIcon size={iconSize.smallest} className="mr-1.5 shrink-0"/>
|
|
459
|
-
<Typography variant="body2" className="text-xs truncate">Root</Typography>
|
|
460
|
-
</div>
|
|
461
|
-
|
|
462
|
-
{loading && folders.length === 0 ? (
|
|
463
|
-
<div className="flex justify-center p-4">
|
|
464
|
-
<CircularProgress size="small"/>
|
|
465
|
-
</div>
|
|
466
|
-
) : (
|
|
467
|
-
<div className="mt-1 space-y-0.5">
|
|
468
|
-
{folders.map(folder => {
|
|
469
|
-
const isSelected = currentPath === folder.fullPath;
|
|
470
|
-
return (
|
|
471
|
-
<div
|
|
472
|
-
key={folder.fullPath}
|
|
473
|
-
className={cls(
|
|
474
|
-
"flex items-center p-1.5 cursor-pointer rounded transition-colors group",
|
|
475
|
-
isSelected
|
|
476
|
-
? "bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary-light"
|
|
477
|
-
: "hover:bg-surface-100 dark:hover:bg-surface-950 text-text-secondary dark:text-text-secondary-dark"
|
|
478
|
-
)}
|
|
479
|
-
onClick={() => onNavigate(folder.fullPath)}
|
|
480
|
-
>
|
|
481
|
-
<FolderIcon size={iconSize.smallest} className="mr-1.5 shrink-0 text-amber-500 dark:text-amber-400"/>
|
|
482
|
-
<Typography variant="body2" className="text-xs truncate flex-1 min-w-0">
|
|
483
|
-
{folder.name}
|
|
484
|
-
</Typography>
|
|
485
|
-
</div>
|
|
486
|
-
);
|
|
487
|
-
})}
|
|
488
|
-
</div>
|
|
489
|
-
)}
|
|
490
|
-
</div>
|
|
491
|
-
</div>
|
|
492
|
-
);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
429
|
// ──────────────────────────────────────────────
|
|
496
430
|
// Main StorageView Export
|
|
497
431
|
// ──────────────────────────────────────────────
|
|
@@ -501,7 +435,8 @@ export const StorageView = () => {
|
|
|
501
435
|
const snackbarController = useSnackbarController();
|
|
502
436
|
|
|
503
437
|
// Navigation
|
|
504
|
-
const [
|
|
438
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
439
|
+
const currentPath = searchParams.get("path") || "";
|
|
505
440
|
const [loading, setLoading] = useState(true);
|
|
506
441
|
const [error, setError] = useState<string | null>(null);
|
|
507
442
|
|
|
@@ -519,30 +454,32 @@ export const StorageView = () => {
|
|
|
519
454
|
// View mode
|
|
520
455
|
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
|
|
521
456
|
|
|
522
|
-
//
|
|
457
|
+
// Multi-selection
|
|
458
|
+
const [selectedPaths, setSelectedPaths] = useState<Set<string>>(new Set());
|
|
459
|
+
const lastClickedRef = useRef<string | null>(null);
|
|
523
460
|
|
|
524
|
-
//
|
|
525
|
-
const [
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
461
|
+
// Bulk / folder delete
|
|
462
|
+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
463
|
+
const [deleteDialogTarget, setDeleteDialogTarget] = useState<"selection" | StorageFile | null>(null);
|
|
464
|
+
const [deleting, setDeleting] = useState(false);
|
|
465
|
+
|
|
466
|
+
// New folder
|
|
467
|
+
const [newFolderDialogOpen, setNewFolderDialogOpen] = useState(false);
|
|
468
|
+
const [newFolderName, setNewFolderName] = useState("");
|
|
469
|
+
const [creatingFolder, setCreatingFolder] = useState(false);
|
|
470
|
+
const apiConfig = useApiConfig();
|
|
533
471
|
|
|
472
|
+
const storageSourceRef = React.useRef(storageSource);
|
|
534
473
|
useEffect(() => {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
} catch { /* noop */ }
|
|
538
|
-
}, [sidebarSize]);
|
|
474
|
+
storageSourceRef.current = storageSource;
|
|
475
|
+
}, [storageSource]);
|
|
539
476
|
|
|
540
477
|
// ── Fetch directory contents ──
|
|
541
478
|
const fetchContents = useCallback(async (path: string) => {
|
|
542
479
|
setLoading(true);
|
|
543
480
|
setError(null);
|
|
544
481
|
try {
|
|
545
|
-
const result: StorageListResult = await
|
|
482
|
+
const result: StorageListResult = await storageSourceRef.current.listObjects(path);
|
|
546
483
|
|
|
547
484
|
const folderItems: StorageFile[] = (result.prefixes ?? []).map(ref => ({
|
|
548
485
|
name: ref.name,
|
|
@@ -554,7 +491,7 @@ export const StorageView = () => {
|
|
|
554
491
|
const fileItems: StorageFile[] = await Promise.all(
|
|
555
492
|
(result.items ?? []).map(async (ref) => {
|
|
556
493
|
try {
|
|
557
|
-
const downloadConfig = await
|
|
494
|
+
const downloadConfig = await storageSourceRef.current.getSignedUrl(ref.fullPath);
|
|
558
495
|
return {
|
|
559
496
|
name: ref.name,
|
|
560
497
|
fullPath: ref.fullPath,
|
|
@@ -581,7 +518,7 @@ export const StorageView = () => {
|
|
|
581
518
|
} finally {
|
|
582
519
|
setLoading(false);
|
|
583
520
|
}
|
|
584
|
-
}, [
|
|
521
|
+
}, []);
|
|
585
522
|
|
|
586
523
|
useEffect(() => {
|
|
587
524
|
fetchContents(currentPath);
|
|
@@ -589,10 +526,16 @@ export const StorageView = () => {
|
|
|
589
526
|
|
|
590
527
|
// Navigate to path
|
|
591
528
|
const handleNavigate = useCallback((path: string) => {
|
|
592
|
-
|
|
529
|
+
if (!path) {
|
|
530
|
+
setSearchParams({});
|
|
531
|
+
} else {
|
|
532
|
+
setSearchParams({ path });
|
|
533
|
+
}
|
|
593
534
|
setSelectedFile(null);
|
|
594
535
|
setSelectedDownloadUrl(null);
|
|
595
|
-
|
|
536
|
+
setSelectedPaths(new Set());
|
|
537
|
+
lastClickedRef.current = null;
|
|
538
|
+
}, [setSearchParams]);
|
|
596
539
|
|
|
597
540
|
// Navigate up one level
|
|
598
541
|
const handleNavigateUp = useCallback(() => {
|
|
@@ -601,26 +544,76 @@ export const StorageView = () => {
|
|
|
601
544
|
handleNavigate(parts.join("/"));
|
|
602
545
|
}, [currentPath, handleNavigate]);
|
|
603
546
|
|
|
604
|
-
//
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
547
|
+
// All items (folders + files) in display order, for shift-range select
|
|
548
|
+
const allItems = useMemo(() => [...folders, ...files], [folders, files]);
|
|
549
|
+
|
|
550
|
+
// ── Multi-select click handler ──
|
|
551
|
+
const handleItemClick = useCallback((item: StorageFile, e: React.MouseEvent) => {
|
|
552
|
+
const path = item.fullPath;
|
|
553
|
+
if (e.metaKey || e.ctrlKey) {
|
|
554
|
+
// Toggle individual item
|
|
555
|
+
setSelectedPaths(prev => {
|
|
556
|
+
const next = new Set(prev);
|
|
557
|
+
if (next.has(path)) next.delete(path);
|
|
558
|
+
else next.add(path);
|
|
559
|
+
return next;
|
|
560
|
+
});
|
|
561
|
+
lastClickedRef.current = path;
|
|
562
|
+
} else if (e.shiftKey && lastClickedRef.current) {
|
|
563
|
+
// Range select
|
|
564
|
+
const allPaths = allItems.map(i => i.fullPath);
|
|
565
|
+
const anchorIdx = allPaths.indexOf(lastClickedRef.current);
|
|
566
|
+
const currentIdx = allPaths.indexOf(path);
|
|
567
|
+
if (anchorIdx >= 0 && currentIdx >= 0) {
|
|
568
|
+
const [start, end] = anchorIdx < currentIdx ? [anchorIdx, currentIdx] : [currentIdx, anchorIdx];
|
|
569
|
+
setSelectedPaths(prev => {
|
|
570
|
+
const next = new Set(prev);
|
|
571
|
+
for (let i = start; i <= end; i++) next.add(allPaths[i]);
|
|
572
|
+
return next;
|
|
573
|
+
});
|
|
574
|
+
}
|
|
609
575
|
} else {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
576
|
+
// Exclusive select
|
|
577
|
+
setSelectedPaths(new Set([path]));
|
|
578
|
+
lastClickedRef.current = path;
|
|
579
|
+
// Also open preview if it's a file
|
|
580
|
+
if (!item.isFolder) {
|
|
581
|
+
setSelectedFile(item);
|
|
582
|
+
if (item.downloadUrl) {
|
|
583
|
+
setSelectedDownloadUrl(item.downloadUrl);
|
|
584
|
+
} else {
|
|
585
|
+
storageSourceRef.current.getSignedUrl(item.fullPath)
|
|
586
|
+
.then(config => setSelectedDownloadUrl(config.url))
|
|
587
|
+
.catch(() => setSelectedDownloadUrl(null));
|
|
588
|
+
}
|
|
589
|
+
} else {
|
|
590
|
+
setSelectedFile(null);
|
|
614
591
|
setSelectedDownloadUrl(null);
|
|
615
592
|
}
|
|
616
593
|
}
|
|
617
|
-
}, [
|
|
594
|
+
}, [allItems]);
|
|
595
|
+
|
|
596
|
+
// Double-click: open folder or preview file
|
|
597
|
+
const handleItemDoubleClick = useCallback((item: StorageFile) => {
|
|
598
|
+
if (item.isFolder) {
|
|
599
|
+
handleNavigate(item.fullPath);
|
|
600
|
+
} else {
|
|
601
|
+
setSelectedFile(item);
|
|
602
|
+
if (item.downloadUrl) {
|
|
603
|
+
setSelectedDownloadUrl(item.downloadUrl);
|
|
604
|
+
} else {
|
|
605
|
+
storageSourceRef.current.getSignedUrl(item.fullPath)
|
|
606
|
+
.then(config => setSelectedDownloadUrl(config.url))
|
|
607
|
+
.catch(() => setSelectedDownloadUrl(null));
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}, [handleNavigate]);
|
|
618
611
|
|
|
619
612
|
// Upload files
|
|
620
613
|
const handleUpload = useCallback(async (uploadFiles: File[]) => {
|
|
621
614
|
for (const file of uploadFiles) {
|
|
622
615
|
const key = currentPath ? `${currentPath}/${file.name}` : file.name;
|
|
623
|
-
await
|
|
616
|
+
await storageSourceRef.current.putObject({
|
|
624
617
|
file,
|
|
625
618
|
key
|
|
626
619
|
});
|
|
@@ -629,23 +622,214 @@ export const StorageView = () => {
|
|
|
629
622
|
type: "success",
|
|
630
623
|
message: `${uploadFiles.length} file${uploadFiles.length > 1 ? "s" : ""} uploaded successfully`
|
|
631
624
|
});
|
|
632
|
-
fetchContents(currentPath);
|
|
633
|
-
}, [
|
|
625
|
+
await fetchContents(currentPath);
|
|
626
|
+
}, [currentPath, snackbarController, fetchContents]);
|
|
627
|
+
|
|
628
|
+
// Create new folder
|
|
629
|
+
const handleCreateFolder = useCallback(async () => {
|
|
630
|
+
if (!newFolderName.trim() || !apiConfig?.apiUrl) return;
|
|
631
|
+
|
|
632
|
+
// Validate folder name
|
|
633
|
+
const name = newFolderName.trim();
|
|
634
|
+
if (name.includes("/") || name.includes("\\")) {
|
|
635
|
+
snackbarController.open({ type: "error", message: "Folder name cannot contain slashes" });
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Check if folder already exists
|
|
640
|
+
const existingFolder = folders.find(f => f.name === name);
|
|
641
|
+
if (existingFolder) {
|
|
642
|
+
snackbarController.open({ type: "error", message: `Folder "${name}" already exists` });
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
634
645
|
|
|
635
|
-
|
|
646
|
+
setCreatingFolder(true);
|
|
647
|
+
try {
|
|
648
|
+
const folderPath = currentPath ? `default/${currentPath}/${name}` : `default/${name}`;
|
|
649
|
+
const token = apiConfig.getAuthToken ? await apiConfig.getAuthToken() : null;
|
|
650
|
+
const response = await fetch(`${apiConfig.apiUrl}/api/storage/folder`, {
|
|
651
|
+
method: "POST",
|
|
652
|
+
headers: {
|
|
653
|
+
"Content-Type": "application/json",
|
|
654
|
+
...(token ? { "Authorization": `Bearer ${token}` } : {})
|
|
655
|
+
},
|
|
656
|
+
body: JSON.stringify({ path: folderPath })
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
if (!response.ok) {
|
|
660
|
+
const err = await response.json().catch(() => ({ error: "Failed to create folder" }));
|
|
661
|
+
throw new Error(err.error || "Failed to create folder");
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
snackbarController.open({ type: "success", message: `Folder "${name}" created` });
|
|
665
|
+
setNewFolderDialogOpen(false);
|
|
666
|
+
setNewFolderName("");
|
|
667
|
+
await fetchContents(currentPath);
|
|
668
|
+
} catch (e) {
|
|
669
|
+
snackbarController.open({ type: "error", message: e instanceof Error ? e.message : String(e) });
|
|
670
|
+
} finally {
|
|
671
|
+
setCreatingFolder(false);
|
|
672
|
+
}
|
|
673
|
+
}, [newFolderName, currentPath, apiConfig, snackbarController, fetchContents, folders]);
|
|
674
|
+
|
|
675
|
+
// Drag-and-drop on main view
|
|
676
|
+
const handleDropFiles = useCallback(async (droppedFiles: File[]) => {
|
|
677
|
+
if (droppedFiles.length === 0) return;
|
|
678
|
+
try {
|
|
679
|
+
for (const file of droppedFiles) {
|
|
680
|
+
const key = currentPath ? `${currentPath}/${file.name}` : file.name;
|
|
681
|
+
await storageSourceRef.current.putObject({ file, key });
|
|
682
|
+
}
|
|
683
|
+
snackbarController.open({
|
|
684
|
+
type: "success",
|
|
685
|
+
message: `${droppedFiles.length} file${droppedFiles.length > 1 ? "s" : ""} uploaded successfully`
|
|
686
|
+
});
|
|
687
|
+
await fetchContents(currentPath);
|
|
688
|
+
} catch (e) {
|
|
689
|
+
snackbarController.open({
|
|
690
|
+
type: "error",
|
|
691
|
+
message: e instanceof Error ? e.message : String(e)
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}, [currentPath, snackbarController, fetchContents]);
|
|
695
|
+
|
|
696
|
+
const {
|
|
697
|
+
getRootProps: getDropRootProps,
|
|
698
|
+
getInputProps: getDropInputProps,
|
|
699
|
+
isDragActive
|
|
700
|
+
} = useDropzone({
|
|
701
|
+
onDrop: handleDropFiles,
|
|
702
|
+
noClick: true,
|
|
703
|
+
noKeyboard: true,
|
|
704
|
+
noDragEventsBubbling: true
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// ── Recursive folder delete helper ──
|
|
708
|
+
const deleteFolderRecursive = useCallback(async (prefix: string) => {
|
|
709
|
+
const result = await storageSourceRef.current.listObjects(prefix);
|
|
710
|
+
// Delete all files in this level
|
|
711
|
+
for (const item of result.items ?? []) {
|
|
712
|
+
await storageSourceRef.current.deleteObject(item.fullPath);
|
|
713
|
+
}
|
|
714
|
+
// Recurse into sub-folders
|
|
715
|
+
for (const sub of result.prefixes ?? []) {
|
|
716
|
+
await deleteFolderRecursive(sub.fullPath);
|
|
717
|
+
}
|
|
718
|
+
// Delete the folder entry itself (needed for local filesystem)
|
|
719
|
+
try {
|
|
720
|
+
await storageSourceRef.current.deleteObject(prefix);
|
|
721
|
+
} catch {
|
|
722
|
+
// Ignore — S3 folders are virtual and may not exist as objects
|
|
723
|
+
}
|
|
724
|
+
}, []);
|
|
725
|
+
|
|
726
|
+
// Delete a single file
|
|
636
727
|
const handleDeleteFile = useCallback(async (file: StorageFile) => {
|
|
637
728
|
try {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
729
|
+
if (file.isFolder) {
|
|
730
|
+
await deleteFolderRecursive(file.fullPath);
|
|
731
|
+
} else {
|
|
732
|
+
await storageSourceRef.current.deleteObject(file.fullPath);
|
|
733
|
+
}
|
|
734
|
+
snackbarController.open({ type: "success", message: `"${file.name}" deleted` });
|
|
641
735
|
setSelectedFile(null);
|
|
642
736
|
setSelectedDownloadUrl(null);
|
|
737
|
+
setSelectedPaths(prev => {
|
|
738
|
+
const next = new Set(prev);
|
|
739
|
+
next.delete(file.fullPath);
|
|
740
|
+
return next;
|
|
741
|
+
});
|
|
643
742
|
fetchContents(currentPath);
|
|
644
743
|
} catch (e) {
|
|
645
|
-
snackbarController.open({ type: "error",
|
|
646
|
-
message: e instanceof Error ? e.message : String(e) });
|
|
744
|
+
snackbarController.open({ type: "error", message: e instanceof Error ? e.message : String(e) });
|
|
647
745
|
}
|
|
648
|
-
}, [
|
|
746
|
+
}, [currentPath, snackbarController, fetchContents, deleteFolderRecursive]);
|
|
747
|
+
|
|
748
|
+
// Bulk delete (selected items)
|
|
749
|
+
const handleBulkDelete = useCallback(async () => {
|
|
750
|
+
setDeleting(true);
|
|
751
|
+
try {
|
|
752
|
+
const items = allItems.filter(i => selectedPaths.has(i.fullPath));
|
|
753
|
+
for (const item of items) {
|
|
754
|
+
if (item.isFolder) {
|
|
755
|
+
await deleteFolderRecursive(item.fullPath);
|
|
756
|
+
} else {
|
|
757
|
+
await storageSourceRef.current.deleteObject(item.fullPath);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
snackbarController.open({ type: "success", message: `${items.length} item${items.length !== 1 ? "s" : ""} deleted` });
|
|
761
|
+
setSelectedPaths(new Set());
|
|
762
|
+
setSelectedFile(null);
|
|
763
|
+
setSelectedDownloadUrl(null);
|
|
764
|
+
await fetchContents(currentPath);
|
|
765
|
+
} catch (e) {
|
|
766
|
+
snackbarController.open({ type: "error", message: e instanceof Error ? e.message : String(e) });
|
|
767
|
+
} finally {
|
|
768
|
+
setDeleting(false);
|
|
769
|
+
setDeleteDialogOpen(false);
|
|
770
|
+
setDeleteDialogTarget(null);
|
|
771
|
+
}
|
|
772
|
+
}, [allItems, selectedPaths, currentPath, snackbarController, fetchContents, deleteFolderRecursive]);
|
|
773
|
+
|
|
774
|
+
// Confirm delete for a single folder
|
|
775
|
+
const handleConfirmDeleteFolder = useCallback(async () => {
|
|
776
|
+
if (!deleteDialogTarget || deleteDialogTarget === "selection") return;
|
|
777
|
+
setDeleting(true);
|
|
778
|
+
try {
|
|
779
|
+
await deleteFolderRecursive(deleteDialogTarget.fullPath);
|
|
780
|
+
snackbarController.open({ type: "success", message: `Folder "${deleteDialogTarget.name}" deleted` });
|
|
781
|
+
setSelectedPaths(prev => {
|
|
782
|
+
const next = new Set(prev);
|
|
783
|
+
next.delete(deleteDialogTarget.fullPath);
|
|
784
|
+
return next;
|
|
785
|
+
});
|
|
786
|
+
await fetchContents(currentPath);
|
|
787
|
+
} catch (e) {
|
|
788
|
+
snackbarController.open({ type: "error", message: e instanceof Error ? e.message : String(e) });
|
|
789
|
+
} finally {
|
|
790
|
+
setDeleting(false);
|
|
791
|
+
setDeleteDialogOpen(false);
|
|
792
|
+
setDeleteDialogTarget(null);
|
|
793
|
+
}
|
|
794
|
+
}, [deleteDialogTarget, currentPath, snackbarController, fetchContents, deleteFolderRecursive]);
|
|
795
|
+
|
|
796
|
+
// Select all / deselect
|
|
797
|
+
const handleSelectAll = useCallback(() => {
|
|
798
|
+
if (selectedPaths.size === allItems.length) {
|
|
799
|
+
setSelectedPaths(new Set());
|
|
800
|
+
} else {
|
|
801
|
+
setSelectedPaths(new Set(allItems.map(i => i.fullPath)));
|
|
802
|
+
}
|
|
803
|
+
}, [allItems, selectedPaths]);
|
|
804
|
+
|
|
805
|
+
// ── Keyboard shortcuts ──
|
|
806
|
+
useEffect(() => {
|
|
807
|
+
const handler = (e: KeyboardEvent) => {
|
|
808
|
+
// Don't handle shortcuts when a dialog is open
|
|
809
|
+
if (deleteDialogOpen || uploadDialogOpen || newFolderDialogOpen) return;
|
|
810
|
+
// Cmd/Ctrl+A: select all
|
|
811
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "a") {
|
|
812
|
+
e.preventDefault();
|
|
813
|
+
handleSelectAll();
|
|
814
|
+
}
|
|
815
|
+
// Escape: deselect
|
|
816
|
+
if (e.key === "Escape") {
|
|
817
|
+
setSelectedPaths(new Set());
|
|
818
|
+
setSelectedFile(null);
|
|
819
|
+
setSelectedDownloadUrl(null);
|
|
820
|
+
}
|
|
821
|
+
// Delete / Backspace: delete selected
|
|
822
|
+
if ((e.key === "Delete" || e.key === "Backspace") && selectedPaths.size > 0 && !e.metaKey && !e.ctrlKey) {
|
|
823
|
+
// Don't trigger if user is typing in an input
|
|
824
|
+
if ((e.target as HTMLElement)?.tagName === "INPUT" || (e.target as HTMLElement)?.tagName === "TEXTAREA") return;
|
|
825
|
+
e.preventDefault();
|
|
826
|
+
setDeleteDialogTarget("selection");
|
|
827
|
+
setDeleteDialogOpen(true);
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
window.addEventListener("keydown", handler);
|
|
831
|
+
return () => window.removeEventListener("keydown", handler);
|
|
832
|
+
}, [handleSelectAll, selectedPaths, deleteDialogOpen, uploadDialogOpen, newFolderDialogOpen]);
|
|
649
833
|
|
|
650
834
|
// Handle refresh
|
|
651
835
|
const handleRefresh = useCallback(() => {
|
|
@@ -654,6 +838,7 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
654
838
|
|
|
655
839
|
const segments = breadcrumbSegments(currentPath);
|
|
656
840
|
|
|
841
|
+
|
|
657
842
|
// ── Render file grid/list ──
|
|
658
843
|
const renderContents = () => {
|
|
659
844
|
if (loading) {
|
|
@@ -677,7 +862,6 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
677
862
|
);
|
|
678
863
|
}
|
|
679
864
|
|
|
680
|
-
const allItems = [...folders, ...files];
|
|
681
865
|
|
|
682
866
|
if (allItems.length === 0) {
|
|
683
867
|
return (
|
|
@@ -689,10 +873,19 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
689
873
|
<Typography variant="body2">
|
|
690
874
|
This folder is empty
|
|
691
875
|
</Typography>
|
|
692
|
-
<
|
|
693
|
-
<
|
|
694
|
-
|
|
695
|
-
|
|
876
|
+
<div className="flex items-center gap-2 mt-3">
|
|
877
|
+
<Button variant="text" onClick={() => {
|
|
878
|
+
setNewFolderName("");
|
|
879
|
+
setNewFolderDialogOpen(true);
|
|
880
|
+
}}>
|
|
881
|
+
<FolderPlusIcon size={iconSize.smallest}/>
|
|
882
|
+
New folder
|
|
883
|
+
</Button>
|
|
884
|
+
<Button onClick={() => setUploadDialogOpen(true)}>
|
|
885
|
+
<PlusIcon size={iconSize.smallest}/>
|
|
886
|
+
Upload files
|
|
887
|
+
</Button>
|
|
888
|
+
</div>
|
|
696
889
|
</div>
|
|
697
890
|
</div>
|
|
698
891
|
);
|
|
@@ -704,53 +897,105 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
704
897
|
<table className="w-full">
|
|
705
898
|
<thead>
|
|
706
899
|
<tr className={cls("border-b text-left text-[10px] uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark", defaultBorderMixin)}>
|
|
707
|
-
<th className="
|
|
900
|
+
<th className="pl-3 pr-0 py-2 w-8">
|
|
901
|
+
<Checkbox
|
|
902
|
+
size="small"
|
|
903
|
+
checked={allItems.length > 0 && selectedPaths.size === allItems.length}
|
|
904
|
+
indeterminate={selectedPaths.size > 0 && selectedPaths.size < allItems.length}
|
|
905
|
+
onCheckedChange={handleSelectAll}
|
|
906
|
+
/>
|
|
907
|
+
</th>
|
|
908
|
+
<th className="px-2 py-2 font-bold">Name</th>
|
|
708
909
|
<th className="px-4 py-2 font-bold w-24">Type</th>
|
|
709
910
|
<th className="px-4 py-2 font-bold w-24 text-right">Size</th>
|
|
911
|
+
<th className="px-2 py-2 w-10"/>
|
|
710
912
|
</tr>
|
|
711
913
|
</thead>
|
|
712
914
|
<tbody>
|
|
713
|
-
{folders.map(folder =>
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
915
|
+
{folders.map(folder => {
|
|
916
|
+
const isChecked = selectedPaths.has(folder.fullPath);
|
|
917
|
+
return (
|
|
918
|
+
<tr
|
|
919
|
+
key={folder.fullPath}
|
|
920
|
+
data-storage-item
|
|
921
|
+
className={cls(
|
|
922
|
+
"cursor-pointer transition-colors border-b group",
|
|
923
|
+
defaultBorderMixin,
|
|
924
|
+
isChecked
|
|
925
|
+
? "bg-primary/5 dark:bg-primary/10"
|
|
926
|
+
: "hover:bg-surface-100 dark:hover:bg-surface-800"
|
|
927
|
+
)}
|
|
928
|
+
onClick={(e) => handleItemClick(folder, e)}
|
|
929
|
+
onDoubleClick={() => handleItemDoubleClick(folder)}
|
|
930
|
+
>
|
|
931
|
+
<td className="pl-3 pr-0 py-2.5" onClick={(e) => e.stopPropagation()}>
|
|
932
|
+
<Checkbox
|
|
933
|
+
size="small"
|
|
934
|
+
checked={isChecked}
|
|
935
|
+
onCheckedChange={() => {
|
|
936
|
+
setSelectedPaths(prev => {
|
|
937
|
+
const next = new Set(prev);
|
|
938
|
+
if (next.has(folder.fullPath)) next.delete(folder.fullPath);
|
|
939
|
+
else next.add(folder.fullPath);
|
|
940
|
+
return next;
|
|
941
|
+
});
|
|
942
|
+
}}
|
|
943
|
+
/>
|
|
944
|
+
</td>
|
|
945
|
+
<td className="px-2 py-2.5">
|
|
946
|
+
<div className="flex items-center gap-2">
|
|
947
|
+
<FolderIcon size={iconSize.smallest} className="text-amber-500 dark:text-amber-400 shrink-0"/>
|
|
948
|
+
<Typography variant="body2" className="text-[13px] font-medium truncate">
|
|
949
|
+
{folder.name}
|
|
950
|
+
</Typography>
|
|
951
|
+
</div>
|
|
952
|
+
</td>
|
|
953
|
+
<td className="px-4 py-2.5">
|
|
954
|
+
<Typography variant="caption" className="text-text-secondary dark:text-text-secondary-dark">
|
|
955
|
+
Folder
|
|
724
956
|
</Typography>
|
|
725
|
-
</
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
</
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
</Typography>
|
|
736
|
-
</td>
|
|
737
|
-
</tr>
|
|
738
|
-
))}
|
|
957
|
+
</td>
|
|
958
|
+
<td className="px-4 py-2.5 text-right">
|
|
959
|
+
<Typography variant="caption" className="text-text-disabled dark:text-text-disabled-dark">
|
|
960
|
+
—
|
|
961
|
+
</Typography>
|
|
962
|
+
</td>
|
|
963
|
+
<td className="px-2 py-2.5"/>
|
|
964
|
+
</tr>
|
|
965
|
+
);
|
|
966
|
+
})}
|
|
739
967
|
{files.map(file => {
|
|
740
968
|
const FileIconComp = getFileIcon(file.contentType);
|
|
741
|
-
const
|
|
969
|
+
const isChecked = selectedPaths.has(file.fullPath);
|
|
742
970
|
return (
|
|
743
971
|
<tr
|
|
744
972
|
key={file.fullPath}
|
|
973
|
+
data-storage-item
|
|
745
974
|
className={cls(
|
|
746
|
-
"cursor-pointer transition-colors border-b
|
|
747
|
-
|
|
975
|
+
"cursor-pointer transition-colors border-b group",
|
|
976
|
+
defaultBorderMixin,
|
|
977
|
+
isChecked
|
|
748
978
|
? "bg-primary/5 dark:bg-primary/10"
|
|
749
|
-
: "hover:bg-surface-100 dark:hover:bg-surface-
|
|
979
|
+
: "hover:bg-surface-100 dark:hover:bg-surface-800"
|
|
750
980
|
)}
|
|
751
|
-
onClick={() =>
|
|
981
|
+
onClick={(e) => handleItemClick(file, e)}
|
|
982
|
+
onDoubleClick={() => handleItemDoubleClick(file)}
|
|
752
983
|
>
|
|
753
|
-
<td className="
|
|
984
|
+
<td className="pl-3 pr-0 py-2.5" onClick={(e) => e.stopPropagation()}>
|
|
985
|
+
<Checkbox
|
|
986
|
+
size="small"
|
|
987
|
+
checked={isChecked}
|
|
988
|
+
onCheckedChange={() => {
|
|
989
|
+
setSelectedPaths(prev => {
|
|
990
|
+
const next = new Set(prev);
|
|
991
|
+
if (next.has(file.fullPath)) next.delete(file.fullPath);
|
|
992
|
+
else next.add(file.fullPath);
|
|
993
|
+
return next;
|
|
994
|
+
});
|
|
995
|
+
}}
|
|
996
|
+
/>
|
|
997
|
+
</td>
|
|
998
|
+
<td className="px-2 py-2.5">
|
|
754
999
|
<div className="flex items-center gap-2">
|
|
755
1000
|
<FileIconComp size={iconSize.smallest} className="text-surface-accent-400 shrink-0"/>
|
|
756
1001
|
<Typography variant="body2" className="text-[13px] truncate">
|
|
@@ -768,6 +1013,15 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
768
1013
|
{file.size !== undefined ? formatFileSize(file.size) : "—"}
|
|
769
1014
|
</Typography>
|
|
770
1015
|
</td>
|
|
1016
|
+
<td className="px-2 py-2.5" onClick={(e) => e.stopPropagation()}>
|
|
1017
|
+
<IconButton
|
|
1018
|
+
size="smallest"
|
|
1019
|
+
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
|
1020
|
+
onClick={() => handleDeleteFile(file)}
|
|
1021
|
+
>
|
|
1022
|
+
<Trash2Icon size={14}/>
|
|
1023
|
+
</IconButton>
|
|
1024
|
+
</td>
|
|
771
1025
|
</tr>
|
|
772
1026
|
);
|
|
773
1027
|
})}
|
|
@@ -786,24 +1040,31 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
786
1040
|
<Typography variant="caption" className="text-[10px] uppercase tracking-wider font-bold text-text-disabled dark:text-text-disabled-dark mb-2 block">
|
|
787
1041
|
Folders
|
|
788
1042
|
</Typography>
|
|
789
|
-
<div className="grid
|
|
790
|
-
{folders.map(folder =>
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1043
|
+
<div className="grid gap-3 grid-cols-[repeat(auto-fill,minmax(140px,1fr))]">
|
|
1044
|
+
{folders.map(folder => {
|
|
1045
|
+
const isChecked = selectedPaths.has(folder.fullPath);
|
|
1046
|
+
return (
|
|
1047
|
+
<div
|
|
1048
|
+
key={folder.fullPath}
|
|
1049
|
+
data-storage-item
|
|
1050
|
+
className={cls(
|
|
1051
|
+
"rounded-lg p-3 cursor-pointer border",
|
|
1052
|
+
"transition-colors duration-150",
|
|
1053
|
+
defaultBorderMixin,
|
|
1054
|
+
"hover:bg-surface-100 dark:hover:bg-surface-800 hover:shadow-sm",
|
|
1055
|
+
"flex items-center gap-2",
|
|
1056
|
+
isChecked && "ring-2 ring-primary bg-primary/5 dark:bg-primary/10"
|
|
1057
|
+
)}
|
|
1058
|
+
onClick={(e) => handleItemClick(folder, e)}
|
|
1059
|
+
onDoubleClick={() => handleItemDoubleClick(folder)}
|
|
1060
|
+
>
|
|
1061
|
+
<FolderIcon size={iconSize.smallest} className="text-amber-500 dark:text-amber-400 shrink-0"/>
|
|
1062
|
+
<Typography variant="body2" className="text-[13px] font-medium truncate">
|
|
1063
|
+
{folder.name}
|
|
1064
|
+
</Typography>
|
|
1065
|
+
</div>
|
|
1066
|
+
);
|
|
1067
|
+
})}
|
|
807
1068
|
</div>
|
|
808
1069
|
</div>
|
|
809
1070
|
)}
|
|
@@ -814,31 +1075,34 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
814
1075
|
<Typography variant="caption" className="text-[10px] uppercase tracking-wider font-bold text-text-disabled dark:text-text-disabled-dark mb-2 block">
|
|
815
1076
|
Files ({files.length})
|
|
816
1077
|
</Typography>
|
|
817
|
-
<div className="grid
|
|
1078
|
+
<div className="grid gap-3 grid-cols-[repeat(auto-fill,minmax(140px,1fr))]">
|
|
818
1079
|
{files.map(file => {
|
|
819
1080
|
const FileIconComp = getFileIcon(file.contentType);
|
|
820
1081
|
const ext = getExtension(file.name)?.toLowerCase() || "";
|
|
821
1082
|
const isImage = file.contentType?.startsWith("image/") || ["jpg", "jpeg", "png", "gif", "webp", "svg"].includes(ext);
|
|
822
|
-
const
|
|
1083
|
+
const isChecked = selectedPaths.has(file.fullPath);
|
|
823
1084
|
|
|
824
1085
|
return (
|
|
825
1086
|
<div
|
|
826
1087
|
key={file.fullPath}
|
|
1088
|
+
data-storage-item
|
|
827
1089
|
className={cls(
|
|
828
|
-
"rounded-lg overflow-hidden cursor-pointer
|
|
1090
|
+
"rounded-lg overflow-hidden cursor-pointer border",
|
|
1091
|
+
"transition-shadow duration-150",
|
|
829
1092
|
defaultBorderMixin,
|
|
830
|
-
"hover:shadow-md
|
|
831
|
-
|
|
1093
|
+
"hover:shadow-md",
|
|
1094
|
+
isChecked && "ring-2 ring-primary"
|
|
832
1095
|
)}
|
|
833
|
-
onClick={() =>
|
|
1096
|
+
onClick={(e) => handleItemClick(file, e)}
|
|
1097
|
+
onDoubleClick={() => handleItemDoubleClick(file)}
|
|
834
1098
|
>
|
|
835
1099
|
{/* Thumbnail or icon */}
|
|
836
|
-
<div className="aspect-square relative overflow-hidden bg-surface-100 dark:bg-surface-
|
|
1100
|
+
<div className="aspect-square relative overflow-hidden bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
|
|
837
1101
|
{isImage && file.downloadUrl ? (
|
|
838
1102
|
<img
|
|
839
1103
|
src={file.downloadUrl}
|
|
840
1104
|
alt={file.name}
|
|
841
|
-
className="w-full h-full object-cover
|
|
1105
|
+
className="w-full h-full object-cover"
|
|
842
1106
|
loading="lazy"
|
|
843
1107
|
/>
|
|
844
1108
|
) : (
|
|
@@ -851,12 +1115,6 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
851
1115
|
{getExtension(file.name)}
|
|
852
1116
|
</div>
|
|
853
1117
|
)}
|
|
854
|
-
|
|
855
|
-
{/* Hover overlay */}
|
|
856
|
-
<div className={cls(
|
|
857
|
-
"absolute inset-0 bg-black/0 group-hover:bg-black/10",
|
|
858
|
-
"transition-colors duration-200"
|
|
859
|
-
)}/>
|
|
860
1118
|
</div>
|
|
861
1119
|
|
|
862
1120
|
{/* Name & size */}
|
|
@@ -879,28 +1137,14 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
879
1137
|
};
|
|
880
1138
|
|
|
881
1139
|
return (
|
|
882
|
-
<div className="flex h-full w-full bg-white dark:bg-surface-
|
|
883
|
-
<
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
onPanelSizeChange={setSidebarSize}
|
|
887
|
-
minPanelSizePx={180}
|
|
888
|
-
firstPanel={
|
|
889
|
-
<StorageSidebar
|
|
890
|
-
folders={folders}
|
|
891
|
-
currentPath={currentPath}
|
|
892
|
-
onNavigate={handleNavigate}
|
|
893
|
-
loading={loading}
|
|
894
|
-
/>
|
|
895
|
-
}
|
|
896
|
-
secondPanel={
|
|
897
|
-
<div className="flex h-full w-full">
|
|
898
|
-
{/* Main content */}
|
|
899
|
-
<div className="flex-grow flex flex-col min-w-0 h-full">
|
|
1140
|
+
<div className="flex h-full w-full bg-white dark:bg-surface-800 overflow-hidden text-text-primary dark:text-text-primary-dark">
|
|
1141
|
+
<div className="flex h-full w-full">
|
|
1142
|
+
{/* Main content */}
|
|
1143
|
+
<div className="flex-grow flex flex-col min-w-0 h-full">
|
|
900
1144
|
{/* Toolbar */}
|
|
901
|
-
<div className={cls("flex items-center justify-between pr-2 border-b bg-white dark:bg-surface-
|
|
1145
|
+
<div className={cls("flex items-center justify-between pr-2 border-b bg-white dark:bg-surface-800 shrink-0 h-10", defaultBorderMixin)}>
|
|
902
1146
|
<div className="flex items-center gap-1.5 flex-grow overflow-hidden px-3 py-2">
|
|
903
|
-
{/* Breadcrumbs */}
|
|
1147
|
+
{/* Breadcrumbs — always visible */}
|
|
904
1148
|
{currentPath && (
|
|
905
1149
|
<Tooltip title="Go up">
|
|
906
1150
|
<IconButton size="small" onClick={handleNavigateUp}>
|
|
@@ -933,13 +1177,42 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
933
1177
|
|
|
934
1178
|
<div className="flex-1"/>
|
|
935
1179
|
|
|
936
|
-
{/*
|
|
937
|
-
{
|
|
1180
|
+
{/* Selection actions or file count */}
|
|
1181
|
+
{selectedPaths.size > 0 ? (
|
|
1182
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
1183
|
+
<Typography variant="body2" className="text-[13px] font-medium whitespace-nowrap">
|
|
1184
|
+
{selectedPaths.size} selected
|
|
1185
|
+
</Typography>
|
|
1186
|
+
<Button
|
|
1187
|
+
size="small"
|
|
1188
|
+
variant="text"
|
|
1189
|
+
onClick={() => {
|
|
1190
|
+
setDeleteDialogTarget("selection");
|
|
1191
|
+
setDeleteDialogOpen(true);
|
|
1192
|
+
}}
|
|
1193
|
+
>
|
|
1194
|
+
<Trash2Icon size={14} className="mr-1"/>
|
|
1195
|
+
Delete
|
|
1196
|
+
</Button>
|
|
1197
|
+
<Button
|
|
1198
|
+
size="small"
|
|
1199
|
+
variant="text"
|
|
1200
|
+
onClick={() => {
|
|
1201
|
+
setSelectedPaths(new Set());
|
|
1202
|
+
setSelectedFile(null);
|
|
1203
|
+
setSelectedDownloadUrl(null);
|
|
1204
|
+
}}
|
|
1205
|
+
>
|
|
1206
|
+
<XIcon size={14} className="mr-1"/>
|
|
1207
|
+
Deselect
|
|
1208
|
+
</Button>
|
|
1209
|
+
</div>
|
|
1210
|
+
) : !loading ? (
|
|
938
1211
|
<Chip size="small" className="shrink-0 text-[10px]">
|
|
939
1212
|
{files.length} file{files.length !== 1 ? "s" : ""}
|
|
940
1213
|
{folders.length > 0 ? `, ${folders.length} folder${folders.length !== 1 ? "s" : ""}` : ""}
|
|
941
1214
|
</Chip>
|
|
942
|
-
)}
|
|
1215
|
+
) : null}
|
|
943
1216
|
</div>
|
|
944
1217
|
|
|
945
1218
|
<div className="flex shrink-0 items-center justify-end gap-1.5 pr-1">
|
|
@@ -948,7 +1221,7 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
948
1221
|
<IconButton
|
|
949
1222
|
size="small"
|
|
950
1223
|
onClick={() => setViewMode("grid")}
|
|
951
|
-
className={cls(viewMode === "grid" && "bg-surface-100 dark:bg-surface-
|
|
1224
|
+
className={cls(viewMode === "grid" && "bg-surface-100 dark:bg-surface-800")}
|
|
952
1225
|
>
|
|
953
1226
|
<LayoutGridIcon size={iconSize.smallest}/>
|
|
954
1227
|
</IconButton>
|
|
@@ -957,13 +1230,13 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
957
1230
|
<IconButton
|
|
958
1231
|
size="small"
|
|
959
1232
|
onClick={() => setViewMode("list")}
|
|
960
|
-
className={cls(viewMode === "list" && "bg-surface-100 dark:bg-surface-
|
|
1233
|
+
className={cls(viewMode === "list" && "bg-surface-100 dark:bg-surface-800")}
|
|
961
1234
|
>
|
|
962
1235
|
<ListIcon size={iconSize.smallest}/>
|
|
963
1236
|
</IconButton>
|
|
964
1237
|
</Tooltip>
|
|
965
1238
|
|
|
966
|
-
<div className="h-4 w-px bg-surface-200 dark:bg-surface-
|
|
1239
|
+
<div className={cls("h-4 w-px mx-0.5", defaultBorderMixin, "bg-surface-200 dark:bg-surface-700")}/>
|
|
967
1240
|
|
|
968
1241
|
<Tooltip title="Refresh">
|
|
969
1242
|
<IconButton size="small" onClick={handleRefresh} disabled={loading}>
|
|
@@ -971,6 +1244,17 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
971
1244
|
</IconButton>
|
|
972
1245
|
</Tooltip>
|
|
973
1246
|
|
|
1247
|
+
<Tooltip title="New folder">
|
|
1248
|
+
<IconButton
|
|
1249
|
+
size="small"
|
|
1250
|
+
onClick={() => {
|
|
1251
|
+
setNewFolderName("");
|
|
1252
|
+
setNewFolderDialogOpen(true);
|
|
1253
|
+
}}
|
|
1254
|
+
>
|
|
1255
|
+
<FolderPlusIcon size={iconSize.smallest}/>
|
|
1256
|
+
</IconButton>
|
|
1257
|
+
</Tooltip>
|
|
974
1258
|
<Button
|
|
975
1259
|
size="small"
|
|
976
1260
|
color="primary"
|
|
@@ -982,13 +1266,38 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
982
1266
|
</div>
|
|
983
1267
|
</div>
|
|
984
1268
|
|
|
985
|
-
{/*
|
|
986
|
-
<div
|
|
1269
|
+
{/* File grid / list — drop zone */}
|
|
1270
|
+
<div {...getDropRootProps()}
|
|
1271
|
+
className="flex-grow flex flex-col overflow-hidden min-h-0 relative"
|
|
1272
|
+
onClick={(e) => {
|
|
1273
|
+
const target = e.target as HTMLElement;
|
|
1274
|
+
if (!target.closest("[data-storage-item]") && selectedPaths.size > 0) {
|
|
1275
|
+
setSelectedPaths(new Set());
|
|
1276
|
+
setSelectedFile(null);
|
|
1277
|
+
setSelectedDownloadUrl(null);
|
|
1278
|
+
}
|
|
1279
|
+
}}
|
|
1280
|
+
>
|
|
1281
|
+
<input {...getDropInputProps()} />
|
|
987
1282
|
{renderContents()}
|
|
1283
|
+
{/* Drag overlay */}
|
|
1284
|
+
{isDragActive && (
|
|
1285
|
+
<div className="absolute inset-0 z-10 flex items-center justify-center bg-primary/5 dark:bg-primary/10 backdrop-blur-[2px]">
|
|
1286
|
+
<div className="flex flex-col items-center gap-2 p-6 rounded-xl border-2 border-dashed border-primary bg-white/80 dark:bg-surface-900/80">
|
|
1287
|
+
<UploadCloudIcon className="w-10 h-10 text-primary"/>
|
|
1288
|
+
<Typography variant="subtitle2" className="text-primary font-semibold">
|
|
1289
|
+
Drop files to upload
|
|
1290
|
+
</Typography>
|
|
1291
|
+
<Typography variant="caption" color="secondary">
|
|
1292
|
+
to /{currentPath || "root"}
|
|
1293
|
+
</Typography>
|
|
1294
|
+
</div>
|
|
1295
|
+
</div>
|
|
1296
|
+
)}
|
|
988
1297
|
</div>
|
|
989
1298
|
|
|
990
1299
|
{/* Status bar */}
|
|
991
|
-
<div className={cls("px-4 py-1.5 border-t bg-surface-50 dark:bg-surface-
|
|
1300
|
+
<div className={cls("px-4 py-1.5 border-t bg-surface-50 dark:bg-surface-800 flex items-center justify-between shrink-0", defaultBorderMixin)}>
|
|
992
1301
|
<div className="flex items-center gap-4 text-[11px]">
|
|
993
1302
|
<span className="text-text-disabled dark:text-text-disabled-dark font-bold uppercase tracking-tighter">
|
|
994
1303
|
Path
|
|
@@ -997,11 +1306,15 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
997
1306
|
/{currentPath || ""}
|
|
998
1307
|
</span>
|
|
999
1308
|
</div>
|
|
1000
|
-
{
|
|
1309
|
+
{selectedPaths.size > 0 ? (
|
|
1310
|
+
<div className="text-[11px] text-text-secondary dark:text-text-secondary-dark">
|
|
1311
|
+
{selectedPaths.size} item{selectedPaths.size !== 1 ? "s" : ""} selected
|
|
1312
|
+
</div>
|
|
1313
|
+
) : selectedFile ? (
|
|
1001
1314
|
<div className="text-[11px] text-text-secondary dark:text-text-secondary-dark">
|
|
1002
1315
|
Selected: <span className="font-mono">{selectedFile.name}</span>
|
|
1003
1316
|
</div>
|
|
1004
|
-
)}
|
|
1317
|
+
) : null}
|
|
1005
1318
|
</div>
|
|
1006
1319
|
</div>
|
|
1007
1320
|
|
|
@@ -1019,9 +1332,7 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
1019
1332
|
/>
|
|
1020
1333
|
</div>
|
|
1021
1334
|
)}
|
|
1022
|
-
|
|
1023
|
-
}
|
|
1024
|
-
/>
|
|
1335
|
+
</div>
|
|
1025
1336
|
|
|
1026
1337
|
{/* Upload Dialog */}
|
|
1027
1338
|
<UploadDialog
|
|
@@ -1030,6 +1341,110 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
1030
1341
|
onClose={() => setUploadDialogOpen(false)}
|
|
1031
1342
|
onUpload={handleUpload}
|
|
1032
1343
|
/>
|
|
1344
|
+
|
|
1345
|
+
{/* Delete confirmation dialog */}
|
|
1346
|
+
<Dialog
|
|
1347
|
+
open={deleteDialogOpen}
|
|
1348
|
+
onOpenChange={(open) => {
|
|
1349
|
+
if (!open && !deleting) {
|
|
1350
|
+
setDeleteDialogOpen(false);
|
|
1351
|
+
setDeleteDialogTarget(null);
|
|
1352
|
+
}
|
|
1353
|
+
}}
|
|
1354
|
+
>
|
|
1355
|
+
<DialogContent>
|
|
1356
|
+
<Typography variant="subtitle1" className="font-semibold mb-2">
|
|
1357
|
+
{deleteDialogTarget === "selection"
|
|
1358
|
+
? `Delete ${selectedPaths.size} item${selectedPaths.size !== 1 ? "s" : ""}?`
|
|
1359
|
+
: deleteDialogTarget
|
|
1360
|
+
? `Delete folder "${deleteDialogTarget.name}"?`
|
|
1361
|
+
: "Delete?"}
|
|
1362
|
+
</Typography>
|
|
1363
|
+
<Typography variant="body2" color="secondary">
|
|
1364
|
+
{deleteDialogTarget === "selection"
|
|
1365
|
+
? "This will permanently delete all selected files and folders, including their contents. This action cannot be undone."
|
|
1366
|
+
: "This will permanently delete the folder and all of its contents. This action cannot be undone."}
|
|
1367
|
+
</Typography>
|
|
1368
|
+
</DialogContent>
|
|
1369
|
+
<DialogActions>
|
|
1370
|
+
<Button
|
|
1371
|
+
variant="text"
|
|
1372
|
+
onClick={() => {
|
|
1373
|
+
setDeleteDialogOpen(false);
|
|
1374
|
+
setDeleteDialogTarget(null);
|
|
1375
|
+
}}
|
|
1376
|
+
disabled={deleting}
|
|
1377
|
+
>
|
|
1378
|
+
Cancel
|
|
1379
|
+
</Button>
|
|
1380
|
+
<LoadingButton
|
|
1381
|
+
color="error"
|
|
1382
|
+
loading={deleting}
|
|
1383
|
+
onClick={deleteDialogTarget === "selection" ? handleBulkDelete : handleConfirmDeleteFolder}
|
|
1384
|
+
>
|
|
1385
|
+
<Trash2Icon size={14} className="mr-1"/>
|
|
1386
|
+
Delete
|
|
1387
|
+
</LoadingButton>
|
|
1388
|
+
</DialogActions>
|
|
1389
|
+
</Dialog>
|
|
1390
|
+
|
|
1391
|
+
{/* New Folder Dialog */}
|
|
1392
|
+
<Dialog
|
|
1393
|
+
open={newFolderDialogOpen}
|
|
1394
|
+
onOpenChange={(open) => {
|
|
1395
|
+
if (!open && !creatingFolder) {
|
|
1396
|
+
setNewFolderDialogOpen(false);
|
|
1397
|
+
setNewFolderName("");
|
|
1398
|
+
}
|
|
1399
|
+
}}
|
|
1400
|
+
>
|
|
1401
|
+
<DialogContent>
|
|
1402
|
+
<Typography variant="subtitle1" className="font-semibold mb-4">
|
|
1403
|
+
New Folder
|
|
1404
|
+
</Typography>
|
|
1405
|
+
<TextField
|
|
1406
|
+
autoFocus
|
|
1407
|
+
size="small"
|
|
1408
|
+
label="Folder name"
|
|
1409
|
+
value={newFolderName}
|
|
1410
|
+
onChange={(e) => setNewFolderName(e.target.value)}
|
|
1411
|
+
onKeyDown={(e) => {
|
|
1412
|
+
if (e.key === "Enter" && newFolderName.trim()) {
|
|
1413
|
+
e.preventDefault();
|
|
1414
|
+
handleCreateFolder();
|
|
1415
|
+
}
|
|
1416
|
+
}}
|
|
1417
|
+
disabled={creatingFolder}
|
|
1418
|
+
placeholder="Enter folder name"
|
|
1419
|
+
/>
|
|
1420
|
+
{currentPath && (
|
|
1421
|
+
<Typography variant="caption" color="secondary" className="mt-2">
|
|
1422
|
+
Will be created in <span className="font-mono">/{currentPath}/</span>
|
|
1423
|
+
</Typography>
|
|
1424
|
+
)}
|
|
1425
|
+
</DialogContent>
|
|
1426
|
+
<DialogActions>
|
|
1427
|
+
<Button
|
|
1428
|
+
variant="text"
|
|
1429
|
+
onClick={() => {
|
|
1430
|
+
setNewFolderDialogOpen(false);
|
|
1431
|
+
setNewFolderName("");
|
|
1432
|
+
}}
|
|
1433
|
+
disabled={creatingFolder}
|
|
1434
|
+
>
|
|
1435
|
+
Cancel
|
|
1436
|
+
</Button>
|
|
1437
|
+
<LoadingButton
|
|
1438
|
+
color="primary"
|
|
1439
|
+
loading={creatingFolder}
|
|
1440
|
+
disabled={!newFolderName.trim()}
|
|
1441
|
+
onClick={handleCreateFolder}
|
|
1442
|
+
>
|
|
1443
|
+
<FolderPlusIcon size={14} className="mr-1"/>
|
|
1444
|
+
Create
|
|
1445
|
+
</LoadingButton>
|
|
1446
|
+
</DialogActions>
|
|
1447
|
+
</Dialog>
|
|
1033
1448
|
</div>
|
|
1034
1449
|
);
|
|
1035
1450
|
};
|