@sparkstudio/storage-ui 1.0.27 → 1.0.29

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,557 @@ 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
+ } = props;
735
+ const sdkDb = useMemo2(
736
+ () => new SparkStudioStorageSDK(containerApiBaseUrl),
737
+ [containerApiBaseUrl]
738
+ );
739
+ const sdkS3 = useMemo2(
740
+ () => new SparkStudioStorageSDK(storageApiBaseUrl),
741
+ [storageApiBaseUrl]
742
+ );
743
+ const [loading, setLoading] = useState3(false);
744
+ const [files, setFiles] = useState3([]);
745
+ const [dragOver, setDragOver] = useState3(false);
746
+ const [errMsg, setErrMsg] = useState3(null);
747
+ const idsRef = useRef(containerIds);
748
+ useEffect(() => {
749
+ idsRef.current = containerIds;
750
+ }, [containerIds]);
751
+ const createdByFileRef = useRef(/* @__PURE__ */ new WeakMap());
752
+ useEffect(() => {
753
+ let cancelled = false;
754
+ async function loadByIds(ids) {
755
+ setErrMsg(null);
756
+ if (!ids || ids.length === 0) {
757
+ setFiles([]);
758
+ return;
759
+ }
760
+ setLoading(true);
761
+ try {
762
+ const results = await Promise.all(
763
+ ids.map(async (id) => {
764
+ try {
765
+ const dto = await sdkDb.container.Read?.(id);
766
+ return dto ?? null;
767
+ } catch {
768
+ return null;
769
+ }
770
+ })
771
+ );
772
+ if (cancelled) return;
773
+ const map = /* @__PURE__ */ new Map();
774
+ for (const r of results) if (r?.Id) map.set(r.Id, r);
775
+ setFiles(
776
+ ids.map((id) => map.get(id)).filter(Boolean)
777
+ );
778
+ } catch (e) {
779
+ if (!cancelled)
780
+ setErrMsg(e instanceof Error ? e.message : "Failed to load files");
781
+ } finally {
782
+ if (!cancelled) setLoading(false);
783
+ }
784
+ }
785
+ void loadByIds(containerIds);
786
+ return () => {
787
+ cancelled = true;
788
+ };
789
+ }, [containerIds, sdkDb]);
790
+ const getPresignedUrl = async (file) => {
791
+ const contentType = file.type || "application/octet-stream";
792
+ const containerDTO = await sdkDb.container.CreateFileContainer(
793
+ file.name,
794
+ file.size,
795
+ encodeURIComponent(contentType)
796
+ );
797
+ createdByFileRef.current.set(file, containerDTO);
798
+ let lastError;
799
+ for (let i = 1; i <= 3; i++) {
800
+ try {
801
+ return await sdkS3.s3.GetPreSignedUrl(containerDTO);
802
+ } catch (e) {
803
+ lastError = e;
804
+ if (i < 3) await new Promise((r) => setTimeout(r, 500 * i));
805
+ }
806
+ }
807
+ throw lastError instanceof Error ? lastError : new Error("Failed to fetch presigned URL");
808
+ };
809
+ const { uploads, startUploadsIfNeeded } = UseUploadManager({
810
+ autoUpload: true,
811
+ getPresignedUrl,
812
+ onUploadComplete: async (file) => {
813
+ setErrMsg(null);
814
+ const created = createdByFileRef.current.get(file);
815
+ if (created?.Id) {
816
+ const prev = idsRef.current ?? [];
817
+ const next = prev.includes(created.Id) ? prev : [...prev, created.Id];
818
+ onContainerIdsChange(next);
819
+ }
820
+ if (created?.Id) {
821
+ try {
822
+ const refreshed = await sdkDb.container.Read?.(created.Id);
823
+ if (refreshed) {
824
+ setFiles((prevFiles) => {
825
+ const nextFiles = prevFiles.slice();
826
+ const idx = nextFiles.findIndex((x) => x.Id === created.Id);
827
+ if (idx >= 0) nextFiles[idx] = refreshed;
828
+ else nextFiles.push(refreshed);
829
+ return nextFiles;
830
+ });
831
+ }
832
+ } catch {
833
+ }
834
+ }
835
+ },
836
+ onUploadError: (_file, error) => {
837
+ setErrMsg(error?.message ?? "Upload failed");
838
+ }
839
+ });
840
+ const handleDelete = async (file) => {
841
+ if (deleteDisabled) return;
842
+ await sdkDb.container.DeleteContainer(file.Id);
843
+ await sdkS3.s3.DeleteS3(file);
844
+ const prev = idsRef.current ?? [];
845
+ onContainerIdsChange(prev.filter((id) => id !== file.Id));
846
+ setFiles((prevFiles) => prevFiles.filter((x) => x.Id !== file.Id));
847
+ };
848
+ const openPicker = () => {
849
+ const input = document.createElement("input");
850
+ input.type = "file";
851
+ input.multiple = multiple;
852
+ input.accept = accept;
853
+ input.onchange = () => {
854
+ if (input.files) startUploadsIfNeeded(input.files);
855
+ };
856
+ input.click();
857
+ };
858
+ const onDrop = (ev) => {
859
+ ev.preventDefault();
860
+ ev.stopPropagation();
861
+ setDragOver(false);
862
+ const list = ev.dataTransfer.files;
863
+ if (!list || list.length === 0) return;
864
+ startUploadsIfNeeded(list);
865
+ };
866
+ return /* @__PURE__ */ jsxs3("div", { className, style: { display: "grid", gap: 12 }, children: [
867
+ /* @__PURE__ */ jsxs3("div", { children: [
868
+ /* @__PURE__ */ jsx4(UploadProgressList, { uploads }),
869
+ errMsg ? /* @__PURE__ */ jsx4("div", { style: { fontSize: 12, color: "crimson", marginTop: 6 }, children: errMsg }) : null
870
+ ] }),
871
+ /* @__PURE__ */ jsxs3(
872
+ "div",
873
+ {
874
+ onDragEnter: (e) => {
875
+ e.preventDefault();
876
+ e.stopPropagation();
877
+ setDragOver(true);
878
+ },
879
+ onDragOver: (e) => {
880
+ e.preventDefault();
881
+ e.stopPropagation();
882
+ setDragOver(true);
883
+ },
884
+ onDragLeave: (e) => {
885
+ e.preventDefault();
886
+ e.stopPropagation();
887
+ setDragOver(false);
888
+ },
889
+ onDrop,
890
+ style: {
891
+ position: "relative",
892
+ borderRadius: 14,
893
+ border: `2px dashed ${dragOver ? "rgba(13,110,253,0.9)" : "rgba(0,0,0,0.15)"}`,
894
+ background: dragOver ? "rgba(13,110,253,0.06)" : "transparent",
895
+ padding: 12
896
+ },
897
+ children: [
898
+ dragOver ? /* @__PURE__ */ jsx4(
899
+ "div",
900
+ {
901
+ style: {
902
+ position: "absolute",
903
+ inset: 0,
904
+ borderRadius: 14,
905
+ display: "grid",
906
+ placeItems: "center",
907
+ pointerEvents: "none",
908
+ background: "rgba(13,110,253,0.10)",
909
+ fontWeight: 800
910
+ },
911
+ children: "Drop files to upload"
912
+ }
913
+ ) : null,
914
+ /* @__PURE__ */ jsxs3(
915
+ "div",
916
+ {
917
+ style: {
918
+ display: "flex",
919
+ justifyContent: "space-between",
920
+ marginBottom: 10
921
+ },
922
+ children: [
923
+ /* @__PURE__ */ jsx4("div", { style: { fontWeight: 700 }, children: "Files" }),
924
+ /* @__PURE__ */ jsx4(
925
+ "button",
926
+ {
927
+ type: "button",
928
+ onClick: openPicker,
929
+ style: {
930
+ border: "1px solid rgba(0,0,0,0.18)",
931
+ background: "white",
932
+ borderRadius: 10,
933
+ padding: "8px 10px",
934
+ cursor: "pointer",
935
+ fontWeight: 600
936
+ },
937
+ children: "Browse\u2026"
938
+ }
939
+ )
940
+ ]
941
+ }
942
+ ),
943
+ loading ? /* @__PURE__ */ jsx4(
944
+ "div",
945
+ {
946
+ className: "d-flex justify-content-center align-items-center",
947
+ style: { minHeight: 120 },
948
+ children: /* @__PURE__ */ jsx4("div", { className: "spinner-border text-secondary", role: "status" })
949
+ }
950
+ ) : /* @__PURE__ */ jsx4(
951
+ FileIconGrid,
952
+ {
953
+ files,
954
+ deleteDisabled,
955
+ onDeleted: handleDelete,
956
+ selectedId,
957
+ onSelect,
958
+ icon,
959
+ iconHtml
960
+ }
961
+ )
962
+ ]
963
+ }
964
+ )
965
+ ] });
966
+ }
967
+
968
+ // src/components/ContainerUploadPanel.tsx
969
+ import "react";
970
+
971
+ // src/components/UploadContainer.tsx
972
+ import {
973
+ forwardRef,
974
+ useImperativeHandle,
975
+ useMemo as useMemo3,
976
+ useRef as useRef3,
977
+ useState as useState5
978
+ } from "react";
979
+
980
+ // src/components/UploadDropzone.tsx
981
+ import "react";
982
+ import { jsx as jsx5 } from "react/jsx-runtime";
983
+ var UploadDropzone = ({
984
+ isDragging,
985
+ onDragOver,
986
+ onDragLeave,
987
+ onDrop,
988
+ className = "",
989
+ style,
990
+ children
991
+ }) => {
992
+ const baseClass = "rounded-3 d-flex flex-column align-items-center justify-content-center";
993
+ const stateClass = isDragging ? "bg-body-secondary border-dashed border-2 border-secondary" : "bg-body-trasparent border-solid border-transparent border-2";
994
+ const combinedClassName = `${baseClass} ${stateClass} ${className}`.trim();
995
+ const handleDragOver = (e) => {
996
+ e.preventDefault();
997
+ onDragOver?.(e);
998
+ };
999
+ const handleDragLeave = (e) => {
1000
+ e.preventDefault();
1001
+ onDragLeave?.(e);
1002
+ };
1003
+ const handleDrop = (e) => {
1004
+ e.preventDefault();
1005
+ onDrop?.(e);
1006
+ };
1007
+ return /* @__PURE__ */ jsx5(
1008
+ "div",
1009
+ {
1010
+ className: combinedClassName,
1011
+ style: { minHeight: "140px", ...style },
1012
+ onDragOver: handleDragOver,
1013
+ onDragLeave: handleDragLeave,
1014
+ onDrop: handleDrop,
1015
+ children
1016
+ }
1017
+ );
1018
+ };
1019
+
512
1020
  // src/components/DesktopFileIcon.tsx
