@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.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 useState2 } from "react";
215
+ import { useState as useState3 } from "react";
214
216
 
215
217
  // src/components/UploadDropzone.tsx
216
218
  import "react";
217
- import { jsx, jsxs } from "react/jsx-runtime";
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
- return /* @__PURE__ */ jsxs(
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: "border rounded-3 p-4 text-center mb-3 d-flex flex-column align-items-center justify-content-center " + (isDragging ? "bg-light border-primary" : "border-secondary border-dashed"),
228
- style: { cursor: "pointer", minHeight: "140px" },
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 getPresignedUrl(
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 { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
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] = useState2(false);
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 handleExistingFileClickInternal = (file) => {
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__ */ jsxs4(Fragment, { children: [
487
- /* @__PURE__ */ jsx4("h5", { className: "card-title mb-2", children: title }),
488
- /* @__PURE__ */ jsx4("p", { className: "card-text text-muted", children: description }),
489
- existingFiles.length > 0 && /* @__PURE__ */ jsxs4("div", { className: "mb-3", children: [
490
- /* @__PURE__ */ jsx4("h6", { className: "mb-2", children: "Existing files" }),
491
- /* @__PURE__ */ jsx4("ul", { className: "list-group", children: existingFiles.map((file) => /* @__PURE__ */ jsxs4(
492
- "li",
493
- {
494
- className: "list-group-item d-flex justify-content-between align-items-center",
495
- style: { cursor: "pointer" },
496
- onClick: () => handleExistingFileClickInternal(file),
497
- children: [
498
- /* @__PURE__ */ jsx4("span", { children: file.Name }),
499
- typeof file.FileSize === "number" && /* @__PURE__ */ jsxs4("small", { className: "text-muted", children: [
500
- (file.FileSize / 1024).toFixed(1),
501
- " KB"
502
- ] })
503
- ]
504
- },
505
- file.Id
506
- )) })
507
- ] }),
508
- /* @__PURE__ */ jsx4(
509
- UploadDropzone,
510
- {
511
- isDragging,
512
- onDragOver: handleDragOver,
513
- onDragLeave: handleDragLeave,
514
- onDrop: handleDrop
515
- }
516
- ),
517
- /* @__PURE__ */ jsx4(
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 useState3 } from "react";
739
+ import { useEffect as useEffect2, useState as useState4 } from "react";
532
740
  function UseContainers({ apiBaseUrl, parentId }) {
533
- const [containers, setContainers] = useState3([]);
534
- const [loading, setLoading] = useState3(false);
535
- const [error, setError] = useState3(null);
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
- "https://lf9zyufpuk.execute-api.us-east-2.amazonaws.com/Prod"
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
- useEffect(() => {
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, jsxs as jsxs5 } from "react/jsx-runtime";
771
+ import { jsx as jsx5 } from "react/jsx-runtime";
564
772
  var ContainerUploadPanel = ({
565
773
  containerApiBaseUrl,
566
- parentContainerId,
567
- title = "Upload files",
568
- description = "Drop files to upload. Existing files are listed below."
774
+ storageApiBaseUrl,
775
+ parentContainerId
569
776
  }) => {
570
- const { containers, loading, error, reload } = UseContainers({
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
- "https://lf9zyufpuk.execute-api.us-east-2.amazonaws.com/Prod"
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
- const resultS3 = await sdkS3.s3.GetPreSignedUrl(containerDTO);
587
- return resultS3;
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 = downloadUrl;
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__ */ jsxs5("div", { children: [
608
- loading && /* @__PURE__ */ jsx5("p", { children: "Loading existing files\u2026" }),
609
- error && /* @__PURE__ */ jsxs5("p", { className: "text-danger", children: [
610
- "Failed to load containers: ",
611
- error.message
612
- ] }),
613
- /* @__PURE__ */ jsx5(
614
- UploadContainer,
615
- {
616
- title,
617
- description,
618
- existingFiles: containers,
619
- onExistingFileClick: handleExistingFileClick,
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 jsxs6 } from "react/jsx-runtime";
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__ */ jsxs6(Fragment2, { children: [
865
+ return /* @__PURE__ */ jsxs4(Fragment2, { children: [
651
866
  /* @__PURE__ */ jsx6(UserInfoCard, {}),
652
867
  user && /* @__PURE__ */ jsx6(
653
868
  ContainerUploadPanel,
654
869
  {
655
- containerApiBaseUrl: "https://lf9zyufpuk.execute-api.us-east-2.amazonaws.com/Prod",
656
- storageApiBaseUrl: "https://iq0gmcn0pd.execute-api.us-east-2.amazonaws.com/Prod"
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,