@sparkstudio/storage-ui 1.0.28 → 1.0.30

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
@@ -233,51 +233,8 @@ var ContainerType = /* @__PURE__ */ ((ContainerType2) => {
233
233
  return ContainerType2;
234
234
  })(ContainerType || {});
235
235
 
236
- // src/components/ContainerUploadPanel.tsx
237
- import "react";
238
-
239
- // src/components/UploadContainer.tsx
240
- import { useState as useState3 } from "react";
241
-
242
- // src/components/UploadDropzone.tsx
243
- import "react";
244
- import { jsx } from "react/jsx-runtime";
245
- var UploadDropzone = ({
246
- isDragging,
247
- onDragOver,
248
- onDragLeave,
249
- onDrop,
250
- className = "",
251
- style,
252
- children
253
- }) => {
254
- const baseClass = "rounded-3 d-flex flex-column align-items-center justify-content-center";
255
- const stateClass = isDragging ? "bg-body-secondary border-dashed border-2 border-secondary" : "bg-body-trasparent border-solid border-transparent border-2";
256
- const combinedClassName = `${baseClass} ${stateClass} ${className}`.trim();
257
- const handleDragOver = (e) => {
258
- e.preventDefault();
259
- onDragOver?.(e);
260
- };
261
- const handleDragLeave = (e) => {
262
- e.preventDefault();
263
- onDragLeave?.(e);
264
- };
265
- const handleDrop = (e) => {
266
- e.preventDefault();
267
- onDrop?.(e);
268
- };
269
- return /* @__PURE__ */ jsx(
270
- "div",
271
- {
272
- className: combinedClassName,
273
- style: { minHeight: "140px", ...style },
274
- onDragOver: handleDragOver,
275
- onDragLeave: handleDragLeave,
276
- onDrop: handleDrop,
277
- children
278
- }
279
- );
280
- };
236
+ // src/components/ContainerIdGridPanel.tsx
237
+ import { useEffect, useMemo as useMemo2, useRef, useState as useState3 } from "react";
281
238
 
282
239
  // src/hooks/UseUploadManager.ts
283
240
  import { useState, useCallback } from "react";