513
1021
  import {
514
1022
  faFile,
@@ -522,8 +1030,8 @@ import {
522
1030
  faFileCode
523
1031
  } from "@fortawesome/free-solid-svg-icons";
524
1032
  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";
1033
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState4 } from "react";
1034
+ import { Fragment, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
527
1035
  function getFileExtension(name) {
528
1036
  if (!name) return null;
529
1037
  const lastDot = name.lastIndexOf(".");
@@ -602,13 +1110,13 @@ var DesktopFileIcon = ({
602
1110
  onOpen,
603
1111
  onDelete
604
1112
  }) => {
605
- const [contextMenuPos, setContextMenuPos] = useState2(
1113
+ const [contextMenuPos, setContextMenuPos] = useState4(
606
1114
  null
607
1115
  );
608
- const [isHovered, setIsHovered] = useState2(false);
609
- const [isDeleting, setIsDeleting] = useState2(false);
610
- const iconRef = useRef(null);
611
- const menuRef = useRef(null);
1116
+ const [isHovered, setIsHovered] = useState4(false);
1117
+ const [isDeleting, setIsDeleting] = useState4(false);
1118
+ const iconRef = useRef2(null);
1119
+ const menuRef = useRef2(null);
612
1120
  const handleDoubleClick = () => {
613
1121
  if (isDeleting) return;
614
1122
  if (onOpen) {
@@ -666,7 +1174,7 @@ var DesktopFileIcon = ({
666
1174
  const formattedSize = typeof sizeBytes === "number" ? `${(sizeBytes / 1024).toFixed(1)} KB` : void 0;
667
1175
  const ext = getFileExtension(name);
668
1176
  const iconToRender = getIconForExtension(ext);
669
- useEffect(() => {
1177
+ useEffect2(() => {
670
1178
  if (!contextMenuPos) return;
671
1179
  const handleGlobalClick = (e) => {
672
1180
  const target = e.target;
@@ -683,8 +1191,8 @@ var DesktopFileIcon = ({
683
1191
  document.removeEventListener("mousedown", handleGlobalClick);
684
1192
  };
685
1193
  }, [contextMenuPos]);
686
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
687
- /* @__PURE__ */ jsxs2(
1194
+ return /* @__PURE__ */ jsxs4(Fragment, { children: [
1195
+ /* @__PURE__ */ jsxs4(
688
1196
  "div",
689
1197
  {
690
1198
  ref: iconRef,
@@ -705,7 +1213,7 @@ var DesktopFileIcon = ({
705
1213
  onMouseEnter: () => setIsHovered(true),
706
1214
  onMouseLeave: () => setIsHovered(false),
707
1215
  children: [
708
- /* @__PURE__ */ jsxs2(
1216
+ /* @__PURE__ */ jsxs4(
709
1217
  "div",
710
1218
  {
711
1219
  className: "border rounded-3 d-flex align-items-center justify-content-center mb-1 shadow-sm position-relative",
@@ -714,17 +1222,17 @@ var DesktopFileIcon = ({
714
1222
  height: 64
715
1223
  },
716
1224
  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" }) })
1225
+ /* @__PURE__ */ jsx6(FontAwesomeIcon, { icon: iconToRender, className: "fs-2" }),
1226
+ 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
1227
  ]
720
1228
  }
721
1229
  ),
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 })
1230
+ /* @__PURE__ */ jsx6("div", { className: "small text-center text-truncate", style: { width: "100%" }, children: name }),
1231
+ formattedSize && /* @__PURE__ */ jsx6("small", { className: "text-muted mt-1", children: formattedSize })
724
1232
  ]
725
1233
  }
726
1234
  ),
727
- contextMenuPos && !isDeleting && /* @__PURE__ */ jsxs2(
1235
+ contextMenuPos && !isDeleting && /* @__PURE__ */ jsxs4(
728
1236
  "div",
729
1237
  {
730
1238
  ref: menuRef,
@@ -737,7 +1245,7 @@ var DesktopFileIcon = ({
737
1245
  },
738
1246
  onClick: (e) => e.stopPropagation(),
739
1247
  children: [
740
- /* @__PURE__ */ jsx3(
1248
+ /* @__PURE__ */ jsx6(
741
1249
  "button",
742
1250
  {
743
1251
  type: "button",
@@ -747,7 +1255,7 @@ var DesktopFileIcon = ({
747
1255
  children: "Download file"
748
1256
  }
749
1257
  ),
750
- /* @__PURE__ */ jsx3(
1258
+ /* @__PURE__ */ jsx6(
751
1259
  "button",
752
1260
  {
753
1261
  type: "button",
@@ -757,8 +1265,8 @@ var DesktopFileIcon = ({
757
1265
  children: "Copy download URL"
758
1266
  }
759
1267
  ),
760
- /* @__PURE__ */ jsx3("div", { className: "dropdown-divider" }),
761
- /* @__PURE__ */ jsx3(
1268
+ /* @__PURE__ */ jsx6("div", { className: "dropdown-divider" }),
1269
+ /* @__PURE__ */ jsx6(
762
1270
  "button",
763
1271
  {
764
1272
  type: "button",
@@ -774,160 +1282,165 @@ var DesktopFileIcon = ({
774
1282
  };
775
1283
 
776
1284
  // 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,
1285
+ import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1286
+ function filesToFileList(files) {
1287
+ const dt = new DataTransfer();
1288
+ for (const f of files) dt.items.add(f);
1289
+ return dt.files;
1290
+ }
1291
+ var UploadContainer = forwardRef(
1292
+ ({
1293
+ multiple = true,
1294
+ accept = "*/*",
1295
+ onFilesSelected,
1296
+ existingFiles = [],
1297
+ existingFilesLoading = false,
1298
+ onExistingFileClick,
1299
+ onDeleteFile,
1300
+ autoUpload = false,
792
1301
  getPresignedUrl,
793
1302
  onUploadComplete,
794
1303
  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);
1304
+ }, ref) => {
1305
+ const [isDragging, setIsDragging] = useState5(false);
1306
+ const inputId = useMemo3(() => `filePicker_${crypto.randomUUID()}`, []);
1307
+ const inputRef = useRef3(null);
1308
+ const { uploads, startUploadsIfNeeded } = UseUploadManager({
1309
+ autoUpload,
1310
+ getPresignedUrl,
1311
+ onUploadComplete,
1312
+ onUploadError
1313
+ });
1314
+ const selectAndUpload = (files) => {
1315
+ if (!files || files.length === 0) return;
1316
+ onFilesSelected?.(files);
1317
+ startUploadsIfNeeded(files);
1318
+ };
1319
+ useImperativeHandle(ref, () => ({
1320
+ enqueueFiles(files) {
1321
+ const list = Array.isArray(files) ? filesToFileList(files) : files;
1322
+ selectAndUpload(list);
1323
+ },
1324
+ openFilePicker() {
1325
+ inputRef.current?.click();
1326
+ }
1327
+ }));
1328
+ const handleDragOver = (e) => {
1329
+ e.preventDefault();
1330
+ setIsDragging(true);
1331
+ };
1332
+ const handleDragLeave = (e) => {
1333
+ e.preventDefault();
1334
+ setIsDragging(false);
1335
+ };
1336
+ const handleDrop = (e) => {
1337
+ e.preventDefault();
1338
+ setIsDragging(false);
1339
+ const files = e.dataTransfer.files;
1340
+ if (!files || files.length === 0) return;
1341
+ selectAndUpload(files);
1342
+ };
1343
+ const handleExistingFileOpen = (file) => {
1344
+ if (onExistingFileClick) {
1345
+ onExistingFileClick(file);
1346
+ return;
1347
+ }
1348
+ const a = document.createElement("a");
1349
+ a.href = file.PublicUrl ?? "";
1350
+ a.download = file.Name ?? "";
1351
+ a.target = "_blank";
1352
+ a.rel = "noopener noreferrer";
1353
+ document.body.appendChild(a);
1354
+ a.click();
1355
+ document.body.removeChild(a);
1356
+ };
1357
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
1358
+ /* @__PURE__ */ jsxs5("div", { className: "w-100", children: [
1359
+ /* @__PURE__ */ jsx7(
1360
+ "input",
1361
+ {
1362
+ ref: inputRef,
1363
+ id: inputId,
1364
+ type: "file",
1365
+ multiple,
1366
+ accept,
1367
+ className: "d-none",
1368
+ onChange: (e) => {
1369
+ if (!e.target.files) return;
1370
+ selectAndUpload(e.target.files);
1371
+ e.currentTarget.value = "";
1372
+ }
839
1373
  }
840
- }
841
- ),
842
- /* @__PURE__ */ jsx4("div", { className: "text-start", children: /* @__PURE__ */ jsx4(
843
- "button",
1374
+ ),
1375
+ /* @__PURE__ */ jsx7("div", { className: "text-start", children: /* @__PURE__ */ jsx7(
1376
+ "button",
1377
+ {
1378
+ type: "button",
1379
+ className: "btn btn-primary float-start",
1380
+ onClick: () => inputRef.current?.click(),
1381
+ children: "Browse files\u2026"
1382
+ }
1383
+ ) })
1384
+ ] }),
1385
+ /* @__PURE__ */ jsxs5(
1386
+ UploadDropzone,
844
1387
  {
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"
1388
+ isDragging,
1389
+ onDragOver: handleDragOver,
1390
+ onDragLeave: handleDragLeave,
1391
+ onDrop: handleDrop,
1392
+ className: "w-100",
1393
+ style: { minHeight: "100px", alignItems: "stretch" },
1394
+ children: [
1395
+ /* @__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 }) }) }) }),
1396
+ /* @__PURE__ */ jsx7(
1397
+ "div",
1398
+ {
1399
+ className: "w-100 d-flex flex-wrap gap-4 align-content-start",
1400
+ style: { minHeight: "140px" },
1401
+ 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(
1402
+ "div",
1403
+ {
1404
+ className: "w-100 d-flex flex-column align-items-center justify-content-center text-muted",
1405
+ style: {
1406
+ minHeight: "160px",
1407
+ padding: "20px",
1408
+ cursor: "pointer",
1409
+ transition: "background 0.12s, border-color 0.12s"
1410
+ },
1411
+ onClick: () => inputRef.current?.click(),
1412
+ children: [
1413
+ /* @__PURE__ */ jsx7("strong", { children: "Drag & drop files here" }),
1414
+ /* @__PURE__ */ jsx7("small", { className: "mt-1", children: "\u2026or click to browse" })
1415
+ ]
1416
+ }
1417
+ ) : existingFiles.map((file) => /* @__PURE__ */ jsx7(
1418
+ DesktopFileIcon,
1419
+ {
1420
+ name: file.Name,
1421
+ sizeBytes: file.FileSize,
1422
+ downloadUrl: file.PublicUrl,
1423
+ onOpen: () => handleExistingFileOpen(file),
1424
+ onDelete: () => onDeleteFile?.(file)
883
1425
  },
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
- };
1426
+ file.Id
1427
+ ))
1428
+ }
1429
+ )
1430
+ ]
1431
+ }
1432
+ )
1433
+ ] });
1434
+ }
1435
+ );
1436
+ UploadContainer.displayName = "UploadContainer";
924
1437
 
925
1438
  // src/hooks/UseContainers.ts
926
- import { useEffect as useEffect2, useState as useState4 } from "react";
1439
+ import { useEffect as useEffect3, useState as useState6 } from "react";
927
1440
  function UseContainers({ apiBaseUrl, parentId }) {
928
- const [containers, setContainers] = useState4([]);
929
- const [loading, setLoading] = useState4(false);
930
- const [error, setError] = useState4(null);
1441
+ const [containers, setContainers] = useState6([]);
1442
+ const [loading, setLoading] = useState6(false);
1443
+ const [error, setError] = useState6(null);
931
1444
  const load = async () => {
932
1445
  setLoading(true);
933
1446
  setError(null);
@@ -943,7 +1456,7 @@ function UseContainers({ apiBaseUrl, parentId }) {
943
1456
  setLoading(false);
944
1457
  }
945
1458
  };
946
- useEffect2(() => {
1459
+ useEffect3(() => {
947
1460
  void load();
948
1461
  }, [apiBaseUrl, parentId]);
949
1462
  return {
@@ -956,11 +1469,12 @@ function UseContainers({ apiBaseUrl, parentId }) {
956
1469
  }
957
1470
 
958
1471
  // src/components/ContainerUploadPanel.tsx
959
- import { jsx as jsx5 } from "react/jsx-runtime";
1472
+ import { jsx as jsx8 } from "react/jsx-runtime";
960
1473
  var ContainerUploadPanel = ({
961
1474
  containerApiBaseUrl,
962
1475
  storageApiBaseUrl,
963
- parentContainerId
1476
+ parentContainerId,
1477
+ uploadRef
964
1478
  }) => {
965
1479
  const { containers, setContainers, reload, loading } = UseContainers({
966
1480
  apiBaseUrl: containerApiBaseUrl,
@@ -1015,9 +1529,10 @@ var ContainerUploadPanel = ({
1015
1529
  await sdkS3.s3.DeleteS3(file);
1016
1530
  setContainers((prev) => prev.filter((c) => c.Id !== file.Id));
1017
1531
  };
1018
- return /* @__PURE__ */ jsx5(
1532
+ return /* @__PURE__ */ jsx8(
1019
1533
  UploadContainer,
1020
1534
  {
1535
+ ref: uploadRef,
1021
1536
  existingFiles: containers,
1022
1537
  existingFilesLoading: loading,
1023
1538
  onExistingFileClick: handleExistingFileClick,
@@ -1030,9 +1545,172 @@ var ContainerUploadPanel = ({
1030
1545
  );
1031
1546
  };
1032
1547
 
1548
+ // src/components/FileGridUploadPanel.tsx
1549
+ import { useMemo as useMemo4, useState as useState7 } from "react";
1550
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
1551
+ function FileGridUploadPanel(props) {
1552
+ const {
1553
+ containerApiBaseUrl,
1554
+ storageApiBaseUrl,
1555
+ parentContainerId,
1556
+ accept = "*/*",
1557
+ multiple = true,
1558
+ selectedId,
1559
+ onSelect,
1560
+ icon,
1561
+ iconHtml,
1562
+ deleteDisabled
1563
+ } = props;
1564
+ const { containers, setContainers, reload, loading } = UseContainers({
1565
+ apiBaseUrl: containerApiBaseUrl,
1566
+ parentId: parentContainerId
1567
+ });
1568
+ const [isDragging, setIsDragging] = useState7(false);
1569
+ const [error, setError] = useState7(null);
1570
+ const sdkDb = useMemo4(() => new SparkStudioStorageSDK(containerApiBaseUrl), [containerApiBaseUrl]);
1571
+ const sdkS3 = useMemo4(() => new SparkStudioStorageSDK(storageApiBaseUrl), [storageApiBaseUrl]);
1572
+ const getPresignedUrl = async (file) => {
1573
+ const contentType = file.type || "application/octet-stream";
1574
+ const containerDTO = await sdkDb.container.CreateFileContainer(
1575
+ file.name,
1576
+ file.size,
1577
+ encodeURIComponent(contentType)
1578
+ );
1579
+ let lastError;
1580
+ for (let i = 1; i <= 3; i++) {
1581
+ try {
1582
+ return await sdkS3.s3.GetPreSignedUrl(containerDTO);
1583
+ } catch (e) {
1584
+ lastError = e;
1585
+ if (i < 3) await new Promise((r) => setTimeout(r, 500 * i));
1586
+ }
1587
+ }
1588
+ throw lastError instanceof Error ? lastError : new Error("Failed to fetch presigned URL");
1589
+ };
1590
+ const { uploads, startUploadsIfNeeded } = UseUploadManager({
1591
+ autoUpload: true,
1592
+ getPresignedUrl,
1593
+ onUploadComplete: async () => {
1594
+ setError(null);
1595
+ await reload();
1596
+ },
1597
+ onUploadError: (_file, err) => {
1598
+ setError(err.message ?? "Upload failed");
1599
+ }
1600
+ });
1601
+ const handleDeleteFile = async (file) => {
1602
+ const sdkDb2 = new SparkStudioStorageSDK(containerApiBaseUrl);
1603
+ const sdkS32 = new SparkStudioStorageSDK(storageApiBaseUrl);
1604
+ await sdkDb2.container.DeleteContainer(file.Id);
1605
+ await sdkS32.s3.DeleteS3(file);
1606
+ setContainers((prev) => prev.filter((c) => c.Id !== file.Id));
1607
+ };
1608
+ const openPicker = () => {
1609
+ const input = document.createElement("input");
1610
+ input.type = "file";
1611
+ input.multiple = multiple;
1612
+ input.accept = accept;
1613
+ input.onchange = () => {
1614
+ if (!input.files || input.files.length === 0) return;
1615
+ startUploadsIfNeeded(input.files);
1616
+ };
1617
+ input.click();
1618
+ };
1619
+ return /* @__PURE__ */ jsxs6("div", { style: { display: "grid", gap: 12 }, children: [
1620
+ /* @__PURE__ */ jsxs6("div", { children: [
1621
+ /* @__PURE__ */ jsx9(UploadProgressList, { uploads }),
1622
+ error ? /* @__PURE__ */ jsx9("div", { style: { fontSize: 12, color: "crimson", marginTop: 6 }, children: error }) : null
1623
+ ] }),
1624
+ /* @__PURE__ */ jsxs6(
1625
+ "div",
1626
+ {
1627
+ onDragEnter: (e) => {
1628
+ e.preventDefault();
1629
+ e.stopPropagation();
1630
+ setIsDragging(true);
1631
+ },
1632
+ onDragOver: (e) => {
1633
+ e.preventDefault();
1634
+ e.stopPropagation();
1635
+ setIsDragging(true);
1636
+ },
1637
+ onDragLeave: (e) => {
1638
+ e.preventDefault();
1639
+ e.stopPropagation();
1640
+ setIsDragging(false);
1641
+ },
1642
+ onDrop: (e) => {
1643
+ e.preventDefault();
1644
+ e.stopPropagation();
1645
+ setIsDragging(false);
1646
+ const files = e.dataTransfer.files;
1647
+ if (!files || files.length === 0) return;
1648
+ startUploadsIfNeeded(files);
1649
+ },
1650
+ style: {
1651
+ position: "relative",
1652
+ borderRadius: 14,
1653
+ border: isDragging ? "2px dashed rgba(13,110,253,0.9)" : "2px dashed rgba(0,0,0,0.15)",
1654
+ background: isDragging ? "rgba(13,110,253,0.06)" : "transparent",
1655
+ padding: 12
1656
+ },
1657
+ children: [
1658
+ isDragging ? /* @__PURE__ */ jsx9(
1659
+ "div",
1660
+ {
1661
+ style: {
1662
+ position: "absolute",
1663
+ inset: 0,
1664
+ borderRadius: 14,
1665
+ display: "grid",
1666
+ placeItems: "center",
1667
+ pointerEvents: "none",
1668
+ background: "rgba(13,110,253,0.08)",
1669
+ fontWeight: 800
1670
+ },
1671
+ children: "Drop files to upload"
1672
+ }
1673
+ ) : null,
1674
+ /* @__PURE__ */ jsxs6("div", { style: { display: "flex", justifyContent: "space-between", marginBottom: 10 }, children: [
1675
+ /* @__PURE__ */ jsx9("div", { style: { fontWeight: 700 }, children: "Files" }),
1676
+ /* @__PURE__ */ jsx9(
1677
+ "button",
1678
+ {
1679
+ type: "button",
1680
+ onClick: openPicker,
1681
+ style: {
1682
+ border: "1px solid rgba(0,0,0,0.18)",
1683
+ background: "white",
1684
+ borderRadius: 10,
1685
+ padding: "8px 10px",
1686
+ cursor: "pointer",
1687
+ fontWeight: 600
1688
+ },
1689
+ children: "Browse\u2026"
1690
+ }
1691
+ )
1692
+ ] }),
1693
+ 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(
1694
+ FileIconGrid,
1695
+ {
1696
+ files: containers,
1697
+ deleteDisabled,
1698
+ onDeleted: handleDeleteFile,
1699
+ selectedId,
1700
+ onSelect,
1701
+ icon,
1702
+ iconHtml
1703
+ }
1704
+ )
1705
+ ]
1706
+ }
1707
+ )
1708
+ ] });
1709
+ }
1710
+
1033
1711
  // 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";
1712
+ import { useCallback as useCallback2, useRef as useRef4, useState as useState8 } from "react";
1713
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
1036
1714
  var SingleFileProcessUploader = ({
1037
1715
  getPresignedUrl,
1038
1716
  onUploadComplete,
@@ -1041,11 +1719,11 @@ var SingleFileProcessUploader = ({
1041
1719
  disabled,
1042
1720
  uploadOnDrop = false
1043
1721
  }) => {
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);
1722
+ const [selectedFile, setSelectedFile] = useState8(null);
1723
+ const [isDragging, setIsDragging] = useState8(false);
1724
+ const [progress, setProgress] = useState8(null);
1725
+ const [status, setStatus] = useState8("idle");
1726
+ const fileInputRef = useRef4(null);
1049
1727
  function getErrorMessage(err) {
1050
1728
  if (err instanceof Error) return err.message;
1051
1729
  if (typeof err === "string") return err;
@@ -1131,31 +1809,18 @@ var SingleFileProcessUploader = ({
1131
1809
  disabled ? "opacity-50" : "cursor-pointer",
1132
1810
  isDragging ? "bg-body-secondary border-dashed border-2 border-secondary" : "bg-body-trasparent border-dashed border-2"
1133
1811
  ].join(" ");
1134
- return /* @__PURE__ */ jsxs4("div", { className: "d-flex flex-column gap-2", children: [
1135
- isUploading ? /* @__PURE__ */ jsxs4("div", { className: "small", children: [
1136
- /* @__PURE__ */ jsxs4("div", { className: "d-flex justify-content-between mb-1", children: [
1137
- /* @__PURE__ */ jsxs4("span", { children: [
1138
- /* @__PURE__ */ jsx6("div", { className: "spinner-border spinner-border-sm text-primary" }),
1139
- " ",
1140
- "Uploading..."
1141
- ] }),
1142
- /* @__PURE__ */ jsxs4("span", { children: [
1143
- progress ?? 0,
1144
- "%"
1145
- ] })
1146
- ] }),
1147
- /* @__PURE__ */ jsx6("div", { className: "progress", children: /* @__PURE__ */ jsx6(
1148
- "div",
1149
- {
1150
- className: "progress-bar progress-bar-striped progress-bar-animated",
1151
- role: "progressbar",
1152
- "aria-valuemin": 0,
1153
- "aria-valuemax": 100,
1154
- "aria-valuenow": progress ?? 0,
1155
- style: { width: `${progress ?? 0}%` }
1156
- }
1157
- ) })
1158
- ] }) : /* @__PURE__ */ jsxs4(
1812
+ return /* @__PURE__ */ jsxs7("div", { className: "d-flex flex-column gap-2", children: [
1813
+ isUploading ? /* @__PURE__ */ jsx10("div", { className: "small", children: /* @__PURE__ */ jsx10("div", { className: "progress", children: /* @__PURE__ */ jsx10(
1814
+ "div",
1815
+ {
1816
+ className: "progress-bar progress-bar-striped progress-bar-animated",
1817
+ role: "progressbar",
1818
+ "aria-valuemin": 0,
1819
+ "aria-valuemax": 100,
1820
+ "aria-valuenow": progress ?? 0,
1821
+ style: { width: `${progress ?? 0}%` }
1822
+ }
1823
+ ) }) }) : /* @__PURE__ */ jsxs7(
1159
1824
  "div",
1160
1825
  {
1161
1826
  className: dropzoneClasses,
@@ -1166,7 +1831,7 @@ var SingleFileProcessUploader = ({
1166
1831
  role: "button",
1167
1832
  "aria-disabled": disabled,
1168
1833
  children: [
1169
- /* @__PURE__ */ jsx6(
1834
+ /* @__PURE__ */ jsx10(
1170
1835
  "input",
1171
1836
  {
1172
1837
  ref: fileInputRef,
@@ -1177,8 +1842,8 @@ var SingleFileProcessUploader = ({
1177
1842
  disabled: disabled || isUploading
1178
1843
  }
1179
1844
  ),
1180
- selectedFile ? /* @__PURE__ */ jsxs4("div", { className: "small", children: [
1181
- /* @__PURE__ */ jsx6(
1845
+ selectedFile ? /* @__PURE__ */ jsxs7("div", { className: "small", children: [
1846
+ /* @__PURE__ */ jsx10(
1182
1847
  "button",
1183
1848
  {
1184
1849
  type: "button",
@@ -1187,15 +1852,15 @@ var SingleFileProcessUploader = ({
1187
1852
  children: "Browse file\u2026"
1188
1853
  }
1189
1854
  ),
1190
- /* @__PURE__ */ jsxs4("div", { children: [
1191
- /* @__PURE__ */ jsx6("strong", { children: "Selected file:" }),
1855
+ /* @__PURE__ */ jsxs7("div", { children: [
1856
+ /* @__PURE__ */ jsx10("strong", { children: "Selected file:" }),
1192
1857
  " ",
1193
1858
  selectedFile.name
1194
1859
  ] }),
1195
- /* @__PURE__ */ jsx6("div", { className: "text-muted", children: "Click here to change file or drag a new one." }),
1196
- uploadOnDrop && /* @__PURE__ */ jsx6("div", { className: "text-muted", children: "Upload starts automatically." })
1197
- ] }) : /* @__PURE__ */ jsxs4("div", { className: "small", children: [
1198
- /* @__PURE__ */ jsx6(
1860
+ /* @__PURE__ */ jsx10("div", { className: "text-muted", children: "Click here to change file or drag a new one." }),
1861
+ uploadOnDrop && /* @__PURE__ */ jsx10("div", { className: "text-muted", children: "Upload starts automatically." })
1862
+ ] }) : /* @__PURE__ */ jsxs7("div", { className: "small", children: [
1863
+ /* @__PURE__ */ jsx10(
1199
1864
  "button",
1200
1865
  {
1201
1866
  type: "button",
@@ -1204,13 +1869,13 @@ var SingleFileProcessUploader = ({
1204
1869
  children: "Browse file\u2026"
1205
1870
  }
1206
1871
  ),
1207
- /* @__PURE__ */ jsx6("div", { children: "Drag & drop a file here" }),
1208
- /* @__PURE__ */ jsx6("div", { className: "text-muted", children: "or click to browse" })
1872
+ /* @__PURE__ */ jsx10("div", { children: "Drag & drop a file here" }),
1873
+ /* @__PURE__ */ jsx10("div", { className: "text-muted", children: "or click to browse" })
1209
1874
  ] })
1210
1875
  ]
1211
1876
  }
1212
1877
  ),
1213
- !uploadOnDrop && /* @__PURE__ */ jsx6(
1878
+ !uploadOnDrop && /* @__PURE__ */ jsx10(
1214
1879
  "button",
1215
1880
  {
1216
1881
  type: "button",
@@ -1224,39 +1889,49 @@ var SingleFileProcessUploader = ({
1224
1889
  };
1225
1890
 
1226
1891
  // src/views/HomeView.tsx
1892
+ import { useEffect as useEffect4, useState as useState9 } from "react";
1227
1893
  import {
1228
1894
  AppSettings,
1229
1895
  AuthenticatorProvider,
1230
1896
  UserInfoCard,
1231
1897
  useUser
1232
1898
  } from "@sparkstudio/authentication-ui";
1233
- import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1899
+ import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
1900
+ var CONTAINER_API = "https://lf9zyufpuk.execute-api.us-east-2.amazonaws.com/Prod";
1901
+ var STORAGE_API = "https://iq0gmcn0pd.execute-api.us-east-2.amazonaws.com/Prod";
1234
1902
  function HomeView() {
1235
- return /* @__PURE__ */ jsx7(
1903
+ return /* @__PURE__ */ jsx11(
1236
1904
  AuthenticatorProvider,
1237
1905
  {
1238
1906
  googleClientId: AppSettings.GoogleClientId,
1239
1907
  authenticationUrl: AppSettings.AuthenticationUrl,
1240
1908
  accountsUrl: AppSettings.AccountsUrl,
1241
- children: /* @__PURE__ */ jsx7(HomeContent, {})
1909
+ children: /* @__PURE__ */ jsx11(HomeContent, {})
1242
1910
  }
1243
1911
  );
1244
1912
  }
1245
1913
  function HomeContent() {
1246
1914
  const { user } = useUser();
1915
+ const [ids, setIds] = useState9([]);
1916
+ const [selectedId, setSelectedId] = useState9(void 0);
1917
+ const [selectedFile, setSelectedFile] = useState9(null);
1918
+ useEffect4(() => {
1919
+ if (selectedId && !ids.includes(selectedId)) {
1920
+ setSelectedId(void 0);
1921
+ setSelectedFile(null);
1922
+ }
1923
+ }, [ids, selectedId]);
1247
1924
  async function getPresignedUrlFromApi(file) {
1248
- const res = new SparkStudioStorageSDK(
1249
- "https://iq0gmcn0pd.execute-api.us-east-2.amazonaws.com/Prod"
1250
- //"https://localhost:5001"
1251
- );
1925
+ const sdk = new SparkStudioStorageSDK(STORAGE_API);
1252
1926
  const contentType = file.type || "application/octet-stream";
1253
- const result = await res.s3.GetTemporaryPreSignedUrl(new TemporaryFileDTO({ Name: file.name, ContentType: contentType }));
1254
- return result;
1927
+ return sdk.s3.GetTemporaryPreSignedUrl(
1928
+ new TemporaryFileDTO({ Name: file.name, ContentType: contentType })
1929
+ );
1255
1930
  }
1256
- return /* @__PURE__ */ jsxs5(Fragment3, { children: [
1257
- /* @__PURE__ */ jsx7(UserInfoCard, {}),
1258
- user && /* @__PURE__ */ jsxs5(Fragment3, { children: [
1259
- /* @__PURE__ */ jsx7(
1931
+ return /* @__PURE__ */ jsxs8(Fragment3, { children: [
1932
+ /* @__PURE__ */ jsx11(UserInfoCard, {}),
1933
+ user ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
1934
+ /* @__PURE__ */ jsx11(
1260
1935
  SingleFileProcessUploader,
1261
1936
  {
1262
1937
  uploadOnDrop: true,
@@ -1267,23 +1942,52 @@ function HomeContent() {
1267
1942
  }
1268
1943
  }
1269
1944
  ),
1270
- /* @__PURE__ */ jsx7(
1271
- ContainerUploadPanel,
1945
+ /* @__PURE__ */ jsx11(
1946
+ ContainerIdGridPanel,
1272
1947
  {
1273
- containerApiBaseUrl: "https://lf9zyufpuk.execute-api.us-east-2.amazonaws.com/Prod",
1274
- storageApiBaseUrl: "https://iq0gmcn0pd.execute-api.us-east-2.amazonaws.com/Prod"
1948
+ containerApiBaseUrl: CONTAINER_API,
1949
+ storageApiBaseUrl: STORAGE_API,
1950
+ containerIds: ids,
1951
+ onContainerIdsChange: setIds,
1952
+ multiple: true,
1953
+ accept: "*/*",
1954
+ selectedId,
1955
+ onSelect: (file) => {
1956
+ setSelectedId(file.Id);
1957
+ setSelectedFile(file);
1958
+ }
1275
1959
  }
1276
- )
1277
- ] })
1960
+ ),
1961
+ selectedFile ? /* @__PURE__ */ jsxs8(
1962
+ "div",
1963
+ {
1964
+ style: {
1965
+ marginTop: 12,
1966
+ padding: 12,
1967
+ border: "1px solid rgba(0,0,0,0.12)",
1968
+ borderRadius: 12
1969
+ },
1970
+ children: [
1971
+ /* @__PURE__ */ jsx11("div", { style: { fontWeight: 700 }, children: "Selected file" }),
1972
+ /* @__PURE__ */ jsx11("div", { children: selectedFile.Name ?? "(no name)" }),
1973
+ /* @__PURE__ */ jsx11("div", { style: { fontSize: 12, opacity: 0.7 }, children: selectedFile.PublicUrl ?? "(no public url)" })
1974
+ ]
1975
+ }
1976
+ ) : null
1977
+ ] }) : null
1278
1978
  ] });
1279
1979
  }
1280
1980
  export {
1281
1981
  AWSPresignedUrlDTO,
1282
1982
  Container,
1283
1983
  ContainerDTO,
1984
+ ContainerIdGridPanel,
1284
1985
  ContainerType,
1285
1986
  ContainerUploadPanel,
1286
1987
  DesktopFileIcon,
1988
+ FileGridUploadPanel,
1989
+ FileIconCard,
1990
+ FileIconGrid,
1287
1991
  Home,
1288
1992
  HomeView,
1289
1993
  S3,