@sparkstudio/storage-ui 1.0.13 → 1.0.15
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/index.cjs +425 -185
- package/dist/index.d.cts +34 -23
- package/dist/index.d.ts +34 -23
- package/dist/index.js +431 -191
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -182,6 +182,7 @@ var ContainerDTO = class {
|
|
|
182
182
|
ContainerType;
|
|
183
183
|
Name;
|
|
184
184
|
ContentType;
|
|
185
|
+
PublicUrl;
|
|
185
186
|
CreatedDate;
|
|
186
187
|
FileSize;
|
|
187
188
|
UserId;
|
|
@@ -191,6 +192,7 @@ var ContainerDTO = class {
|
|
|
191
192
|
this.ContainerType = init.ContainerType;
|
|
192
193
|
this.Name = init.Name;
|
|
193
194
|
this.ContentType = init.ContentType;
|
|
195
|
+
this.PublicUrl = init.PublicUrl;
|
|
194
196
|
this.CreatedDate = init.CreatedDate;
|
|
195
197
|
this.FileSize = init.FileSize;
|
|
196
198
|
this.UserId = init.UserId;
|
|
@@ -210,101 +212,72 @@ var ContainerType = /* @__PURE__ */ ((ContainerType2) => {
|
|
|
210
212
|
import "react";
|
|
211
213
|
|
|
212
214
|
// src/components/UploadContainer.tsx
|
|
213
|
-
import { useState as
|
|
215
|
+
import { useState as useState3 } from "react";
|
|
214
216
|
|
|
215
217
|
// src/components/UploadDropzone.tsx
|
|
216
218
|
import "react";
|
|
217
|
-
import { jsx
|
|
219
|
+
import { jsx } from "react/jsx-runtime";
|
|
218
220
|
var UploadDropzone = ({
|
|
219
221
|
isDragging,
|
|
220
222
|
onDragOver,
|
|
221
223
|
onDragLeave,
|
|
222
|
-
onDrop
|
|
224
|
+
onDrop,
|
|
225
|
+
className = "",
|
|
226
|
+
style,
|
|
227
|
+
children
|
|
223
228
|
}) => {
|
|
224
|
-
|
|
229
|
+
const baseClass = "border rounded-3 p-4 mb-3 d-flex flex-column align-items-center justify-content-center ";
|
|
230
|
+
const stateClass = isDragging ? "bg-light border-primary" : "border-secondary border-dashed";
|
|
231
|
+
const combinedClassName = `${baseClass}${stateClass} ${className}`.trim();
|
|
232
|
+
const handleDragOver = (e) => {
|
|
233
|
+
e.preventDefault();
|
|
234
|
+
if (onDragOver) onDragOver(e);
|
|
235
|
+
};
|
|
236
|
+
const handleDragLeave = (e) => {
|
|
237
|
+
e.preventDefault();
|
|
238
|
+
if (onDragLeave) onDragLeave(e);
|
|
239
|
+
};
|
|
240
|
+
const handleDrop = (e) => {
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
if (onDrop) onDrop(e);
|
|
243
|
+
};
|
|
244
|
+
return /* @__PURE__ */ jsx(
|
|
225
245
|
"div",
|
|
226
246
|
{
|
|
227
|
-
className:
|
|
228
|
-
style: {
|
|
229
|
-
onDragOver,
|
|
230
|
-
onDragLeave,
|
|
231
|
-
onDrop,
|
|
232
|
-
children
|
|
233
|
-
/* @__PURE__ */ jsx("i", { className: "bi bi-cloud-arrow-up fs-1 mb-2" }),
|
|
234
|
-
/* @__PURE__ */ jsx("p", { className: "mb-2", children: "Drop files here" }),
|
|
235
|
-
/* @__PURE__ */ jsx("small", { className: "text-muted", children: "or click the button below" })
|
|
236
|
-
]
|
|
247
|
+
className: combinedClassName,
|
|
248
|
+
style: { minHeight: "140px", ...style },
|
|
249
|
+
onDragOver: handleDragOver,
|
|
250
|
+
onDragLeave: handleDragLeave,
|
|
251
|
+
onDrop: handleDrop,
|
|
252
|
+
children
|
|
237
253
|
}
|
|
238
254
|
);
|
|
239
255
|
};
|
|
240
256
|
|
|
241
|
-
// src/components/UploadFilePicker.tsx
|
|
242
|
-
import "react";
|
|
243
|
-
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
244
|
-
var UploadFilePicker = ({
|
|
245
|
-
multiple,
|
|
246
|
-
accept,
|
|
247
|
-
fileNames,
|
|
248
|
-
onFileChange
|
|
249
|
-
}) => {
|
|
250
|
-
return /* @__PURE__ */ jsxs2("div", { className: "d-flex gap-2 align-items-center", children: [
|
|
251
|
-
/* @__PURE__ */ jsxs2("label", { className: "btn btn-primary mb-0", children: [
|
|
252
|
-
/* @__PURE__ */ jsx2("i", { className: "bi bi-folder2-open me-2" }),
|
|
253
|
-
"Browse files",
|
|
254
|
-
/* @__PURE__ */ jsx2(
|
|
255
|
-
"input",
|
|
256
|
-
{
|
|
257
|
-
type: "file",
|
|
258
|
-
className: "d-none",
|
|
259
|
-
multiple,
|
|
260
|
-
accept,
|
|
261
|
-
onChange: onFileChange
|
|
262
|
-
}
|
|
263
|
-
)
|
|
264
|
-
] }),
|
|
265
|
-
fileNames.length > 0 && /* @__PURE__ */ jsxs2("div", { className: "flex-grow-1", children: [
|
|
266
|
-
/* @__PURE__ */ jsx2("div", { className: "small text-muted", children: "Selected:" }),
|
|
267
|
-
/* @__PURE__ */ jsx2("ul", { className: "mb-0 small", children: fileNames.map((name) => /* @__PURE__ */ jsx2("li", { children: name }, name)) })
|
|
268
|
-
] })
|
|
269
|
-
] });
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
// src/components/UploadProgressList.tsx
|
|
273
|
-
import "react";
|
|
274
|
-
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
275
|
-
var UploadProgressList = ({
|
|
276
|
-
uploads
|
|
277
|
-
}) => {
|
|
278
|
-
if (uploads.length === 0) return null;
|
|
279
|
-
return /* @__PURE__ */ jsx3("div", { className: "mt-3", children: uploads.map((u) => /* @__PURE__ */ jsxs3("div", { className: "mb-2", children: [
|
|
280
|
-
/* @__PURE__ */ jsxs3("div", { className: "d-flex justify-content-between small mb-1", children: [
|
|
281
|
-
/* @__PURE__ */ jsxs3("span", { children: [
|
|
282
|
-
u.file.name,
|
|
283
|
-
" - ",
|
|
284
|
-
u?.publicUrl ?? ""
|
|
285
|
-
] }),
|
|
286
|
-
/* @__PURE__ */ jsx3("span", { children: u.status === "success" ? "Completed" : u.status === "error" ? "Error" : `${u.progress}%` })
|
|
287
|
-
] }),
|
|
288
|
-
/* @__PURE__ */ jsx3("div", { className: "progress", style: { height: "6px" }, children: /* @__PURE__ */ jsx3(
|
|
289
|
-
"div",
|
|
290
|
-
{
|
|
291
|
-
className: "progress-bar " + (u.status === "success" ? "bg-success" : u.status === "error" ? "bg-danger" : ""),
|
|
292
|
-
role: "progressbar",
|
|
293
|
-
style: { width: `${u.progress}%` },
|
|
294
|
-
"aria-valuenow": u.progress,
|
|
295
|
-
"aria-valuemin": 0,
|
|
296
|
-
"aria-valuemax": 100
|
|
297
|
-
}
|
|
298
|
-
) }),
|
|
299
|
-
u.status === "error" && /* @__PURE__ */ jsx3("div", { className: "text-danger small mt-1", children: u.error ?? "Upload failed" })
|
|
300
|
-
] }, u.id)) });
|
|
301
|
-
};
|
|
302
|
-
|
|
303
257
|
// src/hooks/UseUploadManager.ts
|
|
304
258
|
import { useState, useCallback } from "react";
|
|
305
259
|
|
|
306
260
|
// src/engines/UploadFileToS3.ts
|
|
307
|
-
async function UploadFileToS3(file, presignedUrl, onProgress) {
|
|
261
|
+
async function UploadFileToS3(file, presignedUrl, onProgress, maxRetries = 3) {
|
|
262
|
+
let attempt = 0;
|
|
263
|
+
while (true) {
|
|
264
|
+
try {
|
|
265
|
+
await uploadOnce(file, presignedUrl, onProgress);
|
|
266
|
+
return;
|
|
267
|
+
} catch (err) {
|
|
268
|
+
if (attempt >= maxRetries) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`Upload failed after ${attempt + 1} attempts: ${err.message}`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
attempt++;
|
|
274
|
+
const delay = 300 * attempt;
|
|
275
|
+
await new Promise((res) => setTimeout(res, delay));
|
|
276
|
+
console.warn(`Retrying upload (attempt ${attempt + 1})...`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function uploadOnce(file, presignedUrl, onProgress) {
|
|
308
281
|
return new Promise((resolve, reject) => {
|
|
309
282
|
const xhr = new XMLHttpRequest();
|
|
310
283
|
xhr.open("PUT", presignedUrl);
|
|
@@ -319,15 +292,11 @@ async function UploadFileToS3(file, presignedUrl, onProgress) {
|
|
|
319
292
|
resolve();
|
|
320
293
|
} else {
|
|
321
294
|
reject(
|
|
322
|
-
new Error(
|
|
323
|
-
`S3 upload failed with status ${xhr.status}: ${xhr.statusText}`
|
|
324
|
-
)
|
|
295
|
+
new Error(`S3 upload failed: ${xhr.status} ${xhr.statusText}`)
|
|
325
296
|
);
|
|
326
297
|
}
|
|
327
298
|
};
|
|
328
|
-
xhr.onerror = () =>
|
|
329
|
-
reject(new Error("Network error while uploading to S3"));
|
|
330
|
-
};
|
|
299
|
+
xhr.onerror = () => reject(new Error("Network error while uploading to S3"));
|
|
331
300
|
xhr.setRequestHeader(
|
|
332
301
|
"Content-Type",
|
|
333
302
|
file.type || "application/octet-stream"
|
|
@@ -361,9 +330,7 @@ function UseUploadManager({
|
|
|
361
330
|
if (!getPresignedUrl) {
|
|
362
331
|
throw new Error("getPresignedUrl is not provided.");
|
|
363
332
|
}
|
|
364
|
-
const presignedUrl = await
|
|
365
|
-
upload.file
|
|
366
|
-
);
|
|
333
|
+
const presignedUrl = await getPresignedUrlWithRetry(upload.file, 10);
|
|
367
334
|
const url = presignedUrl?.PresignedUrl ?? "";
|
|
368
335
|
await UploadFileToS3(upload.file, url, (progress) => {
|
|
369
336
|
setUploads(
|
|
@@ -404,6 +371,20 @@ function UseUploadManager({
|
|
|
404
371
|
},
|
|
405
372
|
[getPresignedUrl, onUploadComplete, onUploadError]
|
|
406
373
|
);
|
|
374
|
+
async function getPresignedUrlWithRetry(file, attempts = 3) {
|
|
375
|
+
let lastError;
|
|
376
|
+
for (let i = 1; i <= attempts; i++) {
|
|
377
|
+
try {
|
|
378
|
+
return await getPresignedUrl(file);
|
|
379
|
+
} catch (err) {
|
|
380
|
+
lastError = err;
|
|
381
|
+
if (i < attempts) {
|
|
382
|
+
await new Promise((r) => setTimeout(r, 500 * i));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
throw lastError instanceof Error ? lastError : new Error("Failed to fetch presigned URL");
|
|
387
|
+
}
|
|
407
388
|
const startUploadsIfNeeded = useCallback(
|
|
408
389
|
(files) => {
|
|
409
390
|
if (!autoUpload || !getPresignedUrl) return;
|
|
@@ -423,23 +404,290 @@ function UseUploadManager({
|
|
|
423
404
|
};
|
|
424
405
|
}
|
|
425
406
|
|
|
407
|
+
// src/components/UploadProgressList.tsx
|
|
408
|
+
import "react";
|
|
409
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
410
|
+
var UploadProgressList = ({
|
|
411
|
+
uploads
|
|
412
|
+
}) => {
|
|
413
|
+
const visibleUploads = uploads.filter((u) => u.status !== "success");
|
|
414
|
+
if (visibleUploads.length === 0) return null;
|
|
415
|
+
return /* @__PURE__ */ jsx2(
|
|
416
|
+
"div",
|
|
417
|
+
{
|
|
418
|
+
className: "w-100 d-flex flex-wrap gap-4 align-content-start mt-3",
|
|
419
|
+
style: { minHeight: "80px" },
|
|
420
|
+
children: visibleUploads.map((u) => /* @__PURE__ */ jsxs(
|
|
421
|
+
"div",
|
|
422
|
+
{
|
|
423
|
+
className: "d-flex flex-column align-items-center",
|
|
424
|
+
style: {
|
|
425
|
+
width: 96,
|
|
426
|
+
userSelect: "none"
|
|
427
|
+
},
|
|
428
|
+
title: u.file.name,
|
|
429
|
+
children: [
|
|
430
|
+
/* @__PURE__ */ jsxs(
|
|
431
|
+
"div",
|
|
432
|
+
{
|
|
433
|
+
className: "bg-white border rounded-3 d-flex align-items-center justify-content-center mb-1 shadow-sm position-relative",
|
|
434
|
+
style: {
|
|
435
|
+
width: 64,
|
|
436
|
+
height: 64
|
|
437
|
+
},
|
|
438
|
+
children: [
|
|
439
|
+
/* @__PURE__ */ jsx2("i", { className: "bi bi-file-earmark fs-2" }),
|
|
440
|
+
u.status === "uploading" && /* @__PURE__ */ jsx2(
|
|
441
|
+
"div",
|
|
442
|
+
{
|
|
443
|
+
className: "position-absolute top-50 start-50 translate-middle",
|
|
444
|
+
style: { pointerEvents: "none" },
|
|
445
|
+
children: /* @__PURE__ */ jsx2("div", { className: "spinner-border spinner-border-sm text-primary" })
|
|
446
|
+
}
|
|
447
|
+
),
|
|
448
|
+
u.status === "error" && /* @__PURE__ */ jsx2("span", { className: "position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger", children: "!" })
|
|
449
|
+
]
|
|
450
|
+
}
|
|
451
|
+
),
|
|
452
|
+
/* @__PURE__ */ jsx2(
|
|
453
|
+
"div",
|
|
454
|
+
{
|
|
455
|
+
className: "small text-center text-truncate",
|
|
456
|
+
style: { width: "100%" },
|
|
457
|
+
children: u.file.name
|
|
458
|
+
}
|
|
459
|
+
),
|
|
460
|
+
/* @__PURE__ */ jsx2("div", { className: "w-100 mt-1", children: /* @__PURE__ */ jsx2("div", { className: "progress", style: { height: "4px" }, children: /* @__PURE__ */ jsx2(
|
|
461
|
+
"div",
|
|
462
|
+
{
|
|
463
|
+
className: "progress-bar " + (u.status === "error" ? "bg-danger" : ""),
|
|
464
|
+
role: "progressbar",
|
|
465
|
+
style: { width: `${u.progress}%` },
|
|
466
|
+
"aria-valuenow": u.progress,
|
|
467
|
+
"aria-valuemin": 0,
|
|
468
|
+
"aria-valuemax": 100
|
|
469
|
+
}
|
|
470
|
+
) }) }),
|
|
471
|
+
u.status === "error" && /* @__PURE__ */ jsx2(
|
|
472
|
+
"div",
|
|
473
|
+
{
|
|
474
|
+
className: "text-danger small mt-1 text-center",
|
|
475
|
+
style: { width: "100%" },
|
|
476
|
+
children: u.error ?? "Upload failed"
|
|
477
|
+
}
|
|
478
|
+
)
|
|
479
|
+
]
|
|
480
|
+
},
|
|
481
|
+
u.id
|
|
482
|
+
))
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
// src/components/DesktopFileIcon.tsx
|
|
488
|
+
import { useEffect, useRef, useState as useState2 } from "react";
|
|
489
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
490
|
+
var DesktopFileIcon = ({
|
|
491
|
+
name,
|
|
492
|
+
sizeBytes,
|
|
493
|
+
downloadUrl,
|
|
494
|
+
onOpen,
|
|
495
|
+
onDelete
|
|
496
|
+
}) => {
|
|
497
|
+
const [contextMenuPos, setContextMenuPos] = useState2(
|
|
498
|
+
null
|
|
499
|
+
);
|
|
500
|
+
const [isHovered, setIsHovered] = useState2(false);
|
|
501
|
+
const [isDeleting, setIsDeleting] = useState2(false);
|
|
502
|
+
const iconRef = useRef(null);
|
|
503
|
+
const menuRef = useRef(null);
|
|
504
|
+
const handleDoubleClick = () => {
|
|
505
|
+
if (isDeleting) return;
|
|
506
|
+
if (onOpen) {
|
|
507
|
+
onOpen();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (downloadUrl) {
|
|
511
|
+
const a = document.createElement("a");
|
|
512
|
+
a.href = downloadUrl;
|
|
513
|
+
a.download = name ?? "";
|
|
514
|
+
a.target = "_blank";
|
|
515
|
+
a.rel = "noopener noreferrer";
|
|
516
|
+
document.body.appendChild(a);
|
|
517
|
+
a.click();
|
|
518
|
+
document.body.removeChild(a);
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
const handleContextMenu = (e) => {
|
|
522
|
+
if (isDeleting) return;
|
|
523
|
+
e.preventDefault();
|
|
524
|
+
setContextMenuPos({ x: e.clientX, y: e.clientY });
|
|
525
|
+
};
|
|
526
|
+
const closeMenu = () => setContextMenuPos(null);
|
|
527
|
+
const handleDownload = () => {
|
|
528
|
+
closeMenu();
|
|
529
|
+
if (!downloadUrl || isDeleting) return;
|
|
530
|
+
const a = document.createElement("a");
|
|
531
|
+
a.href = downloadUrl;
|
|
532
|
+
a.download = name ?? "";
|
|
533
|
+
a.target = "_blank";
|
|
534
|
+
a.rel = "noopener noreferrer";
|
|
535
|
+
document.body.appendChild(a);
|
|
536
|
+
a.click();
|
|
537
|
+
document.body.removeChild(a);
|
|
538
|
+
};
|
|
539
|
+
const handleCopyUrl = async () => {
|
|
540
|
+
closeMenu();
|
|
541
|
+
if (!downloadUrl || isDeleting) return;
|
|
542
|
+
try {
|
|
543
|
+
await navigator.clipboard?.writeText(downloadUrl);
|
|
544
|
+
} catch (err) {
|
|
545
|
+
console.error("Failed to copy URL", err);
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
const handleDelete = async () => {
|
|
549
|
+
closeMenu();
|
|
550
|
+
if (!onDelete) return;
|
|
551
|
+
try {
|
|
552
|
+
setIsDeleting(true);
|
|
553
|
+
await Promise.resolve(onDelete());
|
|
554
|
+
} catch (err) {
|
|
555
|
+
console.error("Delete failed", err);
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
const formattedSize = typeof sizeBytes === "number" ? `${(sizeBytes / 1024).toFixed(1)} KB` : void 0;
|
|
559
|
+
useEffect(() => {
|
|
560
|
+
if (!contextMenuPos) return;
|
|
561
|
+
const handleGlobalClick = (e) => {
|
|
562
|
+
const target = e.target;
|
|
563
|
+
if (menuRef.current && menuRef.current.contains(target)) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (iconRef.current && iconRef.current.contains(target)) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
closeMenu();
|
|
570
|
+
};
|
|
571
|
+
document.addEventListener("mousedown", handleGlobalClick);
|
|
572
|
+
return () => {
|
|
573
|
+
document.removeEventListener("mousedown", handleGlobalClick);
|
|
574
|
+
};
|
|
575
|
+
}, [contextMenuPos]);
|
|
576
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
577
|
+
/* @__PURE__ */ jsxs2(
|
|
578
|
+
"div",
|
|
579
|
+
{
|
|
580
|
+
ref: iconRef,
|
|
581
|
+
className: "d-flex flex-column align-items-center rounded-3 p-1 " + (isHovered || contextMenuPos ? "bg-light border-primary" : "border-transparent"),
|
|
582
|
+
style: {
|
|
583
|
+
width: 96,
|
|
584
|
+
cursor: isDeleting ? "default" : "pointer",
|
|
585
|
+
userSelect: "none",
|
|
586
|
+
transition: "background-color 0.1s ease, border-color 0.1s ease, opacity 0.1s ease",
|
|
587
|
+
opacity: isDeleting ? 0.6 : 1,
|
|
588
|
+
borderWidth: 1,
|
|
589
|
+
borderStyle: "solid"
|
|
590
|
+
},
|
|
591
|
+
onDoubleClick: handleDoubleClick,
|
|
592
|
+
onContextMenu: handleContextMenu,
|
|
593
|
+
title: name ?? void 0,
|
|
594
|
+
onClick: () => {
|
|
595
|
+
if (contextMenuPos) {
|
|
596
|
+
closeMenu();
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
onMouseEnter: () => setIsHovered(true),
|
|
600
|
+
onMouseLeave: () => setIsHovered(false),
|
|
601
|
+
children: [
|
|
602
|
+
/* @__PURE__ */ jsxs2(
|
|
603
|
+
"div",
|
|
604
|
+
{
|
|
605
|
+
className: "bg-white border rounded-3 d-flex align-items-center justify-content-center mb-1 shadow-sm position-relative",
|
|
606
|
+
style: {
|
|
607
|
+
width: 64,
|
|
608
|
+
height: 64
|
|
609
|
+
},
|
|
610
|
+
children: [
|
|
611
|
+
/* @__PURE__ */ jsx3("i", { className: "bi bi-file-earmark fs-2" }),
|
|
612
|
+
isDeleting && /* @__PURE__ */ jsx3("div", { className: "position-absolute top-50 start-50 translate-middle", children: /* @__PURE__ */ jsx3("div", { className: "spinner-border spinner-border-sm text-danger" }) })
|
|
613
|
+
]
|
|
614
|
+
}
|
|
615
|
+
),
|
|
616
|
+
/* @__PURE__ */ jsx3(
|
|
617
|
+
"div",
|
|
618
|
+
{
|
|
619
|
+
className: "small text-center text-truncate",
|
|
620
|
+
style: { width: "100%" },
|
|
621
|
+
children: name
|
|
622
|
+
}
|
|
623
|
+
),
|
|
624
|
+
formattedSize && /* @__PURE__ */ jsx3("small", { className: "text-muted mt-1", children: formattedSize })
|
|
625
|
+
]
|
|
626
|
+
}
|
|
627
|
+
),
|
|
628
|
+
contextMenuPos && !isDeleting && /* @__PURE__ */ jsxs2(
|
|
629
|
+
"div",
|
|
630
|
+
{
|
|
631
|
+
ref: menuRef,
|
|
632
|
+
className: "position-fixed dropdown-menu show shadow-sm",
|
|
633
|
+
style: {
|
|
634
|
+
top: contextMenuPos.y,
|
|
635
|
+
left: contextMenuPos.x,
|
|
636
|
+
zIndex: 1050,
|
|
637
|
+
minWidth: 180
|
|
638
|
+
},
|
|
639
|
+
onClick: (e) => e.stopPropagation(),
|
|
640
|
+
children: [
|
|
641
|
+
/* @__PURE__ */ jsx3(
|
|
642
|
+
"button",
|
|
643
|
+
{
|
|
644
|
+
type: "button",
|
|
645
|
+
className: "dropdown-item small",
|
|
646
|
+
onClick: handleDownload,
|
|
647
|
+
disabled: !downloadUrl,
|
|
648
|
+
children: "Download file"
|
|
649
|
+
}
|
|
650
|
+
),
|
|
651
|
+
/* @__PURE__ */ jsx3(
|
|
652
|
+
"button",
|
|
653
|
+
{
|
|
654
|
+
type: "button",
|
|
655
|
+
className: "dropdown-item small",
|
|
656
|
+
onClick: handleCopyUrl,
|
|
657
|
+
disabled: !downloadUrl,
|
|
658
|
+
children: "Copy download URL"
|
|
659
|
+
}
|
|
660
|
+
),
|
|
661
|
+
/* @__PURE__ */ jsx3("div", { className: "dropdown-divider" }),
|
|
662
|
+
/* @__PURE__ */ jsx3(
|
|
663
|
+
"button",
|
|
664
|
+
{
|
|
665
|
+
type: "button",
|
|
666
|
+
className: "dropdown-item text-danger small",
|
|
667
|
+
onClick: handleDelete,
|
|
668
|
+
children: "Delete file"
|
|
669
|
+
}
|
|
670
|
+
)
|
|
671
|
+
]
|
|
672
|
+
}
|
|
673
|
+
)
|
|
674
|
+
] });
|
|
675
|
+
};
|
|
676
|
+
|
|
426
677
|
// src/components/UploadContainer.tsx
|
|
427
|
-
import {
|
|
678
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
428
679
|
var UploadContainer = ({
|
|
429
|
-
title = "Upload files",
|
|
430
|
-
description = "Drag and drop files here, or click the button to browse.",
|
|
431
|
-
multiple = true,
|
|
432
|
-
accept,
|
|
433
680
|
onFilesSelected,
|
|
434
681
|
existingFiles = [],
|
|
682
|
+
existingFilesLoading = false,
|
|
435
683
|
onExistingFileClick,
|
|
684
|
+
onDeleteFile,
|
|
436
685
|
autoUpload = false,
|
|
437
686
|
getPresignedUrl,
|
|
438
687
|
onUploadComplete,
|
|
439
688
|
onUploadError
|
|
440
689
|
}) => {
|
|
441
|
-
const [isDragging, setIsDragging] =
|
|
442
|
-
const [fileNames, setFileNames] = useState2([]);
|
|
690
|
+
const [isDragging, setIsDragging] = useState3(false);
|
|
443
691
|
const { uploads, startUploadsIfNeeded } = UseUploadManager({
|
|
444
692
|
autoUpload,
|
|
445
693
|
getPresignedUrl,
|
|
@@ -459,23 +707,16 @@ var UploadContainer = ({
|
|
|
459
707
|
setIsDragging(false);
|
|
460
708
|
const files = e.dataTransfer.files;
|
|
461
709
|
if (!files || files.length === 0) return;
|
|
462
|
-
setFileNames(Array.from(files).map((f) => f.name));
|
|
463
710
|
onFilesSelected?.(files);
|
|
464
711
|
startUploadsIfNeeded(files);
|
|
465
712
|
};
|
|
466
|
-
const
|
|
467
|
-
const files = e.target.files;
|
|
468
|
-
if (!files || files.length === 0) return;
|
|
469
|
-
setFileNames(Array.from(files).map((f) => f.name));
|
|
470
|
-
onFilesSelected?.(files);
|
|
471
|
-
startUploadsIfNeeded(files);
|
|
472
|
-
};
|
|
473
|
-
const handleExistingFileClickInternal = (file) => {
|
|
713
|
+
const handleExistingFileOpen = (file) => {
|
|
474
714
|
if (onExistingFileClick) {
|
|
475
715
|
onExistingFileClick(file);
|
|
476
716
|
return;
|
|
477
717
|
}
|
|
478
718
|
const a = document.createElement("a");
|
|
719
|
+
a.href = file.PublicUrl ?? "";
|
|
479
720
|
a.download = file.Name ?? "";
|
|
480
721
|
a.target = "_blank";
|
|
481
722
|
a.rel = "noopener noreferrer";
|
|
@@ -483,62 +724,52 @@ var UploadContainer = ({
|
|
|
483
724
|
a.click();
|
|
484
725
|
document.body.removeChild(a);
|
|
485
726
|
};
|
|
486
|
-
return /* @__PURE__ */
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
UploadFilePicker,
|
|
519
|
-
{
|
|
520
|
-
multiple,
|
|
521
|
-
accept,
|
|
522
|
-
fileNames,
|
|
523
|
-
onFileChange: handleFileChange
|
|
524
|
-
}
|
|
525
|
-
),
|
|
526
|
-
/* @__PURE__ */ jsx4(UploadProgressList, { uploads })
|
|
527
|
-
] });
|
|
727
|
+
return /* @__PURE__ */ jsxs3(
|
|
728
|
+
UploadDropzone,
|
|
729
|
+
{
|
|
730
|
+
isDragging,
|
|
731
|
+
onDragOver: handleDragOver,
|
|
732
|
+
onDragLeave: handleDragLeave,
|
|
733
|
+
onDrop: handleDrop,
|
|
734
|
+
className: "w-100",
|
|
735
|
+
style: { minHeight: "260px", alignItems: "stretch" },
|
|
736
|
+
children: [
|
|
737
|
+
/* @__PURE__ */ jsx4("div", { className: "w-100 mb-3", children: /* @__PURE__ */ jsx4("div", { className: "d-flex flex-column flex-md-row align-items-start align-items-md-center justify-content-between gap-3", children: /* @__PURE__ */ jsx4("div", { className: "flex-grow-1 w-100", children: /* @__PURE__ */ jsx4(UploadProgressList, { uploads }) }) }) }),
|
|
738
|
+
/* @__PURE__ */ jsx4(
|
|
739
|
+
"div",
|
|
740
|
+
{
|
|
741
|
+
className: "w-100 d-flex flex-wrap gap-4 align-content-start",
|
|
742
|
+
style: { minHeight: "140px" },
|
|
743
|
+
children: existingFilesLoading ? /* @__PURE__ */ jsx4("div", { className: "w-100 d-flex justify-content-center align-items-center", children: /* @__PURE__ */ jsx4("div", { className: "spinner-border text-secondary", role: "status", children: /* @__PURE__ */ jsx4("span", { className: "visually-hidden", children: "Loading containers..." }) }) }) : existingFiles.map((file) => /* @__PURE__ */ jsx4(
|
|
744
|
+
DesktopFileIcon,
|
|
745
|
+
{
|
|
746
|
+
name: file.Name,
|
|
747
|
+
sizeBytes: file.FileSize,
|
|
748
|
+
downloadUrl: file.PublicUrl,
|
|
749
|
+
onOpen: () => handleExistingFileOpen(file),
|
|
750
|
+
onDelete: () => onDeleteFile?.(file)
|
|
751
|
+
},
|
|
752
|
+
file.Id
|
|
753
|
+
))
|
|
754
|
+
}
|
|
755
|
+
)
|
|
756
|
+
]
|
|
757
|
+
}
|
|
758
|
+
);
|
|
528
759
|
};
|
|
529
760
|
|
|
530
761
|
// src/hooks/UseContainers.ts
|
|
531
|
-
import { useEffect, useState as
|
|
762
|
+
import { useEffect as useEffect2, useState as useState4 } from "react";
|
|
532
763
|
function UseContainers({ apiBaseUrl, parentId }) {
|
|
533
|
-
const [containers, setContainers] =
|
|
534
|
-
const [loading, setLoading] =
|
|
535
|
-
const [error, setError] =
|
|
764
|
+
const [containers, setContainers] = useState4([]);
|
|
765
|
+
const [loading, setLoading] = useState4(false);
|
|
766
|
+
const [error, setError] = useState4(null);
|
|
536
767
|
const load = async () => {
|
|
537
768
|
setLoading(true);
|
|
538
769
|
setError(null);
|
|
539
770
|
try {
|
|
540
771
|
const sdkDb = new SparkStudioStorageSDK(
|
|
541
|
-
|
|
772
|
+
apiBaseUrl
|
|
542
773
|
);
|
|
543
774
|
const result = await sdkDb.container.ReadRootContainers();
|
|
544
775
|
setContainers(result);
|
|
@@ -548,11 +779,12 @@ function UseContainers({ apiBaseUrl, parentId }) {
|
|
|
548
779
|
setLoading(false);
|
|
549
780
|
}
|
|
550
781
|
};
|
|
551
|
-
|
|
782
|
+
useEffect2(() => {
|
|
552
783
|
void load();
|
|
553
784
|
}, [apiBaseUrl, parentId]);
|
|
554
785
|
return {
|
|
555
786
|
containers,
|
|
787
|
+
setContainers,
|
|
556
788
|
loading,
|
|
557
789
|
error,
|
|
558
790
|
reload: load
|
|
@@ -560,31 +792,40 @@ function UseContainers({ apiBaseUrl, parentId }) {
|
|
|
560
792
|
}
|
|
561
793
|
|
|
562
794
|
// src/components/ContainerUploadPanel.tsx
|
|
563
|
-
import { jsx as jsx5
|
|
795
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
564
796
|
var ContainerUploadPanel = ({
|
|
565
797
|
containerApiBaseUrl,
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
description = "Drop files to upload. Existing files are listed below."
|
|
798
|
+
storageApiBaseUrl,
|
|
799
|
+
parentContainerId
|
|
569
800
|
}) => {
|
|
570
|
-
const { containers,
|
|
801
|
+
const { containers, setContainers, reload, loading } = UseContainers({
|
|
571
802
|
apiBaseUrl: containerApiBaseUrl,
|
|
572
803
|
parentId: parentContainerId
|
|
573
804
|
});
|
|
574
805
|
const getPresignedUrl = async (file) => {
|
|
575
|
-
const sdkDb = new SparkStudioStorageSDK(
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
const sdkS3 = new SparkStudioStorageSDK(
|
|
579
|
-
"https://iq0gmcn0pd.execute-api.us-east-2.amazonaws.com/Prod"
|
|
580
|
-
);
|
|
806
|
+
const sdkDb = new SparkStudioStorageSDK(containerApiBaseUrl);
|
|
807
|
+
const sdkS3 = new SparkStudioStorageSDK(storageApiBaseUrl);
|
|
808
|
+
const contentType = file.type || "application/octet-stream";
|
|
581
809
|
const containerDTO = await sdkDb.container.CreateFileContainer(
|
|
582
810
|
file.name,
|
|
583
811
|
file.size,
|
|
584
|
-
encodeURIComponent(
|
|
812
|
+
encodeURIComponent(contentType)
|
|
585
813
|
);
|
|
586
|
-
|
|
587
|
-
|
|
814
|
+
async function getPresignedUrlWithRetry(container, attempts = 3) {
|
|
815
|
+
let lastError;
|
|
816
|
+
for (let i = 1; i <= attempts; i++) {
|
|
817
|
+
try {
|
|
818
|
+
return await sdkS3.s3.GetPreSignedUrl(container);
|
|
819
|
+
} catch (err) {
|
|
820
|
+
lastError = err;
|
|
821
|
+
if (i < attempts) {
|
|
822
|
+
await new Promise((r) => setTimeout(r, 500 * i));
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
throw lastError instanceof Error ? lastError : new Error("Failed to fetch presigned URL");
|
|
827
|
+
}
|
|
828
|
+
return getPresignedUrlWithRetry(containerDTO);
|
|
588
829
|
};
|
|
589
830
|
const handleUploadComplete = async (file, s3Url) => {
|
|
590
831
|
console.log("Upload complete:", file.name, s3Url);
|
|
@@ -594,9 +835,8 @@ var ContainerUploadPanel = ({
|
|
|
594
835
|
console.error("Upload failed:", file.name, err);
|
|
595
836
|
};
|
|
596
837
|
const handleExistingFileClick = (file) => {
|
|
597
|
-
const downloadUrl = `${containerApiBaseUrl}/api/Container/Download/${file.Id}`;
|
|
598
838
|
const a = document.createElement("a");
|
|
599
|
-
a.href =
|
|
839
|
+
a.href = file.PublicUrl ?? "";
|
|
600
840
|
a.download = file.Name ?? "";
|
|
601
841
|
a.target = "_blank";
|
|
602
842
|
a.rel = "noopener noreferrer";
|
|
@@ -604,26 +844,26 @@ var ContainerUploadPanel = ({
|
|
|
604
844
|
a.click();
|
|
605
845
|
document.body.removeChild(a);
|
|
606
846
|
};
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
847
|
+
const handleDeleteFile = async (file) => {
|
|
848
|
+
const sdkDb = new SparkStudioStorageSDK(containerApiBaseUrl);
|
|
849
|
+
const sdkS3 = new SparkStudioStorageSDK(storageApiBaseUrl);
|
|
850
|
+
await sdkDb.container.DeleteContainer(file.Id);
|
|
851
|
+
await sdkS3.s3.DeleteS3(file);
|
|
852
|
+
setContainers((prev) => prev.filter((c) => c.Id !== file.Id));
|
|
853
|
+
};
|
|
854
|
+
return /* @__PURE__ */ jsx5(
|
|
855
|
+
UploadContainer,
|
|
856
|
+
{
|
|
857
|
+
existingFiles: containers,
|
|
858
|
+
existingFilesLoading: loading,
|
|
859
|
+
onExistingFileClick: handleExistingFileClick,
|
|
860
|
+
autoUpload: true,
|
|
861
|
+
getPresignedUrl,
|
|
862
|
+
onUploadComplete: handleUploadComplete,
|
|
863
|
+
onUploadError: handleUploadError,
|
|
864
|
+
onDeleteFile: handleDeleteFile
|
|
865
|
+
}
|
|
866
|
+
);
|
|
627
867
|
};
|
|
628
868
|
|
|
629
869
|
// src/views/HomeView.tsx
|
|
@@ -633,7 +873,7 @@ import {
|
|
|
633
873
|
UserInfoCard,
|
|
634
874
|
useUser
|
|
635
875
|
} from "@sparkstudio/authentication-ui";
|
|
636
|
-
import { Fragment as Fragment2, jsx as jsx6, jsxs as
|
|
876
|
+
import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
637
877
|
function HomeView() {
|
|
638
878
|
return /* @__PURE__ */ jsx6(
|
|
639
879
|
AuthenticatorProvider,
|
|
@@ -647,13 +887,13 @@ function HomeView() {
|
|
|
647
887
|
}
|
|
648
888
|
function HomeContent() {
|
|
649
889
|
const { user } = useUser();
|
|
650
|
-
return /* @__PURE__ */
|
|
890
|
+
return /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
651
891
|
/* @__PURE__ */ jsx6(UserInfoCard, {}),
|
|
652
892
|
user && /* @__PURE__ */ jsx6(
|
|
653
893
|
ContainerUploadPanel,
|
|
654
894
|
{
|
|
655
|
-
containerApiBaseUrl: "https://
|
|
656
|
-
storageApiBaseUrl: "https://
|
|
895
|
+
containerApiBaseUrl: "https://localhost:5001",
|
|
896
|
+
storageApiBaseUrl: "https://localhost:5001"
|
|
657
897
|
}
|
|
658
898
|
)
|
|
659
899
|
] });
|
|
@@ -664,13 +904,13 @@ export {
|
|
|
664
904
|
ContainerDTO,
|
|
665
905
|
ContainerType,
|
|
666
906
|
ContainerUploadPanel,
|
|
907
|
+
DesktopFileIcon,
|
|
667
908
|
Home,
|
|
668
909
|
HomeView,
|
|
669
910
|
S3,
|
|
670
911
|
SparkStudioStorageSDK,
|
|
671
912
|
UploadContainer,
|
|
672
913
|
UploadDropzone,
|
|
673
|
-
UploadFilePicker,
|
|
674
914
|
UploadFileToS3,
|
|
675
915
|
UploadProgressList,
|
|
676
916
|
UseContainers,
|