@@ -431,13 +388,13 @@ function UseUploadManager({
431
388
 
432
389
  // src/components/UploadProgressList.tsx
433
390
  import "react";
434
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
391
+ import { jsx, jsxs } from "react/jsx-runtime";
435
392
  var UploadProgressList = ({
436
393
  uploads
437
394
  }) => {
438
395
  const visibleUploads = uploads.filter((u) => u.status !== "success");
439
396
  if (visibleUploads.length === 0) return null;
440
- return /* @__PURE__ */ jsx2(
397
+ return /* @__PURE__ */ jsx(
441
398
  "div",
442
399
  {
443
400
  className: "w-100 d-flex flex-wrap gap-4 align-content-start mt-3",
@@ -461,20 +418,20 @@ var UploadProgressList = ({
461
418
  height: 64
462
419
  },
463
420
  children: [
464
- /* @__PURE__ */ jsx2("i", { className: "bi bi-file-earmark fs-2" }),
465
- u.status === "uploading" && /* @__PURE__ */ jsx2(
421
+ /* @__PURE__ */ jsx("i", { className: "bi bi-file-earmark fs-2" }),
422
+ u.status === "uploading" && /* @__PURE__ */ jsx(
466
423
  "div",
467
424
  {
468
425
  className: "position-absolute top-50 start-50 translate-middle",
469
426
  style: { pointerEvents: "none" },
470
- children: /* @__PURE__ */ jsx2("div", { className: "spinner-border spinner-border-sm text-primary" })
427
+ children: /* @__PURE__ */ jsx("div", { className: "spinner-border spinner-border-sm text-primary" })
471
428
  }
472
429
  ),
473
- u.status === "error" && /* @__PURE__ */ jsx2("span", { className: "position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger", children: "!" })
430
+ u.status === "error" && /* @__PURE__ */ jsx("span", { className: "position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger", children: "!" })
474
431
  ]
475
432
  }
476
433
  ),
477
- /* @__PURE__ */ jsx2(
434
+ /* @__PURE__ */ jsx(
478
435
  "div",
479
436
  {
480
437
  className: "small text-center text-truncate",
@@ -482,7 +439,7 @@ var UploadProgressList = ({
482
439
  children: u.file.name
483
440
  }
484
441
  ),
485
- /* @__PURE__ */ jsx2("div", { className: "w-100 mt-1", children: /* @__PURE__ */ jsx2("div", { className: "progress", style: { height: "4px" }, children: /* @__PURE__ */ jsx2(
442
+ /* @__PURE__ */ jsx("div", { className: "w-100 mt-1", children: /* @__PURE__ */ jsx("div", { className: "progress", style: { height: "4px" }, children: /* @__PURE__ */ jsx(
486
443
  "div",
487
444
  {
488
445
  className: "progress-bar " + (u.status === "error" ? "bg-danger" : ""),
@@ -493,7 +450,7 @@ var UploadProgressList = ({
493
450
  "aria-valuemax": 100
494
451
  }
495
452
  ) }) }),
496
- u.status === "error" && /* @__PURE__ */ jsx2(
453
+ u.status === "error" && /* @__PURE__ */ jsx(
497
454
  "div",
498
455
  {
499
456
  className: "text-danger small mt-1 text-center",
@@ -509,6 +466,559 @@ var UploadProgressList = ({
509
466
  );
510
467
  };
511
468
 
469
+ // src/components/FileIconGrid.tsx
470
+ import "react";
471
+
472
+ // src/components/FileIconCard.tsx
473
+ import { useMemo, useState as useState2 } from "react";
474
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
475
+ function sanitizeIconHtml(input) {
476
+ return input.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "").replace(/\son\w+="[^"]*"/gi, "").replace(/\son\w+='[^']*'/gi, "").replace(/javascript:/gi, "");
477
+ }
478
+ var FileIconCard = (props) => {
479
+ const {
480
+ file,
481
+ deleteDisabled,
482
+ selected,
483
+ onSelect,
484
+ onDeleteFile,
485
+ onClickFile,
486
+ icon,
487
+ iconHtml,
488
+ title,
489
+ className
490
+ } = props;
491
+ const [deleting, setDeleting] = useState2(false);
492
+ const [error, setError] = useState2(null);
493
+ const safeIconHtml = useMemo(() => {
494
+ if (!iconHtml) return null;
495
+ return sanitizeIconHtml(iconHtml);
496
+ }, [iconHtml]);
497
+ const canDelete = !!onDeleteFile && !deleteDisabled && !deleting;
498
+ const handleClick = () => {
499
+ onSelect?.(file);
500
+ onClickFile?.(file);
501
+ };
502
+ const handleDelete = async (ev) => {
503
+ ev.stopPropagation();
504
+ if (!canDelete) return;
505
+ setError(null);
506
+ setDeleting(true);
507
+ try {
508
+ await onDeleteFile?.(file);
509
+ } catch (e) {
510
+ setError(e instanceof Error ? e.message : "Delete failed");
511
+ } finally {
512
+ setDeleting(false);
513
+ }
514
+ };
515
+ const borderColor = selected ? "rgba(13,110,253,0.8)" : "rgba(0,0,0,0.12)";
516
+ const boxShadow = selected ? "0 0 0 3px rgba(13,110,253,0.2)" : "none";
517
+ return /* @__PURE__ */ jsxs2(
518
+ "div",
519
+ {
520
+ className: [
521
+ "file-icon-card",
522
+ deleteDisabled ? "is-delete-disabled" : "",
523
+ deleting ? "is-deleting" : "",
524
+ className ?? ""
525
+ ].join(" "),
526
+ role: "button",
527
+ tabIndex: 0,
528
+ onClick: handleClick,
529
+ onKeyDown: (e) => {
530
+ if (e.key === "Enter" || e.key === " ") {
531
+ e.preventDefault();
532
+ handleClick();
533
+ }
534
+ },
535
+ title: title ?? file.Name ?? "File",
536
+ style: {
537
+ display: "flex",
538
+ alignItems: "center",
539
+ gap: 12,
540
+ padding: 12,
541
+ border: `1px solid ${borderColor}`,
542
+ boxShadow,
543
+ borderRadius: 12,
544
+ userSelect: "none",
545
+ cursor: "pointer",
546
+ background: "white"
547
+ },
548
+ children: [
549
+ /* @__PURE__ */ jsx2(
550
+ "div",
551
+ {
552
+ style: {
553
+ width: 44,
554
+ height: 44,
555
+ borderRadius: 10,
556
+ display: "grid",
557
+ placeItems: "center",
558
+ background: "rgba(0,0,0,0.04)",
559
+ flex: "0 0 auto"
560
+ },
561
+ children: icon ? icon : safeIconHtml ? /* @__PURE__ */ jsx2("span", { "aria-hidden": true, dangerouslySetInnerHTML: { __html: safeIconHtml } }) : /* @__PURE__ */ jsx2(DefaultFileIcon, {})
562
+ }
563
+ ),
564
+ /* @__PURE__ */ jsxs2("div", { style: { minWidth: 0, flex: "1 1 auto" }, children: [
565
+ /* @__PURE__ */ jsx2(
566
+ "div",
567
+ {
568
+ style: {
569
+ fontWeight: 600,
570
+ overflow: "hidden",
571
+ textOverflow: "ellipsis",
572
+ whiteSpace: "nowrap"
573
+ },
574
+ children: title ?? file.Name ?? "Untitled"
575
+ }
576
+ ),
577
+ /* @__PURE__ */ jsx2("div", { style: { fontSize: 12, opacity: 0.7 }, children: typeof file.FileSize === "number" ? formatBytes(file.FileSize) : "" }),
578
+ error ? /* @__PURE__ */ jsx2("div", { style: { fontSize: 12, color: "crimson", marginTop: 4 }, children: error }) : null
579
+ ] }),
580
+ !deleteDisabled ? /* @__PURE__ */ jsxs2(
581
+ "button",
582
+ {
583
+ type: "button",
584
+ onClick: handleDelete,
585
+ disabled: !canDelete,
586
+ "aria-label": "Delete file",
587
+ style: {
588
+ border: "1px solid rgba(0,0,0,0.18)",
589
+ background: canDelete ? "white" : "rgba(0,0,0,0.04)",
590
+ borderRadius: 10,
591
+ padding: "8px 10px",
592
+ cursor: canDelete ? "pointer" : "not-allowed",
593
+ display: "flex",
594
+ alignItems: "center",
595
+ gap: 8
596
+ },
597
+ children: [
598
+ deleting ? /* @__PURE__ */ jsx2(SmallSpinner, {}) : /* @__PURE__ */ jsx2(TrashIcon, {}),
599
+ /* @__PURE__ */ jsx2("span", { style: { fontSize: 13 }, children: deleting ? "Deleting\u2026" : "Delete" })
600
+ ]
601
+ }
602
+ ) : null
603
+ ]
604
+ }
605
+ );
606
+ };
607
+ function formatBytes(bytes) {
608
+ if (!Number.isFinite(bytes) || bytes < 0) return "";
609
+ const units = ["B", "KB", "MB", "GB", "TB"];
610
+ let v = bytes;
611
+ let i = 0;
612
+ while (v >= 1024 && i < units.length - 1) {
613
+ v /= 1024;
614
+ i++;
615
+ }
616
+ return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
617
+ }
618
+ function SmallSpinner() {
619
+ return /* @__PURE__ */ jsx2(
620
+ "span",
621
+ {
622
+ "aria-hidden": true,
623
+ style: {
624
+ width: 14,
625
+ height: 14,
626
+ borderRadius: "50%",
627
+ border: "2px solid rgba(0,0,0,0.2)",
628
+ borderTopColor: "rgba(0,0,0,0.7)",
629
+ display: "inline-block",
630
+ animation: "fileIconSpin 0.8s linear infinite"
631
+ }
632
+ }
633
+ );
634
+ }
635
+ function DefaultFileIcon() {
636
+ return /* @__PURE__ */ jsxs2("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", "aria-hidden": true, children: [
637
+ /* @__PURE__ */ jsx2(
638
+ "path",
639
+ {
640
+ d: "M7 3h7l3 3v15a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z",
641
+ stroke: "currentColor",
642
+ strokeWidth: "2"
643
+ }
644
+ ),
645
+ /* @__PURE__ */ jsx2("path", { d: "M14 3v4a1 1 0 0 0 1 1h4", stroke: "currentColor", strokeWidth: "2" })
646
+ ] });
647
+ }
648
+ function TrashIcon() {
649
+ return /* @__PURE__ */ jsxs2("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", "aria-hidden": true, children: [
650
+ /* @__PURE__ */ jsx2("path", { d: "M3 6h18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }),
651
+ /* @__PURE__ */ jsx2(
652
+ "path",
653
+ {
654
+ d: "M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2",
655
+ stroke: "currentColor",
656
+ strokeWidth: "2"
657
+ }
658
+ ),
659
+ /* @__PURE__ */ jsx2(
660
+ "path",
661
+ {
662
+ d: "M6 6l1 16a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-16",
663
+ stroke: "currentColor",
664
+ strokeWidth: "2"
665
+ }
666
+ ),
667
+ /* @__PURE__ */ jsx2(
668
+ "path",
669
+ {
670
+ d: "M10 11v7M14 11v7",
671
+ stroke: "currentColor",
672
+ strokeWidth: "2",
673
+ strokeLinecap: "round"
674
+ }
675
+ )
676
+ ] });
677
+ }
678
+
679
+ // src/components/FileIconGrid.tsx
680
+ import { jsx as jsx3 } from "react/jsx-runtime";
681
+ function FileIconGrid(props) {
682
+ const {
683
+ files,
684
+ deleteDisabled,
685
+ onDeleted,
686
+ selectedId,
687
+ onSelect,
688
+ icon,
689
+ iconHtml,
690
+ className
691
+ } = props;
692
+ return /* @__PURE__ */ jsx3(
693
+ "div",
694
+ {
695
+ className,
696
+ style: {
697
+ display: "grid",
698
+ gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))",
699
+ gap: 12
700
+ },
701
+ children: files.map((f) => /* @__PURE__ */ jsx3(
702
+ FileIconCard,
703
+ {
704
+ file: f,
705
+ deleteDisabled,
706
+ onDeleteFile: onDeleted,
707
+ onSelect,
708
+ selected: selectedId === f.Id,
709
+ icon,
710
+ iconHtml
711
+ },
712
+ f.Id
713
+ ))
714
+ }
715
+ );
716
+ }
717
+
718
+ // src/components/ContainerIdGridPanel.tsx
719
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
720
+ function ContainerIdGridPanel(props) {
721
+ const {
722
+ containerApiBaseUrl,
723
+ storageApiBaseUrl,
724
+ containerIds,
725
+ onContainerIdsChange,
726
+ accept = "*/*",
727
+ multiple = true,
728
+ selectedId,
729
+ onSelect,
730
+ icon,
731
+ iconHtml,
732
+ deleteDisabled,
733
+ className,
734
+ onDeleted
735
+ } = props;
736
+ const sdkDb = useMemo2(
737
+ () => new SparkStudioStorageSDK(containerApiBaseUrl),
738
+ [containerApiBaseUrl]
739
+ );
740
+ const sdkS3 = useMemo2(
741
+ () => new SparkStudioStorageSDK(storageApiBaseUrl),
742
+ [storageApiBaseUrl]
743
+ );
744
+ const [loading, setLoading] = useState3(false);
745
+ const [files, setFiles] = useState3([]);
746
+ const [dragOver, setDragOver] = useState3(false);
747
+ const [errMsg, setErrMsg] = useState3(null);
748
+ const idsRef = useRef(containerIds);
749
+ useEffect(() => {
750
+ idsRef.current = containerIds;
751
+ }, [containerIds]);
752
+ const createdByFileRef = useRef(/* @__PURE__ */ new WeakMap());
753
+ useEffect(() => {
754
+ let cancelled = false;
755
+ async function loadByIds(ids) {
756
+ setErrMsg(null);
757
+ if (!ids || ids.length === 0) {
758
+ setFiles([]);
759
+ return;
760
+ }
761
+ setLoading(true);
762
+ try {
763
+ const results = await Promise.all(
764
+ ids.map(async (id) => {
765
+ try {
766
+ const dto = await sdkDb.container.Read?.(id);
767
+ return dto ?? null;
768
+ } catch {
769
+ return null;
770
+ }
771
+ })
772
+ );
773
+ if (cancelled) return;
774
+ const map = /* @__PURE__ */ new Map();
775
+ for (const r of results) if (r?.Id) map.set(r.Id, r);
776
+ setFiles(
777
+ ids.map((id) => map.get(id)).filter(Boolean)
778
+ );
779
+ } catch (e) {
780
+ if (!cancelled)
781
+ setErrMsg(e instanceof Error ? e.message : "Failed to load files");
782
+ } finally {
783
+ if (!cancelled) setLoading(false);
784
+ }
785
+ }
786
+ void loadByIds(containerIds);
787
+ return () => {
788
+ cancelled = true;
789
+ };
790
+ }, [containerIds, sdkDb]);
791
+ const getPresignedUrl = async (file) => {
792
+ const contentType = file.type || "application/octet-stream";
793
+ const containerDTO = await sdkDb.container.CreateFileContainer(
794
+ file.name,
795
+ file.size,
796
+ encodeURIComponent(contentType)
797
+ );
798
+ createdByFileRef.current.set(file, containerDTO);
799
+ let lastError;
800
+ for (let i = 1; i <= 3; i++) {
801
+ try {
802
+ return await sdkS3.s3.GetPreSignedUrl(containerDTO);
803
+ } catch (e) {
804
+ lastError = e;
805
+ if (i < 3) await new Promise((r) => setTimeout(r, 500 * i));
806
+ }
807
+ }
808
+ throw lastError instanceof Error ? lastError : new Error("Failed to fetch presigned URL");
809
+ };
810
+ const { uploads, startUploadsIfNeeded } = UseUploadManager({
811
+ autoUpload: true,
812
+ getPresignedUrl,
813
+ onUploadComplete: async (file) => {
814
+ setErrMsg(null);
815
+ const created = createdByFileRef.current.get(file);
816
+ if (created?.Id) {
817
+ const prev = idsRef.current ?? [];
818
+ const next = prev.includes(created.Id) ? prev : [...prev, created.Id];
819
+ onContainerIdsChange(next);
820
+ }
821
+ if (created?.Id) {
822
+ try {
823
+ const refreshed = await sdkDb.container.Read?.(created.Id);
824
+ if (refreshed) {
825
+ setFiles((prevFiles) => {
826
+ const nextFiles = prevFiles.slice();
827
+ const idx = nextFiles.findIndex((x) => x.Id === created.Id);
828
+ if (idx >= 0) nextFiles[idx] = refreshed;
829
+ else nextFiles.push(refreshed);
830
+ return nextFiles;
831
+ });
832
+ }
833
+ } catch {
834
+ }
835
+ }
836
+ },
837
+ onUploadError: (_file, error) => {
838
+ setErrMsg(error?.message ?? "Upload failed");
839
+ }
840
+ });
841
+ const handleDelete = async (file) => {
842
+ if (deleteDisabled) return;
843
+ await sdkDb.container.DeleteContainer(file.Id);
844
+ await sdkS3.s3.DeleteS3(file);
845
+ const prev = idsRef.current ?? [];
846
+ onContainerIdsChange(prev.filter((id) => id !== file.Id));
847
+ setFiles((prevFiles) => prevFiles.filter((x) => x.Id !== file.Id));
848
+ onDeleted?.(file);
849
+ };
850
+ const openPicker = () => {
851
+ const input = document.createElement("input");
852
+ input.type = "file";
853
+ input.multiple = multiple;
854
+ input.accept = accept;
855
+ input.onchange = () => {
856
+ if (input.files) startUploadsIfNeeded(input.files);
857
+ };
858
+ input.click();
859
+ };
860
+ const onDrop = (ev) => {
861
+ ev.preventDefault();
862
+ ev.stopPropagation();
863
+ setDragOver(false);
864
+ const list = ev.dataTransfer.files;
865
+ if (!list || list.length === 0) return;
866
+ startUploadsIfNeeded(list);
867
+ };
868
+ return /* @__PURE__ */ jsxs3("div", { className, style: { display: "grid", gap: 12 }, children: [
869
+ /* @__PURE__ */ jsxs3("div", { children: [
870
+ /* @__PURE__ */ jsx4(UploadProgressList, { uploads }),
871
+ errMsg ? /* @__PURE__ */ jsx4("div", { style: { fontSize: 12, color: "crimson", marginTop: 6 }, children: errMsg }) : null
872
+ ] }),
873
+ /* @__PURE__ */ jsxs3(
874
+ "div",
875
+ {
876
+ onDragEnter: (e) => {
877
+ e.preventDefault();
878
+ e.stopPropagation();
879
+ setDragOver(true);
880
+ },
881
+ onDragOver: (e) => {
882
+ e.preventDefault();
883
+ e.stopPropagation();
884
+ setDragOver(true);
885
+ },
886
+ onDragLeave: (e) => {
887
+ e.preventDefault();
888
+ e.stopPropagation();
889
+ setDragOver(false);
890
+ },
891
+ onDrop,
892
+ style: {
893
+ position: "relative",
894
+ borderRadius: 14,
895
+ border: `2px dashed ${dragOver ? "rgba(13,110,253,0.9)" : "rgba(0,0,0,0.15)"}`,
896
+ background: dragOver ? "rgba(13,110,253,0.06)" : "transparent",
897
+ padding: 12
898
+ },
899
+ children: [
900
+ dragOver ? /* @__PURE__ */ jsx4(
901
+ "div",
902
+ {
903
+ style: {
904
+ position: "absolute",
905
+ inset: 0,
906
+ borderRadius: 14,
907
+ display: "grid",
908
+ placeItems: "center",
909
+ pointerEvents: "none",
910
+ background: "rgba(13,110,253,0.10)",
911
+ fontWeight: 800
912
+ },
913
+ children: "Drop files to upload"
914
+ }
915
+ ) : null,
916
+ /* @__PURE__ */ jsxs3(
917
+ "div",
918
+ {
919
+ style: {
920
+ display: "flex",
921
+ justifyContent: "space-between",
922
+ marginBottom: 10
923
+ },
924
+ children: [
925
+ /* @__PURE__ */ jsx4("div", { style: { fontWeight: 700 }, children: "Files" }),
926
+ /* @__PURE__ */ jsx4(
927
+ "button",
928
+ {
929
+ type: "button",
930
+ onClick: openPicker,
931
+ style: {
932
+ border: "1px solid rgba(0,0,0,0.18)",
933
+ background: "white",
934
+ borderRadius: 10,
935
+ padding: "8px 10px",
936
+ cursor: "pointer",
937
+ fontWeight: 600
938
+ },
939
+ children: "Browse\u2026"
940
+ }
941
+ )
942
+ ]
943
+ }
944
+ ),
945
+ loading ? /* @__PURE__ */ jsx4(
946
+ "div",
947
+ {
948
+ className: "d-flex justify-content-center align-items-center",
949
+ style: { minHeight: 120 },
950
+ children: /* @__PURE__ */ jsx4("div", { className: "spinner-border text-secondary", role: "status" })
951
+ }
952
+ ) : /* @__PURE__ */ jsx4(
953
+ FileIconGrid,
954
+ {
955
+ files,
956
+ deleteDisabled,
957
+ onDeleted: handleDelete,
958
+ selectedId,
959
+ onSelect,
960
+ icon,
961
+ iconHtml
962
+ }
963
+ )
964
+ ]
965
+ }
966
+ )
967
+ ] });
968
+ }
969
+
970
+ // src/components/ContainerUploadPanel.tsx
971
+ import "react";
972
+
973
+ // src/components/UploadContainer.tsx
974
+ import {
975
+ forwardRef,
976
+ useImperativeHandle,
977
+ useMemo as useMemo3,
978
+ useRef as useRef3,
979
+ useState as useState5
980
+ } from "react";
981
+
982
+ // src/components/UploadDropzone.tsx
983
+ import "react";
984
+ import { jsx as jsx5 } from "react/jsx-runtime";
985
+ var UploadDropzone = ({
986
+ isDragging,
987
+ onDragOver,
988
+ onDragLeave,
989
+ onDrop,
990
+ className = "",
991
+ style,
992
+ children
993
+ }) => {
994
+ const baseClass = "rounded-3 d-flex flex-column align-items-center justify-content-center";
995
+ const stateClass = isDragging ? "bg-body-secondary border-dashed border-2 border-secondary" : "bg-body-trasparent border-solid border-transparent border-2";
996
+ const combinedClassName = `${baseClass} ${stateClass} ${className}`.trim();
997
+ const handleDragOver = (e) => {
998
+ e.preventDefault();
999
+ onDragOver?.(e);
1000
+ };
1001
+ const handleDragLeave = (e) => {
1002
+ e.preventDefault();
1003
+ onDragLeave?.(e);
1004
+ };
1005
+ const handleDrop = (e) => {
1006
+ e.preventDefault();
1007
+ onDrop?.(e);
1008
+ };
1009
+ return /* @__PURE__ */ jsx5(
1010
+ "div",
1011
+ {
1012
+ className: combinedClassName,
1013
+ style: { minHeight: "140px", ...style },
1014
+ onDragOver: handleDragOver,
1015
+ onDragLeave: handleDragLeave,
1016
+ onDrop: handleDrop,
1017
+ children
1018
+ }
1019
+ );
1020
+ };
1021
+
512
1022
  // src/components/DesktopFileIcon.tsx
513
1023
  import {
514
1024
  faFile,
@@ -522,8 +1032,8 @@ import {
522
1032
  faFileCode
523
1033
  } from "@fortawesome/free-solid-svg-icons";
524
1034
  import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
525
- import { useEffect, useRef, useState as useState2 } from "react";
526
- import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1035
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState4 } from "react";
1036
+ import { Fragment, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
527
1037
  function getFileExtension(name) {
528
1038
  if (!name) return null;
529
1039
  const lastDot = name.lastIndexOf(".");
@@ -602,13 +1112,13 @@ var DesktopFileIcon = ({
602
1112
  onOpen,
603
1113
  onDelete
604
1114
  }) => {
605
- const [contextMenuPos, setContextMenuPos] = useState2(
1115
+ const [contextMenuPos, setContextMenuPos] = useState4(
606
1116
  null
607
1117
  );
608
- const [isHovered, setIsHovered] = useState2(false);
609
- const [isDeleting, setIsDeleting] = useState2(false);
610
- const iconRef = useRef(null);
611
- const menuRef = useRef(null);
1118
+ const [isHovered, setIsHovered] = useState4(false);
1119
+ const [isDeleting, setIsDeleting] = useState4(false);
1120
+ const iconRef = useRef2(null);
1121
+ const menuRef = useRef2(null);
612
1122
  const handleDoubleClick = () => {
613
1123
  if (isDeleting) return;
614
1124
  if (onOpen) {
@@ -666,7 +1176,7 @@ var DesktopFileIcon = ({
666
1176
  const formattedSize = typeof sizeBytes === "number" ? `${(sizeBytes / 1024).toFixed(1)} KB` : void 0;
667
1177
  const ext = getFileExtension(name);
668
1178
  const iconToRender = getIconForExtension(ext);
669
- useEffect(() => {
1179
+ useEffect2(() => {
670
1180
  if (!contextMenuPos) return;
671
1181
  const handleGlobalClick = (e) => {
672
1182
  const target = e.target;
@@ -683,8 +1193,8 @@ var DesktopFileIcon = ({
683
1193
  document.removeEventListener("mousedown", handleGlobalClick);
684
1194
  };
685
1195
  }, [contextMenuPos]);
686
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
687
- /* @__PURE__ */ jsxs2(
1196
+ return /* @__PURE__ */ jsxs4(Fragment, { children: [
1197
+ /* @__PURE__ */ jsxs4(
688
1198
  "div",
689
1199
  {
690
1200
  ref: iconRef,
@@ -705,7 +1215,7 @@ var DesktopFileIcon = ({
705
1215
  onMouseEnter: () => setIsHovered(true),
706
1216
  onMouseLeave: () => setIsHovered(false),
707
1217
  children: [
708
- /* @__PURE__ */ jsxs2(
1218
+ /* @__PURE__ */ jsxs4(
709
1219
  "div",
710
1220
  {
711
1221
  className: "border rounded-3 d-flex align-items-center justify-content-center mb-1 shadow-sm position-relative",
@@ -714,17 +1224,17 @@ var DesktopFileIcon = ({
714
1224
  height: 64
715
1225
  },
716
1226
  children: [
717
- /* @__PURE__ */ jsx3(FontAwesomeIcon, { icon: iconToRender, className: "fs-2" }),
718
- 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" }) })
1227
+ /* @__PURE__ */ jsx6(FontAwesomeIcon, { icon: iconToRender, className: "fs-2" }),
1228
+ isDeleting && /* @__PURE__ */ jsx6("div", { className: "position-absolute top-50 start-50 translate-middle", children: /* @__PURE__ */ jsx6("div", { className: "spinner-border spinner-border-sm text-danger" }) })
719
1229
  ]
720
1230
  }
721
1231
  ),
722
- /* @__PURE__ */ jsx3("div", { className: "small text-center text-truncate", style: { width: "100%" }, children: name }),
723
- formattedSize && /* @__PURE__ */ jsx3("small", { className: "text-muted mt-1", children: formattedSize })
1232
+ /* @__PURE__ */ jsx6("div", { className: "small text-center text-truncate", style: { width: "100%" }, children: name }),
1233
+ formattedSize && /* @__PURE__ */ jsx6("small", { className: "text-muted mt-1", children: formattedSize })
724
1234
  ]
725
1235
  }
726
1236
  ),
727
- contextMenuPos && !isDeleting && /* @__PURE__ */ jsxs2(
1237
+ contextMenuPos && !isDeleting && /* @__PURE__ */ jsxs4(
728
1238
  "div",
729
1239
  {
730
1240
  ref: menuRef,
@@ -737,7 +1247,7 @@ var DesktopFileIcon = ({
737
1247
  },
738
1248
  onClick: (e) => e.stopPropagation(),
739
1249
  children: [
740
- /* @__PURE__ */ jsx3(
1250
+ /* @__PURE__ */ jsx6(
741
1251
  "button",
742
1252
  {
743
1253
  type: "button",
@@ -747,7 +1257,7 @@ var DesktopFileIcon = ({
747
1257
  children: "Download file"
748
1258
  }
749
1259
  ),
750
- /* @__PURE__ */ jsx3(
1260
+ /* @__PURE__ */ jsx6(
751
1261
  "button",
752
1262
  {
753
1263
  type: "button",
@@ -757,8 +1267,8 @@ var DesktopFileIcon = ({
757
1267
  children: "Copy download URL"
758
1268
  }
759
1269
  ),
760
- /* @__PURE__ */ jsx3("div", { className: "dropdown-divider" }),
761
- /* @__PURE__ */ jsx3(
1270
+ /* @__PURE__ */ jsx6("div", { className: "dropdown-divider" }),
1271
+ /* @__PURE__ */ jsx6(
762
1272
  "button",
763
1273
  {
764
1274
  type: "button",
@@ -774,160 +1284,165 @@ var DesktopFileIcon = ({
774
1284
  };
775
1285
 
776
1286
  // src/components/UploadContainer.tsx
777
- import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
778
- var UploadContainer = ({
779
- onFilesSelected,
780
- existingFiles = [],
781
- existingFilesLoading = false,
782
- onExistingFileClick,
783
- onDeleteFile,
784
- autoUpload = false,
785
- getPresignedUrl,
786
- onUploadComplete,
787
- onUploadError
788
- }) => {
789
- const [isDragging, setIsDragging] = useState3(false);
790
- const { uploads, startUploadsIfNeeded } = UseUploadManager({
791
- autoUpload,
1287
+ import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1288
+ function filesToFileList(files) {
1289
+ const dt = new DataTransfer();
1290
+ for (const f of files) dt.items.add(f);
1291
+ return dt.files;
1292
+ }
1293
+ var UploadContainer = forwardRef(
1294
+ ({
1295
+ multiple = true,
1296
+ accept = "*/*",
1297
+ onFilesSelected,
1298
+ existingFiles = [],
1299
+ existingFilesLoading = false,
1300
+ onExistingFileClick,
1301
+ onDeleteFile,
1302
+ autoUpload = false,
792
1303
  getPresignedUrl,
793
1304
  onUploadComplete,
794
1305
  onUploadError
795
- });
796
- const handleDragOver = (e) => {
797
- e.preventDefault();
798
- setIsDragging(true);
799
- };
800
- const handleDragLeave = (e) => {
801
- e.preventDefault();
802
- setIsDragging(false);
803
- };
804
- const handleDrop = (e) => {
805
- e.preventDefault();
806
- setIsDragging(false);
807
- const files = e.dataTransfer.files;
808
- if (!files || files.length === 0) return;
809
- onFilesSelected?.(files);
810
- startUploadsIfNeeded(files);
811
- };
812
- const handleExistingFileOpen = (file) => {
813
- if (onExistingFileClick) {
814
- onExistingFileClick(file);
815
- return;
816
- }
817
- const a = document.createElement("a");
818
- a.href = file.PublicUrl ?? "";
819
- a.download = file.Name ?? "";
820
- a.target = "_blank";
821
- a.rel = "noopener noreferrer";
822
- document.body.appendChild(a);
823
- a.click();
824
- document.body.removeChild(a);
825
- };
826
- return /* @__PURE__ */ jsxs3(Fragment2, { children: [
827
- /* @__PURE__ */ jsxs3("div", { className: "w-100", children: [
828
- /* @__PURE__ */ jsx4(
829
- "input",
830
- {
831
- id: "filePicker",
832
- type: "file",
833
- multiple: true,
834
- className: "d-none",
835
- onChange: (e) => {
836
- if (!e.target.files) return;
837
- onFilesSelected?.(e.target.files);
838
- startUploadsIfNeeded(e.target.files);
1306
+ }, ref) => {
1307
+ const [isDragging, setIsDragging] = useState5(false);
1308
+ const inputId = useMemo3(() => `filePicker_${crypto.randomUUID()}`, []);
1309
+ const inputRef = useRef3(null);
1310
+ const { uploads, startUploadsIfNeeded } = UseUploadManager({
1311
+ autoUpload,
1312
+ getPresignedUrl,
1313
+ onUploadComplete,
1314
+ onUploadError
1315
+ });
1316
+ const selectAndUpload = (files) => {
1317
+ if (!files || files.length === 0) return;
1318
+ onFilesSelected?.(files);
1319
+ startUploadsIfNeeded(files);
1320
+ };
1321
+ useImperativeHandle(ref, () => ({
1322
+ enqueueFiles(files) {
1323
+ const list = Array.isArray(files) ? filesToFileList(files) : files;
1324
+ selectAndUpload(list);
1325
+ },
1326
+ openFilePicker() {
1327
+ inputRef.current?.click();
1328
+ }
1329
+ }));
1330
+ const handleDragOver = (e) => {
1331
+ e.preventDefault();
1332
+ setIsDragging(true);
1333
+ };
1334
+ const handleDragLeave = (e) => {
1335
+ e.preventDefault();
1336
+ setIsDragging(false);
1337
+ };
1338
+ const handleDrop = (e) => {
1339
+ e.preventDefault();
1340
+ setIsDragging(false);
1341
+ const files = e.dataTransfer.files;
1342
+ if (!files || files.length === 0) return;
1343
+ selectAndUpload(files);
1344
+ };
1345
+ const handleExistingFileOpen = (file) => {
1346
+ if (onExistingFileClick) {
1347
+ onExistingFileClick(file);
1348
+ return;
1349
+ }
1350
+ const a = document.createElement("a");
1351
+ a.href = file.PublicUrl ?? "";
1352
+ a.download = file.Name ?? "";
1353
+ a.target = "_blank";
1354
+ a.rel = "noopener noreferrer";
1355
+ document.body.appendChild(a);
1356
+ a.click();
1357
+ document.body.removeChild(a);
1358
+ };
1359
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
1360
+ /* @__PURE__ */ jsxs5("div", { className: "w-100", children: [
1361
+ /* @__PURE__ */ jsx7(
1362
+ "input",
1363
+ {
1364
+ ref: inputRef,
1365
+ id: inputId,
1366
+ type: "file",
1367
+ multiple,
1368
+ accept,
1369
+ className: "d-none",
1370
+ onChange: (e) => {
1371
+ if (!e.target.files) return;
1372
+ selectAndUpload(e.target.files);
1373
+ e.currentTarget.value = "";
1374
+ }
839
1375
  }
840
- }
841
- ),
842
- /* @__PURE__ */ jsx4("div", { className: "text-start", children: /* @__PURE__ */ jsx4(
843
- "button",
1376
+ ),
1377
+ /* @__PURE__ */ jsx7("div", { className: "text-start", children: /* @__PURE__ */ jsx7(
1378
+ "button",
1379
+ {
1380
+ type: "button",
1381
+ className: "btn btn-primary float-start",
1382
+ onClick: () => inputRef.current?.click(),
1383
+ children: "Browse files\u2026"
1384
+ }
1385
+ ) })
1386
+ ] }),
1387
+ /* @__PURE__ */ jsxs5(
1388
+ UploadDropzone,
844
1389
  {
845
- type: "button",
846
- className: "btn btn-primary float-start",
847
- onClick: () => document.getElementById("filePicker")?.click(),
848
- children: "Browse files\u2026"
849
- }
850
- ) })
851
- ] }),
852
- /* @__PURE__ */ jsxs3(
853
- UploadDropzone,
854
- {
855
- isDragging,
856
- onDragOver: handleDragOver,
857
- onDragLeave: handleDragLeave,
858
- onDrop: handleDrop,
859
- className: "w-100",
860
- style: { minHeight: "100px", alignItems: "stretch" },
861
- children: [
862
- /* @__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 }) }) }) }),
863
- /* @__PURE__ */ jsx4(
864
- "div",
865
- {
866
- className: "w-100 d-flex flex-wrap gap-4 align-content-start",
867
- style: { minHeight: "140px" },
868
- children: existingFilesLoading ? /* @__PURE__ */ jsx4("div", { className: "w-100 d-flex justify-content-center align-items-center", children: /* @__PURE__ */ jsx4(
869
- "div",
870
- {
871
- className: "spinner-border text-secondary",
872
- role: "status"
873
- }
874
- ) }) : existingFiles.length === 0 ? /* @__PURE__ */ jsxs3(
875
- "div",
876
- {
877
- className: "w-100 d-flex flex-column align-items-center justify-content-center text-muted",
878
- style: {
879
- minHeight: "160px",
880
- padding: "20px",
881
- cursor: "pointer",
882
- transition: "background 0.12s, border-color 0.12s"
1390
+ isDragging,
1391
+ onDragOver: handleDragOver,
1392
+ onDragLeave: handleDragLeave,
1393
+ onDrop: handleDrop,
1394
+ className: "w-100",
1395
+ style: { minHeight: "100px", alignItems: "stretch" },
1396
+ children: [
1397
+ /* @__PURE__ */ jsx7("div", { className: "w-100 mb-3", children: /* @__PURE__ */ jsx7("div", { className: "d-flex flex-column flex-md-row align-items-start align-items-md-center justify-content-between gap-3", children: /* @__PURE__ */ jsx7("div", { className: "flex-grow-1 w-100", children: /* @__PURE__ */ jsx7(UploadProgressList, { uploads }) }) }) }),
1398
+ /* @__PURE__ */ jsx7(
1399
+ "div",
1400
+ {
1401
+ className: "w-100 d-flex flex-wrap gap-4 align-content-start",
1402
+ style: { minHeight: "140px" },
1403
+ children: existingFilesLoading ? /* @__PURE__ */ jsx7("div", { className: "w-100 d-flex justify-content-center align-items-center", children: /* @__PURE__ */ jsx7("div", { className: "spinner-border text-secondary", role: "status" }) }) : existingFiles.length === 0 ? /* @__PURE__ */ jsxs5(
1404
+ "div",
1405
+ {
1406
+ className: "w-100 d-flex flex-column align-items-center justify-content-center text-muted",
1407
+ style: {
1408
+ minHeight: "160px",
1409
+ padding: "20px",
1410
+ cursor: "pointer",
1411
+ transition: "background 0.12s, border-color 0.12s"
1412
+ },
1413
+ onClick: () => inputRef.current?.click(),
1414
+ children: [
1415
+ /* @__PURE__ */ jsx7("strong", { children: "Drag & drop files here" }),
1416
+ /* @__PURE__ */ jsx7("small", { className: "mt-1", children: "\u2026or click to browse" })
1417
+ ]
1418
+ }
1419
+ ) : existingFiles.map((file) => /* @__PURE__ */ jsx7(
1420
+ DesktopFileIcon,
1421
+ {
1422
+ name: file.Name,
1423
+ sizeBytes: file.FileSize,
1424
+ downloadUrl: file.PublicUrl,
1425
+ onOpen: () => handleExistingFileOpen(file),
1426
+ onDelete: () => onDeleteFile?.(file)
883
1427
  },
884
- onClick: () => document.getElementById("filePicker")?.click(),
885
- onMouseEnter: (e) => e.currentTarget.style.borderColor = "#888",
886
- onMouseLeave: (e) => e.currentTarget.style.borderColor = "#ccc",
887
- children: [
888
- /* @__PURE__ */ jsx4(
889
- "input",
890
- {
891
- id: "filePicker",
892
- type: "file",
893
- multiple: true,
894
- hidden: true,
895
- onChange: (e) => {
896
- if (!e.target.files) return;
897
- onFilesSelected?.(e.target.files);
898
- startUploadsIfNeeded(e.target.files);
899
- }
900
- }
901
- ),
902
- /* @__PURE__ */ jsx4("strong", { children: "Drag & drop files here" }),
903
- /* @__PURE__ */ jsx4("small", { className: "mt-1", children: "\u2026or click to browse" })
904
- ]
905
- }
906
- ) : existingFiles.map((file) => /* @__PURE__ */ jsx4(
907
- DesktopFileIcon,
908
- {
909
- name: file.Name,
910
- sizeBytes: file.FileSize,
911
- downloadUrl: file.PublicUrl,
912
- onOpen: () => handleExistingFileOpen(file),
913
- onDelete: () => onDeleteFile?.(file)
914
- },
915
- file.Id
916
- ))
917
- }
918
- )
919
- ]
920
- }
921
- )
922
- ] });
923
- };
1428
+ file.Id
1429
+ ))
1430
+ }
1431
+ )
1432
+ ]
1433
+ }
1434
+ )
1435
+ ] });
1436
+ }
1437
+ );
1438
+ UploadContainer.displayName = "UploadContainer";
924
1439
 
925
1440
  // src/hooks/UseContainers.ts
926
- import { useEffect as useEffect2, useState as useState4 } from "react";
1441
+ import { useEffect as useEffect3, useState as useState6 } from "react";
927
1442
  function UseContainers({ apiBaseUrl, parentId }) {
928
- const [containers, setContainers] = useState4([]);
929
- const [loading, setLoading] = useState4(false);
930
- const [error, setError] = useState4(null);
1443
+ const [containers, setContainers] = useState6([]);
1444
+ const [loading, setLoading] = useState6(false);
1445
+ const [error, setError] = useState6(null);
931
1446
  const load = async () => {
932
1447
  setLoading(true);
933
1448
  setError(null);
@@ -943,7 +1458,7 @@ function UseContainers({ apiBaseUrl, parentId }) {
943
1458
  setLoading(false);
944
1459
  }
945
1460
  };
946
- useEffect2(() => {
1461
+ useEffect3(() => {
947
1462
  void load();
948
1463
  }, [apiBaseUrl, parentId]);
949
1464
  return {
@@ -956,11 +1471,12 @@ function UseContainers({ apiBaseUrl, parentId }) {
956
1471
  }
957
1472
 
958
1473
  // src/components/ContainerUploadPanel.tsx
959
- import { jsx as jsx5 } from "react/jsx-runtime";
1474
+ import { jsx as jsx8 } from "react/jsx-runtime";
960
1475
  var ContainerUploadPanel = ({
961
1476
  containerApiBaseUrl,
962
1477
  storageApiBaseUrl,
963
- parentContainerId
1478
+ parentContainerId,
1479
+ uploadRef
964
1480
  }) => {
965
1481
  const { containers, setContainers, reload, loading } = UseContainers({
966
1482
  apiBaseUrl: containerApiBaseUrl,
@@ -1015,9 +1531,10 @@ var ContainerUploadPanel = ({
1015
1531
  await sdkS3.s3.DeleteS3(file);
1016
1532
  setContainers((prev) => prev.filter((c) => c.Id !== file.Id));
1017
1533
  };
1018
- return /* @__PURE__ */ jsx5(
1534
+ return /* @__PURE__ */ jsx8(
1019
1535
  UploadContainer,
1020
1536
  {
1537
+ ref: uploadRef,
1021
1538
  existingFiles: containers,
1022
1539
  existingFilesLoading: loading,
1023
1540
  onExistingFileClick: handleExistingFileClick,
@@ -1030,9 +1547,172 @@ var ContainerUploadPanel = ({
1030
1547
  );
1031
1548
  };
1032
1549
 
1550
+ // src/components/FileGridUploadPanel.tsx
1551
+ import { useMemo as useMemo4, useState as useState7 } from "react";
1552
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
1553
+ function FileGridUploadPanel(props) {
1554
+ const {
1555
+ containerApiBaseUrl,
1556
+ storageApiBaseUrl,
1557
+ parentContainerId,
1558
+ accept = "*/*",
1559
+ multiple = true,
1560
+ selectedId,
1561
+ onSelect,
1562
+ icon,
1563
+ iconHtml,
1564
+ deleteDisabled
1565
+ } = props;
1566
+ const { containers, setContainers, reload, loading } = UseContainers({
1567
+ apiBaseUrl: containerApiBaseUrl,
1568
+ parentId: parentContainerId
1569
+ });
1570
+ const [isDragging, setIsDragging] = useState7(false);
1571
+ const [error, setError] = useState7(null);
1572
+ const sdkDb = useMemo4(() => new SparkStudioStorageSDK(containerApiBaseUrl), [containerApiBaseUrl]);
1573
+ const sdkS3 = useMemo4(() => new SparkStudioStorageSDK(storageApiBaseUrl), [storageApiBaseUrl]);
1574
+ const getPresignedUrl = async (file) => {
1575
+ const contentType = file.type || "application/octet-stream";
1576
+ const containerDTO = await sdkDb.container.CreateFileContainer(
1577
+ file.name,
1578
+ file.size,
1579
+ encodeURIComponent(contentType)
1580
+ );
1581
+ let lastError;
1582
+ for (let i = 1; i <= 3; i++) {
1583
+ try {
1584
+ return await sdkS3.s3.GetPreSignedUrl(containerDTO);
1585
+ } catch (e) {
1586
+ lastError = e;
1587
+ if (i < 3) await new Promise((r) => setTimeout(r, 500 * i));
1588
+ }
1589
+ }
1590
+ throw lastError instanceof Error ? lastError : new Error("Failed to fetch presigned URL");
1591
+ };
1592
+ const { uploads, startUploadsIfNeeded } = UseUploadManager({
1593
+ autoUpload: true,
1594
+ getPresignedUrl,
1595
+ onUploadComplete: async () => {
1596
+ setError(null);
1597
+ await reload();
1598
+ },
1599
+ onUploadError: (_file, err) => {
1600
+ setError(err.message ?? "Upload failed");
1601
+ }
1602
+ });
1603
+ const handleDeleteFile = async (file) => {
1604
+ const sdkDb2 = new SparkStudioStorageSDK(containerApiBaseUrl);
1605
+ const sdkS32 = new SparkStudioStorageSDK(storageApiBaseUrl);
1606
+ await sdkDb2.container.DeleteContainer(file.Id);
1607
+ await sdkS32.s3.DeleteS3(file);
1608
+ setContainers((prev) => prev.filter((c) => c.Id !== file.Id));
1609
+ };
1610
+ const openPicker = () => {
1611
+ const input = document.createElement("input");
1612
+ input.type = "file";
1613
+ input.multiple = multiple;
1614
+ input.accept = accept;
1615
+ input.onchange = () => {
1616
+ if (!input.files || input.files.length === 0) return;
1617
+ startUploadsIfNeeded(input.files);
1618
+ };
1619
+ input.click();
1620
+ };
1621
+ return /* @__PURE__ */ jsxs6("div", { style: { display: "grid", gap: 12 }, children: [
1622
+ /* @__PURE__ */ jsxs6("div", { children: [
1623
+ /* @__PURE__ */ jsx9(UploadProgressList, { uploads }),
1624
+ error ? /* @__PURE__ */ jsx9("div", { style: { fontSize: 12, color: "crimson", marginTop: 6 }, children: error }) : null
1625
+ ] }),
1626
+ /* @__PURE__ */ jsxs6(
1627
+ "div",
1628
+ {
1629
+ onDragEnter: (e) => {
1630
+ e.preventDefault();
1631
+ e.stopPropagation();
1632
+ setIsDragging(true);
1633
+ },
1634
+ onDragOver: (e) => {
1635
+ e.preventDefault();
1636
+ e.stopPropagation();
1637
+ setIsDragging(true);
1638
+ },
1639
+ onDragLeave: (e) => {
1640
+ e.preventDefault();
1641
+ e.stopPropagation();
1642
+ setIsDragging(false);
1643
+ },
1644
+ onDrop: (e) => {
1645
+ e.preventDefault();
1646
+ e.stopPropagation();
1647
+ setIsDragging(false);
1648
+ const files = e.dataTransfer.files;
1649
+ if (!files || files.length === 0) return;
1650
+ startUploadsIfNeeded(files);
1651
+ },
1652
+ style: {
1653
+ position: "relative",
1654
+ borderRadius: 14,
1655
+ border: isDragging ? "2px dashed rgba(13,110,253,0.9)" : "2px dashed rgba(0,0,0,0.15)",
1656
+ background: isDragging ? "rgba(13,110,253,0.06)" : "transparent",
1657
+ padding: 12
1658
+ },
1659
+ children: [
1660
+ isDragging ? /* @__PURE__ */ jsx9(
1661
+ "div",
1662
+ {
1663
+ style: {
1664
+ position: "absolute",
1665
+ inset: 0,
1666
+ borderRadius: 14,
1667
+ display: "grid",
1668
+ placeItems: "center",
1669
+ pointerEvents: "none",
1670
+ background: "rgba(13,110,253,0.08)",
1671
+ fontWeight: 800
1672
+ },
1673
+ children: "Drop files to upload"
1674
+ }
1675
+ ) : null,
1676
+ /* @__PURE__ */ jsxs6("div", { style: { display: "flex", justifyContent: "space-between", marginBottom: 10 }, children: [
1677
+ /* @__PURE__ */ jsx9("div", { style: { fontWeight: 700 }, children: "Files" }),
1678
+ /* @__PURE__ */ jsx9(
1679
+ "button",
1680
+ {
1681
+ type: "button",
1682
+ onClick: openPicker,
1683
+ style: {
1684
+ border: "1px solid rgba(0,0,0,0.18)",
1685
+ background: "white",
1686
+ borderRadius: 10,
1687
+ padding: "8px 10px",
1688
+ cursor: "pointer",
1689
+ fontWeight: 600
1690
+ },
1691
+ children: "Browse\u2026"
1692
+ }
1693
+ )
1694
+ ] }),
1695
+ loading ? /* @__PURE__ */ jsx9("div", { className: "d-flex justify-content-center align-items-center", style: { minHeight: 120 }, children: /* @__PURE__ */ jsx9("div", { className: "spinner-border text-secondary", role: "status" }) }) : /* @__PURE__ */ jsx9(
1696
+ FileIconGrid,
1697
+ {
1698
+ files: containers,
1699
+ deleteDisabled,
1700
+ onDeleted: handleDeleteFile,
1701
+ selectedId,
1702
+ onSelect,
1703
+ icon,
1704
+ iconHtml
1705
+ }
1706
+ )
1707
+ ]
1708
+ }
1709
+ )
1710
+ ] });
1711
+ }
1712
+
1033
1713
  // src/components/SingleFileProcessUploader.tsx
1034
- import { useCallback as useCallback2, useRef as useRef2, useState as useState5 } from "react";
1035
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1714
+ import { useCallback as useCallback2, useRef as useRef4, useState as useState8 } from "react";
1715
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
1036
1716
  var SingleFileProcessUploader = ({
1037
1717
  getPresignedUrl,
1038
1718
  onUploadComplete,
@@ -1041,11 +1721,11 @@ var SingleFileProcessUploader = ({
1041
1721
  disabled,
1042
1722
  uploadOnDrop = false
1043
1723
  }) => {
1044
- const [selectedFile, setSelectedFile] = useState5(null);
1045
- const [isDragging, setIsDragging] = useState5(false);
1046
- const [progress, setProgress] = useState5(null);
1047
- const [status, setStatus] = useState5("idle");
1048
- const fileInputRef = useRef2(null);
1724
+ const [selectedFile, setSelectedFile] = useState8(null);
1725
+ const [isDragging, setIsDragging] = useState8(false);
1726
+ const [progress, setProgress] = useState8(null);
1727
+ const [status, setStatus] = useState8("idle");
1728
+ const fileInputRef = useRef4(null);
1049
1729
  function getErrorMessage(err) {
1050
1730
  if (err instanceof Error) return err.message;
1051
1731
  if (typeof err === "string") return err;
@@ -1131,8 +1811,8 @@ var SingleFileProcessUploader = ({
1131
1811
  disabled ? "opacity-50" : "cursor-pointer",
1132
1812
  isDragging ? "bg-body-secondary border-dashed border-2 border-secondary" : "bg-body-trasparent border-dashed border-2"
1133
1813
  ].join(" ");
1134
- return /* @__PURE__ */ jsxs4("div", { className: "d-flex flex-column gap-2", children: [
1135
- isUploading ? /* @__PURE__ */ jsx6("div", { className: "small", children: /* @__PURE__ */ jsx6("div", { className: "progress", children: /* @__PURE__ */ jsx6(
1814
+ return /* @__PURE__ */ jsxs7("div", { className: "d-flex flex-column gap-2", children: [
1815
+ isUploading ? /* @__PURE__ */ jsx10("div", { className: "small", children: /* @__PURE__ */ jsx10("div", { className: "progress", children: /* @__PURE__ */ jsx10(
1136
1816
  "div",
1137
1817
  {
1138
1818
  className: "progress-bar progress-bar-striped progress-bar-animated",
@@ -1142,7 +1822,7 @@ var SingleFileProcessUploader = ({
1142
1822
  "aria-valuenow": progress ?? 0,
1143
1823
  style: { width: `${progress ?? 0}%` }
1144
1824
  }
1145
- ) }) }) : /* @__PURE__ */ jsxs4(
1825
+ ) }) }) : /* @__PURE__ */ jsxs7(
1146
1826
  "div",
1147
1827
  {
1148
1828
  className: dropzoneClasses,
@@ -1153,7 +1833,7 @@ var SingleFileProcessUploader = ({
1153
1833
  role: "button",
1154
1834
  "aria-disabled": disabled,
1155
1835
  children: [
1156
- /* @__PURE__ */ jsx6(
1836
+ /* @__PURE__ */ jsx10(
1157
1837
  "input",
1158
1838
  {
1159
1839
  ref: fileInputRef,
@@ -1164,8 +1844,8 @@ var SingleFileProcessUploader = ({
1164
1844
  disabled: disabled || isUploading
1165
1845
  }
1166
1846
  ),
1167
- selectedFile ? /* @__PURE__ */ jsxs4("div", { className: "small", children: [
1168
- /* @__PURE__ */ jsx6(
1847
+ selectedFile ? /* @__PURE__ */ jsxs7("div", { className: "small", children: [
1848
+ /* @__PURE__ */ jsx10(
1169
1849
  "button",
1170
1850
  {
1171
1851
  type: "button",
@@ -1174,15 +1854,15 @@ var SingleFileProcessUploader = ({
1174
1854
  children: "Browse file\u2026"
1175
1855
  }
1176
1856
  ),
1177
- /* @__PURE__ */ jsxs4("div", { children: [
1178
- /* @__PURE__ */ jsx6("strong", { children: "Selected file:" }),
1857
+ /* @__PURE__ */ jsxs7("div", { children: [
1858
+ /* @__PURE__ */ jsx10("strong", { children: "Selected file:" }),
1179
1859
  " ",
1180
1860
  selectedFile.name
1181
1861
  ] }),
1182
- /* @__PURE__ */ jsx6("div", { className: "text-muted", children: "Click here to change file or drag a new one." }),
1183
- uploadOnDrop && /* @__PURE__ */ jsx6("div", { className: "text-muted", children: "Upload starts automatically." })
1184
- ] }) : /* @__PURE__ */ jsxs4("div", { className: "small", children: [
1185
- /* @__PURE__ */ jsx6(
1862
+ /* @__PURE__ */ jsx10("div", { className: "text-muted", children: "Click here to change file or drag a new one." }),
1863
+ uploadOnDrop && /* @__PURE__ */ jsx10("div", { className: "text-muted", children: "Upload starts automatically." })
1864
+ ] }) : /* @__PURE__ */ jsxs7("div", { className: "small", children: [
1865
+ /* @__PURE__ */ jsx10(
1186
1866
  "button",
1187
1867
  {
1188
1868
  type: "button",
@@ -1191,13 +1871,13 @@ var SingleFileProcessUploader = ({
1191
1871
  children: "Browse file\u2026"
1192
1872
  }
1193
1873
  ),
1194
- /* @__PURE__ */ jsx6("div", { children: "Drag & drop a file here" }),
1195
- /* @__PURE__ */ jsx6("div", { className: "text-muted", children: "or click to browse" })
1874
+ /* @__PURE__ */ jsx10("div", { children: "Drag & drop a file here" }),
1875
+ /* @__PURE__ */ jsx10("div", { className: "text-muted", children: "or click to browse" })
1196
1876
  ] })
1197
1877
  ]
1198
1878
  }
1199
1879
  ),
1200
- !uploadOnDrop && /* @__PURE__ */ jsx6(
1880
+ !uploadOnDrop && /* @__PURE__ */ jsx10(
1201
1881
  "button",
1202
1882
  {
1203
1883
  type: "button",
@@ -1211,39 +1891,49 @@ var SingleFileProcessUploader = ({
1211
1891
  };
1212
1892
 
1213
1893
  // src/views/HomeView.tsx
1894
+ import { useEffect as useEffect4, useState as useState9 } from "react";
1214
1895
  import {
1215
1896
  AppSettings,
1216
1897
  AuthenticatorProvider,
1217
1898
  UserInfoCard,
1218
1899
  useUser
1219
1900
  } from "@sparkstudio/authentication-ui";
1220
- import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1901
+ import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
1902
+ var CONTAINER_API = "https://lf9zyufpuk.execute-api.us-east-2.amazonaws.com/Prod";
1903
+ var STORAGE_API = "https://iq0gmcn0pd.execute-api.us-east-2.amazonaws.com/Prod";
1221
1904
  function HomeView() {
1222
- return /* @__PURE__ */ jsx7(
1905
+ return /* @__PURE__ */ jsx11(
1223
1906
  AuthenticatorProvider,
1224
1907
  {
1225
1908
  googleClientId: AppSettings.GoogleClientId,
1226
1909
  authenticationUrl: AppSettings.AuthenticationUrl,
1227
1910
  accountsUrl: AppSettings.AccountsUrl,
1228
- children: /* @__PURE__ */ jsx7(HomeContent, {})
1911
+ children: /* @__PURE__ */ jsx11(HomeContent, {})
1229
1912
  }
1230
1913
  );
1231
1914
  }
1232
1915
  function HomeContent() {
1233
1916
  const { user } = useUser();
1917
+ const [ids, setIds] = useState9([]);
1918
+ const [selectedId, setSelectedId] = useState9(void 0);
1919
+ const [selectedFile, setSelectedFile] = useState9(null);
1920
+ useEffect4(() => {
1921
+ if (selectedId && !ids.includes(selectedId)) {
1922
+ setSelectedId(void 0);
1923
+ setSelectedFile(null);
1924
+ }
1925
+ }, [ids, selectedId]);
1234
1926
  async function getPresignedUrlFromApi(file) {
1235
- const res = new SparkStudioStorageSDK(
1236
- "https://iq0gmcn0pd.execute-api.us-east-2.amazonaws.com/Prod"
1237
- //"https://localhost:5001"
1238
- );
1927
+ const sdk = new SparkStudioStorageSDK(STORAGE_API);
1239
1928
  const contentType = file.type || "application/octet-stream";
1240
- const result = await res.s3.GetTemporaryPreSignedUrl(new TemporaryFileDTO({ Name: file.name, ContentType: contentType }));
1241
- return result;
1929
+ return sdk.s3.GetTemporaryPreSignedUrl(
1930
+ new TemporaryFileDTO({ Name: file.name, ContentType: contentType })
1931
+ );
1242
1932
  }
1243
- return /* @__PURE__ */ jsxs5(Fragment3, { children: [
1244
- /* @__PURE__ */ jsx7(UserInfoCard, {}),
1245
- user && /* @__PURE__ */ jsxs5(Fragment3, { children: [
1246
- /* @__PURE__ */ jsx7(
1933
+ return /* @__PURE__ */ jsxs8(Fragment3, { children: [
1934
+ /* @__PURE__ */ jsx11(UserInfoCard, {}),
1935
+ user ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
1936
+ /* @__PURE__ */ jsx11(
1247
1937
  SingleFileProcessUploader,
1248
1938
  {
1249
1939
  uploadOnDrop: true,
@@ -1254,23 +1944,59 @@ function HomeContent() {
1254
1944
  }
1255
1945
  }
1256
1946
  ),
1257
- /* @__PURE__ */ jsx7(
1258
- ContainerUploadPanel,
1947
+ /* @__PURE__ */ jsx11(
1948
+ ContainerIdGridPanel,
1949
+ {
1950
+ containerApiBaseUrl: CONTAINER_API,
1951
+ storageApiBaseUrl: STORAGE_API,
1952
+ containerIds: ids,
1953
+ onContainerIdsChange: setIds,
1954
+ multiple: true,
1955
+ accept: "*/*",
1956
+ selectedId,
1957
+ onSelect: (file) => {
1958
+ setSelectedId(file.Id);
1959
+ setSelectedFile(file);
1960
+ },
1961
+ onDeleted: (file) => {
1962
+ console.log("Deleted:", file);
1963
+ if (selectedId === file.Id) {
1964
+ setSelectedId(void 0);
1965
+ setSelectedFile(null);
1966
+ }
1967
+ }
1968
+ }
1969
+ ),
1970
+ selectedFile ? /* @__PURE__ */ jsxs8(
1971
+ "div",
1259
1972
  {
1260
- containerApiBaseUrl: "https://lf9zyufpuk.execute-api.us-east-2.amazonaws.com/Prod",
1261
- storageApiBaseUrl: "https://iq0gmcn0pd.execute-api.us-east-2.amazonaws.com/Prod"
1973
+ style: {
1974
+ marginTop: 12,
1975
+ padding: 12,
1976
+ border: "1px solid rgba(0,0,0,0.12)",
1977
+ borderRadius: 12
1978
+ },
1979
+ children: [
1980
+ /* @__PURE__ */ jsx11("div", { style: { fontWeight: 700 }, children: "Selected file" }),
1981
+ /* @__PURE__ */ jsx11("div", { children: selectedFile.Name ?? "(no name)" }),
1982
+ /* @__PURE__ */ jsx11("div", { style: { fontSize: 12, opacity: 0.7 }, children: selectedFile.PublicUrl ?? "(no public url)" })
1983
+ ]
1262
1984
  }
1263
- )
1264
- ] })
1985
+ ) : null
1986
+ ] }) : null
1265
1987
  ] });
1266
1988
  }
1267
1989
  export {
1268
1990
  AWSPresignedUrlDTO,
1269
1991
  Container,
1270
1992
  ContainerDTO,
1993
+ ContainerIdGridPanel,
1271
1994
  ContainerType,
1272
1995
  ContainerUploadPanel,
1273
1996
  DesktopFileIcon,
1997
+ FileGridUploadPanel,
1998
+ FileIconCard,
1999
+ FileIconGrid,
1274
2000
  Home,
1275
2001
  HomeView,
1276
2002
  S3,