@sparkstudio/storage-ui 1.0.13 → 1.0.14
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 +399 -184
- package/dist/index.d.cts +27 -18
- package/dist/index.d.ts +27 -18
- package/dist/index.js +405 -190
- 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,267 @@ 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 === "error" && /* @__PURE__ */ jsx2("span", { className: "position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger", children: "!" })
|
|
441
|
+
]
|
|
442
|
+
}
|
|
443
|
+
),
|
|
444
|
+
/* @__PURE__ */ jsx2(
|
|
445
|
+
"div",
|
|
446
|
+
{
|
|
447
|
+
className: "small text-center text-truncate",
|
|
448
|
+
style: { width: "100%" },
|
|
449
|
+
children: u.file.name
|
|
450
|
+
}
|
|
451
|
+
),
|
|
452
|
+
/* @__PURE__ */ jsx2("div", { className: "w-100 mt-1", children: /* @__PURE__ */ jsx2("div", { className: "progress", style: { height: "4px" }, children: /* @__PURE__ */ jsx2(
|
|
453
|
+
"div",
|
|
454
|
+
{
|
|
455
|
+
className: "progress-bar " + (u.status === "error" ? "bg-danger" : ""),
|
|
456
|
+
role: "progressbar",
|
|
457
|
+
style: { width: `${u.progress}%` },
|
|
458
|
+
"aria-valuenow": u.progress,
|
|
459
|
+
"aria-valuemin": 0,
|
|
460
|
+
"aria-valuemax": 100
|
|
461
|
+
}
|
|
462
|
+
) }) }),
|
|
463
|
+
u.status === "error" && /* @__PURE__ */ jsx2(
|
|
464
|
+
"div",
|
|
465
|
+
{
|
|
466
|
+
className: "text-danger small mt-1 text-center",
|
|
467
|
+
style: { width: "100%" },
|
|
468
|
+
children: u.error ?? "Upload failed"
|
|
469
|
+
}
|
|
470
|
+
)
|
|
471
|
+
]
|
|
472
|
+
},
|
|
473
|
+
u.id
|
|
474
|
+
))
|
|
475
|
+
}
|
|
476
|
+
);
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
// src/components/DesktopFileIcon.tsx
|
|
480
|
+
import { useEffect, useRef, useState as useState2 } from "react";
|
|
481
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
482
|
+
var DesktopFileIcon = ({
|
|
483
|
+
name,
|
|
484
|
+
sizeBytes,
|
|
485
|
+
downloadUrl,
|
|
486
|
+
onOpen,
|
|
487
|
+
onDelete
|
|
488
|
+
}) => {
|
|
489
|
+
const [contextMenuPos, setContextMenuPos] = useState2(
|
|
490
|
+
null
|
|
491
|
+
);
|
|
492
|
+
const [isHovered, setIsHovered] = useState2(false);
|
|
493
|
+
const iconRef = useRef(null);
|
|
494
|
+
const menuRef = useRef(null);
|
|
495
|
+
const handleDoubleClick = () => {
|
|
496
|
+
if (onOpen) {
|
|
497
|
+
onOpen();
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (downloadUrl) {
|
|
501
|
+
const a = document.createElement("a");
|
|
502
|
+
a.href = downloadUrl;
|
|
503
|
+
a.download = name ?? "";
|
|
504
|
+
a.target = "_blank";
|
|
505
|
+
a.rel = "noopener noreferrer";
|
|
506
|
+
document.body.appendChild(a);
|
|
507
|
+
a.click();
|
|
508
|
+
document.body.removeChild(a);
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
const handleContextMenu = (e) => {
|
|
512
|
+
e.preventDefault();
|
|
513
|
+
setContextMenuPos({ x: e.clientX, y: e.clientY });
|
|
514
|
+
};
|
|
515
|
+
const closeMenu = () => setContextMenuPos(null);
|
|
516
|
+
const handleDownload = () => {
|
|
517
|
+
closeMenu();
|
|
518
|
+
if (!downloadUrl) return;
|
|
519
|
+
const a = document.createElement("a");
|
|
520
|
+
a.href = downloadUrl;
|
|
521
|
+
a.download = name ?? "";
|
|
522
|
+
a.target = "_blank";
|
|
523
|
+
a.rel = "noopener noreferrer";
|
|
524
|
+
document.body.appendChild(a);
|
|
525
|
+
a.click();
|
|
526
|
+
document.body.removeChild(a);
|
|
527
|
+
};
|
|
528
|
+
const handleCopyUrl = async () => {
|
|
529
|
+
closeMenu();
|
|
530
|
+
if (!downloadUrl) return;
|
|
531
|
+
try {
|
|
532
|
+
await navigator.clipboard?.writeText(downloadUrl);
|
|
533
|
+
} catch (err) {
|
|
534
|
+
console.error("Failed to copy URL", err);
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
const handleDelete = () => {
|
|
538
|
+
closeMenu();
|
|
539
|
+
onDelete?.();
|
|
540
|
+
};
|
|
541
|
+
const formattedSize = typeof sizeBytes === "number" ? `${(sizeBytes / 1024).toFixed(1)} KB` : void 0;
|
|
542
|
+
useEffect(() => {
|
|
543
|
+
if (!contextMenuPos) return;
|
|
544
|
+
const handleGlobalClick = (e) => {
|
|
545
|
+
const target = e.target;
|
|
546
|
+
if (menuRef.current && menuRef.current.contains(target)) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (iconRef.current && iconRef.current.contains(target)) {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
closeMenu();
|
|
553
|
+
};
|
|
554
|
+
document.addEventListener("mousedown", handleGlobalClick);
|
|
555
|
+
return () => {
|
|
556
|
+
document.removeEventListener("mousedown", handleGlobalClick);
|
|
557
|
+
};
|
|
558
|
+
}, [contextMenuPos]);
|
|
559
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
560
|
+
/* @__PURE__ */ jsxs2(
|
|
561
|
+
"div",
|
|
562
|
+
{
|
|
563
|
+
ref: iconRef,
|
|
564
|
+
className: "d-flex flex-column align-items-center rounded-3 p-1 " + (isHovered || contextMenuPos ? "bg-light border" : ""),
|
|
565
|
+
style: {
|
|
566
|
+
width: 96,
|
|
567
|
+
cursor: "pointer",
|
|
568
|
+
userSelect: "none",
|
|
569
|
+
transition: "background-color 0.1s ease, border-color 0.1s ease"
|
|
570
|
+
},
|
|
571
|
+
onDoubleClick: handleDoubleClick,
|
|
572
|
+
onContextMenu: handleContextMenu,
|
|
573
|
+
title: name ?? void 0,
|
|
574
|
+
onClick: () => {
|
|
575
|
+
if (contextMenuPos) {
|
|
576
|
+
closeMenu();
|
|
577
|
+
}
|
|
578
|
+
},
|
|
579
|
+
onMouseEnter: () => setIsHovered(true),
|
|
580
|
+
onMouseLeave: () => setIsHovered(false),
|
|
581
|
+
children: [
|
|
582
|
+
/* @__PURE__ */ jsx3(
|
|
583
|
+
"div",
|
|
584
|
+
{
|
|
585
|
+
className: "bg-white border rounded-3 d-flex align-items-center justify-content-center mb-1 shadow-sm",
|
|
586
|
+
style: {
|
|
587
|
+
width: 64,
|
|
588
|
+
height: 64
|
|
589
|
+
},
|
|
590
|
+
children: /* @__PURE__ */ jsx3("i", { className: "bi bi-file-earmark fs-2" })
|
|
591
|
+
}
|
|
592
|
+
),
|
|
593
|
+
/* @__PURE__ */ jsx3(
|
|
594
|
+
"div",
|
|
595
|
+
{
|
|
596
|
+
className: "small text-center text-truncate",
|
|
597
|
+
style: { width: "100%" },
|
|
598
|
+
children: name
|
|
599
|
+
}
|
|
600
|
+
),
|
|
601
|
+
formattedSize && /* @__PURE__ */ jsx3("small", { className: "text-muted mt-1", children: formattedSize })
|
|
602
|
+
]
|
|
603
|
+
}
|
|
604
|
+
),
|
|
605
|
+
contextMenuPos && /* @__PURE__ */ jsxs2(
|
|
606
|
+
"div",
|
|
607
|
+
{
|
|
608
|
+
ref: menuRef,
|
|
609
|
+
className: "position-fixed dropdown-menu show shadow-sm",
|
|
610
|
+
style: {
|
|
611
|
+
top: contextMenuPos.y,
|
|
612
|
+
left: contextMenuPos.x,
|
|
613
|
+
zIndex: 1050,
|
|
614
|
+
minWidth: 180
|
|
615
|
+
},
|
|
616
|
+
onClick: (e) => e.stopPropagation(),
|
|
617
|
+
children: [
|
|
618
|
+
/* @__PURE__ */ jsx3(
|
|
619
|
+
"button",
|
|
620
|
+
{
|
|
621
|
+
type: "button",
|
|
622
|
+
className: "dropdown-item small",
|
|
623
|
+
onClick: handleDownload,
|
|
624
|
+
disabled: !downloadUrl,
|
|
625
|
+
children: "Download file"
|
|
626
|
+
}
|
|
627
|
+
),
|
|
628
|
+
/* @__PURE__ */ jsx3(
|
|
629
|
+
"button",
|
|
630
|
+
{
|
|
631
|
+
type: "button",
|
|
632
|
+
className: "dropdown-item small",
|
|
633
|
+
onClick: handleCopyUrl,
|
|
634
|
+
disabled: !downloadUrl,
|
|
635
|
+
children: "Copy download URL"
|
|
636
|
+
}
|
|
637
|
+
),
|
|
638
|
+
/* @__PURE__ */ jsx3("div", { className: "dropdown-divider" }),
|
|
639
|
+
/* @__PURE__ */ jsx3(
|
|
640
|
+
"button",
|
|
641
|
+
{
|
|
642
|
+
type: "button",
|
|
643
|
+
className: "dropdown-item text-danger small",
|
|
644
|
+
onClick: handleDelete,
|
|
645
|
+
children: "Delete file"
|
|
646
|
+
}
|
|
647
|
+
)
|
|
648
|
+
]
|
|
649
|
+
}
|
|
650
|
+
)
|
|
651
|
+
] });
|
|
652
|
+
};
|
|
653
|
+
|
|
426
654
|
// src/components/UploadContainer.tsx
|
|
427
|
-
import {
|
|
655
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
428
656
|
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
657
|
onFilesSelected,
|
|
434
658
|
existingFiles = [],
|
|
659
|
+
existingFilesLoading = false,
|
|
435
660
|
onExistingFileClick,
|
|
661
|
+
onDeleteFile,
|
|
436
662
|
autoUpload = false,
|
|
437
663
|
getPresignedUrl,
|
|
438
664
|
onUploadComplete,
|
|
439
665
|
onUploadError
|
|
440
666
|
}) => {
|
|
441
|
-
const [isDragging, setIsDragging] =
|
|
442
|
-
const [fileNames, setFileNames] = useState2([]);
|
|
667
|
+
const [isDragging, setIsDragging] = useState3(false);
|
|
443
668
|
const { uploads, startUploadsIfNeeded } = UseUploadManager({
|
|
444
669
|
autoUpload,
|
|
445
670
|
getPresignedUrl,
|
|
@@ -459,23 +684,16 @@ var UploadContainer = ({
|
|
|
459
684
|
setIsDragging(false);
|
|
460
685
|
const files = e.dataTransfer.files;
|
|
461
686
|
if (!files || files.length === 0) return;
|
|
462
|
-
setFileNames(Array.from(files).map((f) => f.name));
|
|
463
|
-
onFilesSelected?.(files);
|
|
464
|
-
startUploadsIfNeeded(files);
|
|
465
|
-
};
|
|
466
|
-
const handleFileChange = (e) => {
|
|
467
|
-
const files = e.target.files;
|
|
468
|
-
if (!files || files.length === 0) return;
|
|
469
|
-
setFileNames(Array.from(files).map((f) => f.name));
|
|
470
687
|
onFilesSelected?.(files);
|
|
471
688
|
startUploadsIfNeeded(files);
|
|
472
689
|
};
|
|
473
|
-
const
|
|
690
|
+
const handleExistingFileOpen = (file) => {
|
|
474
691
|
if (onExistingFileClick) {
|
|
475
692
|
onExistingFileClick(file);
|
|
476
693
|
return;
|
|
477
694
|
}
|
|
478
695
|
const a = document.createElement("a");
|
|
696
|
+
a.href = file.PublicUrl ?? "";
|
|
479
697
|
a.download = file.Name ?? "";
|
|
480
698
|
a.target = "_blank";
|
|
481
699
|
a.rel = "noopener noreferrer";
|
|
@@ -483,62 +701,52 @@ var UploadContainer = ({
|
|
|
483
701
|
a.click();
|
|
484
702
|
document.body.removeChild(a);
|
|
485
703
|
};
|
|
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
|
-
] });
|
|
704
|
+
return /* @__PURE__ */ jsxs3(
|
|
705
|
+
UploadDropzone,
|
|
706
|
+
{
|
|
707
|
+
isDragging,
|
|
708
|
+
onDragOver: handleDragOver,
|
|
709
|
+
onDragLeave: handleDragLeave,
|
|
710
|
+
onDrop: handleDrop,
|
|
711
|
+
className: "w-100",
|
|
712
|
+
style: { minHeight: "260px", alignItems: "stretch" },
|
|
713
|
+
children: [
|
|
714
|
+
/* @__PURE__ */ jsx4(
|
|
715
|
+
"div",
|
|
716
|
+
{
|
|
717
|
+
className: "w-100 d-flex flex-wrap gap-4 align-content-start",
|
|
718
|
+
style: { minHeight: "140px" },
|
|
719
|
+
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(
|
|
720
|
+
DesktopFileIcon,
|
|
721
|
+
{
|
|
722
|
+
name: file.Name,
|
|
723
|
+
sizeBytes: typeof file.FileSize === "number" ? file.FileSize : null,
|
|
724
|
+
downloadUrl: file.PublicUrl ?? null,
|
|
725
|
+
onOpen: () => handleExistingFileOpen(file),
|
|
726
|
+
onDelete: onDeleteFile ? () => onDeleteFile(file) : void 0
|
|
727
|
+
},
|
|
728
|
+
file.Id
|
|
729
|
+
))
|
|
730
|
+
}
|
|
731
|
+
),
|
|
732
|
+
/* @__PURE__ */ jsx4("div", { className: "w-100 mt-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 }) }) }) })
|
|
733
|
+
]
|
|
734
|
+
}
|
|
735
|
+
);
|
|
528
736
|
};
|
|
529
737
|
|
|
530
738
|
// src/hooks/UseContainers.ts
|
|
531
|
-
import { useEffect, useState as
|
|
739
|
+
import { useEffect as useEffect2, useState as useState4 } from "react";
|
|
532
740
|
function UseContainers({ apiBaseUrl, parentId }) {
|
|
533
|
-
const [containers, setContainers] =
|
|
534
|
-
const [loading, setLoading] =
|
|
535
|
-
const [error, setError] =
|
|
741
|
+
const [containers, setContainers] = useState4([]);
|
|
742
|
+
const [loading, setLoading] = useState4(false);
|
|
743
|
+
const [error, setError] = useState4(null);
|
|
536
744
|
const load = async () => {
|
|
537
745
|
setLoading(true);
|
|
538
746
|
setError(null);
|
|
539
747
|
try {
|
|
540
748
|
const sdkDb = new SparkStudioStorageSDK(
|
|
541
|
-
|
|
749
|
+
apiBaseUrl
|
|
542
750
|
);
|
|
543
751
|
const result = await sdkDb.container.ReadRootContainers();
|
|
544
752
|
setContainers(result);
|
|
@@ -548,7 +756,7 @@ function UseContainers({ apiBaseUrl, parentId }) {
|
|
|
548
756
|
setLoading(false);
|
|
549
757
|
}
|
|
550
758
|
};
|
|
551
|
-
|
|
759
|
+
useEffect2(() => {
|
|
552
760
|
void load();
|
|
553
761
|
}, [apiBaseUrl, parentId]);
|
|
554
762
|
return {
|
|
@@ -560,31 +768,39 @@ function UseContainers({ apiBaseUrl, parentId }) {
|
|
|
560
768
|
}
|
|
561
769
|
|
|
562
770
|
// src/components/ContainerUploadPanel.tsx
|
|
563
|
-
import { jsx as jsx5
|
|
771
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
564
772
|
var ContainerUploadPanel = ({
|
|
565
773
|
containerApiBaseUrl,
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
description = "Drop files to upload. Existing files are listed below."
|
|
774
|
+
storageApiBaseUrl,
|
|
775
|
+
parentContainerId
|
|
569
776
|
}) => {
|
|
570
|
-
const { containers,
|
|
777
|
+
const { containers, reload, loading } = UseContainers({
|
|
571
778
|
apiBaseUrl: containerApiBaseUrl,
|
|
572
779
|
parentId: parentContainerId
|
|
573
780
|
});
|
|
574
781
|
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
|
-
);
|
|
782
|
+
const sdkDb = new SparkStudioStorageSDK(containerApiBaseUrl);
|
|
783
|
+
const sdkS3 = new SparkStudioStorageSDK(storageApiBaseUrl);
|
|
581
784
|
const containerDTO = await sdkDb.container.CreateFileContainer(
|
|
582
785
|
file.name,
|
|
583
786
|
file.size,
|
|
584
787
|
encodeURIComponent(file.type)
|
|
585
788
|
);
|
|
586
|
-
|
|
587
|
-
|
|
789
|
+
async function getPresignedUrlWithRetry(container, attempts = 3) {
|
|
790
|
+
let lastError;
|
|
791
|
+
for (let i = 1; i <= attempts; i++) {
|
|
792
|
+
try {
|
|
793
|
+
return await sdkS3.s3.GetPreSignedUrl(container);
|
|
794
|
+
} catch (err) {
|
|
795
|
+
lastError = err;
|
|
796
|
+
if (i < attempts) {
|
|
797
|
+
await new Promise((r) => setTimeout(r, 500 * i));
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
throw lastError instanceof Error ? lastError : new Error("Failed to fetch presigned URL");
|
|
802
|
+
}
|
|
803
|
+
return getPresignedUrlWithRetry(containerDTO);
|
|
588
804
|
};
|
|
589
805
|
const handleUploadComplete = async (file, s3Url) => {
|
|
590
806
|
console.log("Upload complete:", file.name, s3Url);
|
|
@@ -593,10 +809,16 @@ var ContainerUploadPanel = ({
|
|
|
593
809
|
const handleUploadError = (file, err) => {
|
|
594
810
|
console.error("Upload failed:", file.name, err);
|
|
595
811
|
};
|
|
812
|
+
const handleOnDeleteFile = async (file) => {
|
|
813
|
+
const sdkDb = new SparkStudioStorageSDK(containerApiBaseUrl);
|
|
814
|
+
const sdkS3 = new SparkStudioStorageSDK(storageApiBaseUrl);
|
|
815
|
+
await sdkDb.container.DeleteContainer(file.Id);
|
|
816
|
+
await sdkS3.s3.DeleteS3(file);
|
|
817
|
+
await reload();
|
|
818
|
+
};
|
|
596
819
|
const handleExistingFileClick = (file) => {
|
|
597
|
-
const downloadUrl = `${containerApiBaseUrl}/api/Container/Download/${file.Id}`;
|
|
598
820
|
const a = document.createElement("a");
|
|
599
|
-
a.href =
|
|
821
|
+
a.href = file.PublicUrl ?? "";
|
|
600
822
|
a.download = file.Name ?? "";
|
|
601
823
|
a.target = "_blank";
|
|
602
824
|
a.rel = "noopener noreferrer";
|
|
@@ -604,26 +826,19 @@ var ContainerUploadPanel = ({
|
|
|
604
826
|
a.click();
|
|
605
827
|
document.body.removeChild(a);
|
|
606
828
|
};
|
|
607
|
-
return /* @__PURE__ */
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
autoUpload: true,
|
|
621
|
-
getPresignedUrl,
|
|
622
|
-
onUploadComplete: handleUploadComplete,
|
|
623
|
-
onUploadError: handleUploadError
|
|
624
|
-
}
|
|
625
|
-
)
|
|
626
|
-
] });
|
|
829
|
+
return /* @__PURE__ */ jsx5(
|
|
830
|
+
UploadContainer,
|
|
831
|
+
{
|
|
832
|
+
existingFiles: containers,
|
|
833
|
+
existingFilesLoading: loading,
|
|
834
|
+
onExistingFileClick: handleExistingFileClick,
|
|
835
|
+
autoUpload: true,
|
|
836
|
+
getPresignedUrl,
|
|
837
|
+
onUploadComplete: handleUploadComplete,
|
|
838
|
+
onUploadError: handleUploadError,
|
|
839
|
+
onDeleteFile: handleOnDeleteFile
|
|
840
|
+
}
|
|
841
|
+
);
|
|
627
842
|
};
|
|
628
843
|
|
|
629
844
|
// src/views/HomeView.tsx
|
|
@@ -633,7 +848,7 @@ import {
|
|
|
633
848
|
UserInfoCard,
|
|
634
849
|
useUser
|
|
635
850
|
} from "@sparkstudio/authentication-ui";
|
|
636
|
-
import { Fragment as Fragment2, jsx as jsx6, jsxs as
|
|
851
|
+
import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
637
852
|
function HomeView() {
|
|
638
853
|
return /* @__PURE__ */ jsx6(
|
|
639
854
|
AuthenticatorProvider,
|
|
@@ -647,13 +862,13 @@ function HomeView() {
|
|
|
647
862
|
}
|
|
648
863
|
function HomeContent() {
|
|
649
864
|
const { user } = useUser();
|
|
650
|
-
return /* @__PURE__ */
|
|
865
|
+
return /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
651
866
|
/* @__PURE__ */ jsx6(UserInfoCard, {}),
|
|
652
867
|
user && /* @__PURE__ */ jsx6(
|
|
653
868
|
ContainerUploadPanel,
|
|
654
869
|
{
|
|
655
|
-
containerApiBaseUrl: "https://
|
|
656
|
-
storageApiBaseUrl: "https://
|
|
870
|
+
containerApiBaseUrl: "https://localhost:5001",
|
|
871
|
+
storageApiBaseUrl: "https://localhost:5001"
|
|
657
872
|
}
|
|
658
873
|
)
|
|
659
874
|
] });
|
|
@@ -664,13 +879,13 @@ export {
|
|
|
664
879
|
ContainerDTO,
|
|
665
880
|
ContainerType,
|
|
666
881
|
ContainerUploadPanel,
|
|
882
|
+
DesktopFileIcon,
|
|
667
883
|
Home,
|
|
668
884
|
HomeView,
|
|
669
885
|
S3,
|
|
670
886
|
SparkStudioStorageSDK,
|
|
671
887
|
UploadContainer,
|
|
672
888
|
UploadDropzone,
|
|
673
|
-
UploadFilePicker,
|
|
674
889
|
UploadFileToS3,
|
|
675
890
|
UploadProgressList,
|
|
676
891
|
UseContainers,
|