@sparkstudio/storage-ui 1.0.14 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -478,6 +478,14 @@ var UploadProgressList = ({
478
478
  },
479
479
  children: [
480
480
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("i", { className: "bi bi-file-earmark fs-2" }),
481
+ u.status === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
482
+ "div",
483
+ {
484
+ className: "position-absolute top-50 start-50 translate-middle",
485
+ style: { pointerEvents: "none" },
486
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "spinner-border spinner-border-sm text-primary" })
487
+ }
488
+ ),
481
489
  u.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger", children: "!" })
482
490
  ]
483
491
  }
@@ -518,8 +526,81 @@ var UploadProgressList = ({
518
526
  };
519
527
 
520
528
  // src/components/DesktopFileIcon.tsx
529
+ var import_free_solid_svg_icons = require("@fortawesome/free-solid-svg-icons");
530
+ var import_react_fontawesome = require("@fortawesome/react-fontawesome");
521
531
  var import_react4 = require("react");
522
532
  var import_jsx_runtime3 = require("react/jsx-runtime");
533
+ function getFileExtension(name) {
534
+ if (!name) return null;
535
+ const lastDot = name.lastIndexOf(".");
536
+ if (lastDot === -1 || lastDot === name.length - 1) return null;
537
+ return name.substring(lastDot + 1).toLowerCase();
538
+ }
539
+ function getIconForExtension(ext) {
540
+ if (!ext) return import_free_solid_svg_icons.faFile;
541
+ switch (ext) {
542
+ // Images
543
+ case "jpg":
544
+ case "jpeg":
545
+ case "png":
546
+ case "gif":
547
+ case "webp":
548
+ case "bmp":
549
+ return import_free_solid_svg_icons.faFileImage;
550
+ // PDF
551
+ case "pdf":
552
+ return import_free_solid_svg_icons.faFilePdf;
553
+ // Word docs
554
+ case "doc":
555
+ case "docx":
556
+ return import_free_solid_svg_icons.faFileWord;
557
+ // Excel / spreadsheets
558
+ case "xls":
559
+ case "xlsx":
560
+ case "csv":
561
+ return import_free_solid_svg_icons.faFileExcel;
562
+ // Archives
563
+ case "zip":
564
+ case "rar":
565
+ case "7z":
566
+ case "tar":
567
+ case "gz":
568
+ return import_free_solid_svg_icons.faFileArchive;
569
+ // Video
570
+ case "mp4":
571
+ case "mov":
572
+ case "avi":
573
+ case "mkv":
574
+ case "webm":
575
+ return import_free_solid_svg_icons.faFileVideo;
576
+ // Audio
577
+ case "mp3":
578
+ case "wav":
579
+ case "ogg":
580
+ case "flac":
581
+ return import_free_solid_svg_icons.faFileAudio;
582
+ // Code files
583
+ case "js":
584
+ case "ts":
585
+ case "tsx":
586
+ case "jsx":
587
+ case "json":
588
+ case "html":
589
+ case "css":
590
+ case "cs":
591
+ case "java":
592
+ case "py":
593
+ case "rb":
594
+ case "php":
595
+ case "sql":
596
+ case "xml":
597
+ case "yml":
598
+ case "yaml":
599
+ return import_free_solid_svg_icons.faFileCode;
600
+ default:
601
+ return import_free_solid_svg_icons.faFile;
602
+ }
603
+ }
523
604
  var DesktopFileIcon = ({
524
605
  name,
525
606
  sizeBytes,
@@ -531,9 +612,11 @@ var DesktopFileIcon = ({
531
612
  null
532
613
  );
533
614
  const [isHovered, setIsHovered] = (0, import_react4.useState)(false);
615
+ const [isDeleting, setIsDeleting] = (0, import_react4.useState)(false);
534
616
  const iconRef = (0, import_react4.useRef)(null);
535
617
  const menuRef = (0, import_react4.useRef)(null);
536
618
  const handleDoubleClick = () => {
619
+ if (isDeleting) return;
537
620
  if (onOpen) {
538
621
  onOpen();
539
622
  return;
@@ -550,13 +633,14 @@ var DesktopFileIcon = ({
550
633
  }
551
634
  };
552
635
  const handleContextMenu = (e) => {
636
+ if (isDeleting) return;
553
637
  e.preventDefault();
554
638
  setContextMenuPos({ x: e.clientX, y: e.clientY });
555
639
  };
556
640
  const closeMenu = () => setContextMenuPos(null);
557
641
  const handleDownload = () => {
558
642
  closeMenu();
559
- if (!downloadUrl) return;
643
+ if (!downloadUrl || isDeleting) return;
560
644
  const a = document.createElement("a");
561
645
  a.href = downloadUrl;
562
646
  a.download = name ?? "";
@@ -568,18 +652,26 @@ var DesktopFileIcon = ({
568
652
  };
569
653
  const handleCopyUrl = async () => {
570
654
  closeMenu();
571
- if (!downloadUrl) return;
655
+ if (!downloadUrl || isDeleting) return;
572
656
  try {
573
657
  await navigator.clipboard?.writeText(downloadUrl);
574
658
  } catch (err) {
575
659
  console.error("Failed to copy URL", err);
576
660
  }
577
661
  };
578
- const handleDelete = () => {
662
+ const handleDelete = async () => {
579
663
  closeMenu();
580
- onDelete?.();
664
+ if (!onDelete) return;
665
+ try {
666
+ setIsDeleting(true);
667
+ await Promise.resolve(onDelete());
668
+ } catch (err) {
669
+ console.error("Delete failed", err);
670
+ }
581
671
  };
582
672
  const formattedSize = typeof sizeBytes === "number" ? `${(sizeBytes / 1024).toFixed(1)} KB` : void 0;
673
+ const ext = getFileExtension(name);
674
+ const iconToRender = getIconForExtension(ext);
583
675
  (0, import_react4.useEffect)(() => {
584
676
  if (!contextMenuPos) return;
585
677
  const handleGlobalClick = (e) => {
@@ -602,48 +694,43 @@ var DesktopFileIcon = ({
602
694
  "div",
603
695
  {
604
696
  ref: iconRef,
605
- className: "d-flex flex-column align-items-center rounded-3 p-1 " + (isHovered || contextMenuPos ? "bg-light border" : ""),
697
+ className: "d-flex flex-column align-items-center rounded-3 p-1 " + (isHovered || contextMenuPos ? "bg-primary-subtle" : ""),
606
698
  style: {
607
699
  width: 96,
608
- cursor: "pointer",
700
+ cursor: isDeleting ? "default" : "pointer",
609
701
  userSelect: "none",
610
- transition: "background-color 0.1s ease, border-color 0.1s ease"
702
+ transition: "background-color 0.1s ease, opacity 0.1s ease",
703
+ opacity: isDeleting ? 0.6 : 1
611
704
  },
612
705
  onDoubleClick: handleDoubleClick,
613
706
  onContextMenu: handleContextMenu,
614
707
  title: name ?? void 0,
615
708
  onClick: () => {
616
- if (contextMenuPos) {
617
- closeMenu();
618
- }
709
+ if (contextMenuPos) closeMenu();
619
710
  },
620
711
  onMouseEnter: () => setIsHovered(true),
621
712
  onMouseLeave: () => setIsHovered(false),
622
713
  children: [
623
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
714
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
624
715
  "div",
625
716
  {
626
- className: "bg-white border rounded-3 d-flex align-items-center justify-content-center mb-1 shadow-sm",
717
+ className: "bg-white border rounded-3 d-flex align-items-center justify-content-center mb-1 shadow-sm position-relative",
627
718
  style: {
628
719
  width: 64,
629
720
  height: 64
630
721
  },
631
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("i", { className: "bi bi-file-earmark fs-2" })
632
- }
633
- ),
634
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
635
- "div",
636
- {
637
- className: "small text-center text-truncate",
638
- style: { width: "100%" },
639
- children: name
722
+ children: [
723
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_fontawesome.FontAwesomeIcon, { icon: iconToRender, className: "fs-2" }),
724
+ isDeleting && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "position-absolute top-50 start-50 translate-middle", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "spinner-border spinner-border-sm text-danger" }) })
725
+ ]
640
726
  }
641
727
  ),
728
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "small text-center text-truncate", style: { width: "100%" }, children: name }),
642
729
  formattedSize && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("small", { className: "text-muted mt-1", children: formattedSize })
643
730
  ]
644
731
  }
645
732
  ),
646
- contextMenuPos && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
733
+ contextMenuPos && !isDeleting && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
647
734
  "div",
648
735
  {
649
736
  ref: menuRef,
@@ -752,25 +839,59 @@ var UploadContainer = ({
752
839
  className: "w-100",
753
840
  style: { minHeight: "260px", alignItems: "stretch" },
754
841
  children: [
842
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "w-100 mb-3", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "d-flex flex-column flex-md-row align-items-start align-items-md-center justify-content-between gap-3", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex-grow-1 w-100", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(UploadProgressList, { uploads }) }) }) }),
755
843
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
756
844
  "div",
757
845
  {
758
846
  className: "w-100 d-flex flex-wrap gap-4 align-content-start",
759
847
  style: { minHeight: "140px" },
760
- children: existingFilesLoading ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "w-100 d-flex justify-content-center align-items-center", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "spinner-border text-secondary", role: "status", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "visually-hidden", children: "Loading containers..." }) }) }) : existingFiles.map((file) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
848
+ children: existingFilesLoading ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "w-100 d-flex justify-content-center align-items-center", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "spinner-border text-secondary", role: "status", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "visually-hidden", children: "Loading containers..." }) }) }) : existingFiles.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
849
+ "div",
850
+ {
851
+ className: "w-100 d-flex flex-column align-items-center justify-content-center text-muted",
852
+ style: {
853
+ minHeight: "160px",
854
+ border: "2px dashed #ccc",
855
+ borderRadius: "10px",
856
+ padding: "20px",
857
+ cursor: "pointer",
858
+ transition: "background 0.12s, border-color 0.12s"
859
+ },
860
+ onClick: () => document.getElementById("filePicker")?.click(),
861
+ onMouseEnter: (e) => e.currentTarget.style.borderColor = "#888",
862
+ onMouseLeave: (e) => e.currentTarget.style.borderColor = "#ccc",
863
+ children: [
864
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
865
+ "input",
866
+ {
867
+ id: "filePicker",
868
+ type: "file",
869
+ multiple: true,
870
+ hidden: true,
871
+ onChange: (e) => {
872
+ if (!e.target.files) return;
873
+ onFilesSelected?.(e.target.files);
874
+ startUploadsIfNeeded(e.target.files);
875
+ }
876
+ }
877
+ ),
878
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { children: "Drag & drop files here" }),
879
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("small", { className: "mt-1", children: "\u2026or click to browse" })
880
+ ]
881
+ }
882
+ ) : existingFiles.map((file) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
761
883
  DesktopFileIcon,
762
884
  {
763
885
  name: file.Name,
764
- sizeBytes: typeof file.FileSize === "number" ? file.FileSize : null,
765
- downloadUrl: file.PublicUrl ?? null,
886
+ sizeBytes: file.FileSize,
887
+ downloadUrl: file.PublicUrl,
766
888
  onOpen: () => handleExistingFileOpen(file),
767
- onDelete: onDeleteFile ? () => onDeleteFile(file) : void 0
889
+ onDelete: () => onDeleteFile?.(file)
768
890
  },
769
891
  file.Id
770
892
  ))
771
893
  }
772
- ),
773
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "w-100 mt-3", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "d-flex flex-column flex-md-row align-items-start align-items-md-center justify-content-between gap-3", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex-grow-1 w-100", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(UploadProgressList, { uploads }) }) }) })
894
+ )
774
895
  ]
775
896
  }
776
897
  );
@@ -802,6 +923,7 @@ function UseContainers({ apiBaseUrl, parentId }) {
802
923
  }, [apiBaseUrl, parentId]);
803
924
  return {
804
925
  containers,
926
+ setContainers,
805
927
  loading,
806
928
  error,
807
929
  reload: load
@@ -815,17 +937,18 @@ var ContainerUploadPanel = ({
815
937
  storageApiBaseUrl,
816
938
  parentContainerId
817
939
  }) => {
818
- const { containers, reload, loading } = UseContainers({
940
+ const { containers, setContainers, reload, loading } = UseContainers({
819
941
  apiBaseUrl: containerApiBaseUrl,
820
942
  parentId: parentContainerId
821
943
  });
822
944
  const getPresignedUrl = async (file) => {
823
945
  const sdkDb = new SparkStudioStorageSDK(containerApiBaseUrl);
824
946
  const sdkS3 = new SparkStudioStorageSDK(storageApiBaseUrl);
947
+ const contentType = file.type || "application/octet-stream";
825
948
  const containerDTO = await sdkDb.container.CreateFileContainer(
826
949
  file.name,
827
950
  file.size,
828
- encodeURIComponent(file.type)
951
+ encodeURIComponent(contentType)
829
952
  );
830
953
  async function getPresignedUrlWithRetry(container, attempts = 3) {
831
954
  let lastError;
@@ -850,13 +973,6 @@ var ContainerUploadPanel = ({
850
973
  const handleUploadError = (file, err) => {
851
974
  console.error("Upload failed:", file.name, err);
852
975
  };
853
- const handleOnDeleteFile = async (file) => {
854
- const sdkDb = new SparkStudioStorageSDK(containerApiBaseUrl);
855
- const sdkS3 = new SparkStudioStorageSDK(storageApiBaseUrl);
856
- await sdkDb.container.DeleteContainer(file.Id);
857
- await sdkS3.s3.DeleteS3(file);
858
- await reload();
859
- };
860
976
  const handleExistingFileClick = (file) => {
861
977
  const a = document.createElement("a");
862
978
  a.href = file.PublicUrl ?? "";
@@ -867,6 +983,13 @@ var ContainerUploadPanel = ({
867
983
  a.click();
868
984
  document.body.removeChild(a);
869
985
  };
986
+ const handleDeleteFile = async (file) => {
987
+ const sdkDb = new SparkStudioStorageSDK(containerApiBaseUrl);
988
+ const sdkS3 = new SparkStudioStorageSDK(storageApiBaseUrl);
989
+ await sdkDb.container.DeleteContainer(file.Id);
990
+ await sdkS3.s3.DeleteS3(file);
991
+ setContainers((prev) => prev.filter((c) => c.Id !== file.Id));
992
+ };
870
993
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
871
994
  UploadContainer,
872
995
  {
@@ -877,7 +1000,7 @@ var ContainerUploadPanel = ({
877
1000
  getPresignedUrl,
878
1001
  onUploadComplete: handleUploadComplete,
879
1002
  onUploadError: handleUploadError,
880
- onDeleteFile: handleOnDeleteFile
1003
+ onDeleteFile: handleDeleteFile
881
1004
  }
882
1005
  );
883
1006
  };
@@ -903,8 +1026,8 @@ function HomeContent() {
903
1026
  user && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
904
1027
  ContainerUploadPanel,
905
1028
  {
906
- containerApiBaseUrl: "https://localhost:5001",
907
- storageApiBaseUrl: "https://localhost:5001"
1029
+ containerApiBaseUrl: "https://lf9zyufpuk.execute-api.us-east-2.amazonaws.com/Prod",
1030
+ storageApiBaseUrl: "https://iq0gmcn0pd.execute-api.us-east-2.amazonaws.com/Prod"
908
1031
  }
909
1032
  )
910
1033
  ] });
package/dist/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
- import React from 'react';
1
+ import * as React from 'react';
2
+ import React__default from 'react';
2
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
4
 
4
5
  declare enum ContainerType {
@@ -102,7 +103,7 @@ interface ContainerUploadPanelProps {
102
103
  /** Optional parent container – if set, we query children instead of roots. */
103
104
  parentContainerId?: string;
104
105
  }
105
- declare const ContainerUploadPanel: React.FC<ContainerUploadPanelProps>;
106
+ declare const ContainerUploadPanel: React__default.FC<ContainerUploadPanelProps>;
106
107
 
107
108
  interface DesktopFileIconProps {
108
109
  name?: string | null;
@@ -110,10 +111,10 @@ interface DesktopFileIconProps {
110
111
  downloadUrl?: string | null;
111
112
  /** Double-click / open action */
112
113
  onOpen?: () => void;
113
- /** Delete action */
114
- onDelete?: () => void;
114
+ /** Delete action (can be async) */
115
+ onDelete?: () => Promise<void> | void;
115
116
  }
116
- declare const DesktopFileIcon: React.FC<DesktopFileIconProps>;
117
+ declare const DesktopFileIcon: React__default.FC<DesktopFileIconProps>;
117
118
 
118
119
  interface UploadContainerProps {
119
120
  multiple?: boolean;
@@ -129,20 +130,20 @@ interface UploadContainerProps {
129
130
  onUploadComplete?: (file: File, s3Url: string) => void;
130
131
  onUploadError?: (file: File, error: Error) => void;
131
132
  }
132
- declare const UploadContainer: React.FC<UploadContainerProps>;
133
+ declare const UploadContainer: React__default.FC<UploadContainerProps>;
133
134
 
134
135
  interface UploadDropzoneProps {
135
136
  isDragging: boolean;
136
- onDragOver?: (e: React.DragEvent<HTMLDivElement>) => void;
137
- onDragLeave?: (e: React.DragEvent<HTMLDivElement>) => void;
138
- onDrop?: (e: React.DragEvent<HTMLDivElement>) => void;
137
+ onDragOver?: (e: React__default.DragEvent<HTMLDivElement>) => void;
138
+ onDragLeave?: (e: React__default.DragEvent<HTMLDivElement>) => void;
139
+ onDrop?: (e: React__default.DragEvent<HTMLDivElement>) => void;
139
140
  /** Extra className so you can make this the root wrapper */
140
141
  className?: string;
141
- style?: React.CSSProperties;
142
+ style?: React__default.CSSProperties;
142
143
  /** Custom content to render inside the dropzone */
143
- children?: React.ReactNode;
144
+ children?: React__default.ReactNode;
144
145
  }
145
- declare const UploadDropzone: React.FC<UploadDropzoneProps>;
146
+ declare const UploadDropzone: React__default.FC<UploadDropzoneProps>;
146
147
 
147
148
  type UploadStatus = "pending" | "uploading" | "success" | "error";
148
149
  interface UploadState {
@@ -158,7 +159,7 @@ interface UploadState {
158
159
  interface UploadProgressListProps {
159
160
  uploads: UploadState[];
160
161
  }
161
- declare const UploadProgressList: React.FC<UploadProgressListProps>;
162
+ declare const UploadProgressList: React__default.FC<UploadProgressListProps>;
162
163
 
163
164
  /**
164
165
  * Helper: upload a file to a pre-signed S3 URL with progress + retries.
@@ -171,6 +172,7 @@ interface UseContainersOptions {
171
172
  }
172
173
  declare function UseContainers({ apiBaseUrl, parentId }: UseContainersOptions): {
173
174
  containers: ContainerDTO[];
175
+ setContainers: React.Dispatch<React.SetStateAction<ContainerDTO[]>>;
174
176
  loading: boolean;
175
177
  error: Error | null;
176
178
  reload: () => Promise<void>;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import React from 'react';
1
+ import * as React from 'react';
2
+ import React__default from 'react';
2
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
4
 
4
5
  declare enum ContainerType {
@@ -102,7 +103,7 @@ interface ContainerUploadPanelProps {
102
103
  /** Optional parent container – if set, we query children instead of roots. */
103
104
  parentContainerId?: string;
104
105
  }
105
- declare const ContainerUploadPanel: React.FC<ContainerUploadPanelProps>;
106
+ declare const ContainerUploadPanel: React__default.FC<ContainerUploadPanelProps>;
106
107
 
107
108
  interface DesktopFileIconProps {
108
109
  name?: string | null;
@@ -110,10 +111,10 @@ interface DesktopFileIconProps {
110
111
  downloadUrl?: string | null;
111
112
  /** Double-click / open action */
112
113
  onOpen?: () => void;
113
- /** Delete action */
114
- onDelete?: () => void;
114
+ /** Delete action (can be async) */
115
+ onDelete?: () => Promise<void> | void;
115
116
  }
116
- declare const DesktopFileIcon: React.FC<DesktopFileIconProps>;
117
+ declare const DesktopFileIcon: React__default.FC<DesktopFileIconProps>;
117
118
 
118
119
  interface UploadContainerProps {
119
120
  multiple?: boolean;
@@ -129,20 +130,20 @@ interface UploadContainerProps {
129
130
  onUploadComplete?: (file: File, s3Url: string) => void;
130
131
  onUploadError?: (file: File, error: Error) => void;
131
132
  }
132
- declare const UploadContainer: React.FC<UploadContainerProps>;
133
+ declare const UploadContainer: React__default.FC<UploadContainerProps>;
133
134
 
134
135
  interface UploadDropzoneProps {
135
136
  isDragging: boolean;
136
- onDragOver?: (e: React.DragEvent<HTMLDivElement>) => void;
137
- onDragLeave?: (e: React.DragEvent<HTMLDivElement>) => void;
138
- onDrop?: (e: React.DragEvent<HTMLDivElement>) => void;
137
+ onDragOver?: (e: React__default.DragEvent<HTMLDivElement>) => void;
138
+ onDragLeave?: (e: React__default.DragEvent<HTMLDivElement>) => void;
139
+ onDrop?: (e: React__default.DragEvent<HTMLDivElement>) => void;
139
140
  /** Extra className so you can make this the root wrapper */
140
141
  className?: string;
141
- style?: React.CSSProperties;
142
+ style?: React__default.CSSProperties;
142
143
  /** Custom content to render inside the dropzone */
143
- children?: React.ReactNode;
144
+ children?: React__default.ReactNode;
144
145
  }
145
- declare const UploadDropzone: React.FC<UploadDropzoneProps>;
146
+ declare const UploadDropzone: React__default.FC<UploadDropzoneProps>;
146
147
 
147
148
  type UploadStatus = "pending" | "uploading" | "success" | "error";
148
149
  interface UploadState {
@@ -158,7 +159,7 @@ interface UploadState {
158
159
  interface UploadProgressListProps {
159
160
  uploads: UploadState[];
160
161
  }
161
- declare const UploadProgressList: React.FC<UploadProgressListProps>;
162
+ declare const UploadProgressList: React__default.FC<UploadProgressListProps>;
162
163
 
163
164
  /**
164
165
  * Helper: upload a file to a pre-signed S3 URL with progress + retries.
@@ -171,6 +172,7 @@ interface UseContainersOptions {
171
172
  }
172
173
  declare function UseContainers({ apiBaseUrl, parentId }: UseContainersOptions): {
173
174
  containers: ContainerDTO[];
175
+ setContainers: React.Dispatch<React.SetStateAction<ContainerDTO[]>>;
174
176
  loading: boolean;
175
177
  error: Error | null;
176
178
  reload: () => Promise<void>;
package/dist/index.js CHANGED
@@ -437,6 +437,14 @@ var UploadProgressList = ({
437
437
  },
438
438
  children: [
439
439
  /* @__PURE__ */ jsx2("i", { className: "bi bi-file-earmark fs-2" }),
440
+ u.status === "uploading" && /* @__PURE__ */ jsx2(
441
+ "div",
442
+ {
443
+ className: "position-absolute top-50 start-50 translate-middle",
444
+ style: { pointerEvents: "none" },
445
+ children: /* @__PURE__ */ jsx2("div", { className: "spinner-border spinner-border-sm text-primary" })
446
+ }
447
+ ),
440
448
  u.status === "error" && /* @__PURE__ */ jsx2("span", { className: "position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger", children: "!" })
441
449
  ]
442
450
  }
@@ -477,8 +485,91 @@ var UploadProgressList = ({
477
485
  };
478
486
 
479
487
  // src/components/DesktopFileIcon.tsx
488
+ import {
489
+ faFile,
490
+ faFilePdf,
491
+ faFileImage,
492
+ faFileWord,
493
+ faFileExcel,
494
+ faFileVideo,
495
+ faFileAudio,
496
+ faFileArchive,
497
+ faFileCode
498
+ } from "@fortawesome/free-solid-svg-icons";
499
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
480
500
  import { useEffect, useRef, useState as useState2 } from "react";
481
501
  import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
502
+ function getFileExtension(name) {
503
+ if (!name) return null;
504
+ const lastDot = name.lastIndexOf(".");
505
+ if (lastDot === -1 || lastDot === name.length - 1) return null;
506
+ return name.substring(lastDot + 1).toLowerCase();
507
+ }
508
+ function getIconForExtension(ext) {
509
+ if (!ext) return faFile;
510
+ switch (ext) {
511
+ // Images
512
+ case "jpg":
513
+ case "jpeg":
514
+ case "png":
515
+ case "gif":
516
+ case "webp":
517
+ case "bmp":
518
+ return faFileImage;
519
+ // PDF
520
+ case "pdf":
521
+ return faFilePdf;
522
+ // Word docs
523
+ case "doc":
524
+ case "docx":
525
+ return faFileWord;
526
+ // Excel / spreadsheets
527
+ case "xls":
528
+ case "xlsx":
529
+ case "csv":
530
+ return faFileExcel;
531
+ // Archives
532
+ case "zip":
533
+ case "rar":
534
+ case "7z":
535
+ case "tar":
536
+ case "gz":
537
+ return faFileArchive;
538
+ // Video
539
+ case "mp4":
540
+ case "mov":
541
+ case "avi":
542
+ case "mkv":
543
+ case "webm":
544
+ return faFileVideo;
545
+ // Audio
546
+ case "mp3":
547
+ case "wav":
548
+ case "ogg":
549
+ case "flac":
550
+ return faFileAudio;
551
+ // Code files
552
+ case "js":
553
+ case "ts":
554
+ case "tsx":
555
+ case "jsx":
556
+ case "json":
557
+ case "html":
558
+ case "css":
559
+ case "cs":
560
+ case "java":
561
+ case "py":
562
+ case "rb":
563
+ case "php":
564
+ case "sql":
565
+ case "xml":
566
+ case "yml":
567
+ case "yaml":
568
+ return faFileCode;
569
+ default:
570
+ return faFile;
571
+ }
572
+ }
482
573
  var DesktopFileIcon = ({
483
574
  name,
484
575
  sizeBytes,
@@ -490,9 +581,11 @@ var DesktopFileIcon = ({
490
581
  null
491
582
  );
492
583
  const [isHovered, setIsHovered] = useState2(false);
584
+ const [isDeleting, setIsDeleting] = useState2(false);
493
585
  const iconRef = useRef(null);
494
586
  const menuRef = useRef(null);
495
587
  const handleDoubleClick = () => {
588
+ if (isDeleting) return;
496
589
  if (onOpen) {
497
590
  onOpen();
498
591
  return;
@@ -509,13 +602,14 @@ var DesktopFileIcon = ({
509
602
  }
510
603
  };
511
604
  const handleContextMenu = (e) => {
605
+ if (isDeleting) return;
512
606
  e.preventDefault();
513
607
  setContextMenuPos({ x: e.clientX, y: e.clientY });
514
608
  };
515
609
  const closeMenu = () => setContextMenuPos(null);
516
610
  const handleDownload = () => {
517
611
  closeMenu();
518
- if (!downloadUrl) return;
612
+ if (!downloadUrl || isDeleting) return;
519
613
  const a = document.createElement("a");
520
614
  a.href = downloadUrl;
521
615
  a.download = name ?? "";
@@ -527,18 +621,26 @@ var DesktopFileIcon = ({
527
621
  };
528
622
  const handleCopyUrl = async () => {
529
623
  closeMenu();
530
- if (!downloadUrl) return;
624
+ if (!downloadUrl || isDeleting) return;
531
625
  try {
532
626
  await navigator.clipboard?.writeText(downloadUrl);
533
627
  } catch (err) {
534
628
  console.error("Failed to copy URL", err);
535
629
  }
536
630
  };
537
- const handleDelete = () => {
631
+ const handleDelete = async () => {
538
632
  closeMenu();
539
- onDelete?.();
633
+ if (!onDelete) return;
634
+ try {
635
+ setIsDeleting(true);
636
+ await Promise.resolve(onDelete());
637
+ } catch (err) {
638
+ console.error("Delete failed", err);
639
+ }
540
640
  };
541
641
  const formattedSize = typeof sizeBytes === "number" ? `${(sizeBytes / 1024).toFixed(1)} KB` : void 0;
642
+ const ext = getFileExtension(name);
643
+ const iconToRender = getIconForExtension(ext);
542
644
  useEffect(() => {
543
645
  if (!contextMenuPos) return;
544
646
  const handleGlobalClick = (e) => {
@@ -561,48 +663,43 @@ var DesktopFileIcon = ({
561
663
  "div",
562
664
  {
563
665
  ref: iconRef,
564
- className: "d-flex flex-column align-items-center rounded-3 p-1 " + (isHovered || contextMenuPos ? "bg-light border" : ""),
666
+ className: "d-flex flex-column align-items-center rounded-3 p-1 " + (isHovered || contextMenuPos ? "bg-primary-subtle" : ""),
565
667
  style: {
566
668
  width: 96,
567
- cursor: "pointer",
669
+ cursor: isDeleting ? "default" : "pointer",
568
670
  userSelect: "none",
569
- transition: "background-color 0.1s ease, border-color 0.1s ease"
671
+ transition: "background-color 0.1s ease, opacity 0.1s ease",
672
+ opacity: isDeleting ? 0.6 : 1
570
673
  },
571
674
  onDoubleClick: handleDoubleClick,
572
675
  onContextMenu: handleContextMenu,
573
676
  title: name ?? void 0,
574
677
  onClick: () => {
575
- if (contextMenuPos) {
576
- closeMenu();
577
- }
678
+ if (contextMenuPos) closeMenu();
578
679
  },
579
680
  onMouseEnter: () => setIsHovered(true),
580
681
  onMouseLeave: () => setIsHovered(false),
581
682
  children: [
582
- /* @__PURE__ */ jsx3(
683
+ /* @__PURE__ */ jsxs2(
583
684
  "div",
584
685
  {
585
- className: "bg-white border rounded-3 d-flex align-items-center justify-content-center mb-1 shadow-sm",
686
+ className: "bg-white border rounded-3 d-flex align-items-center justify-content-center mb-1 shadow-sm position-relative",
586
687
  style: {
587
688
  width: 64,
588
689
  height: 64
589
690
  },
590
- children: /* @__PURE__ */ jsx3("i", { className: "bi bi-file-earmark fs-2" })
591
- }
592
- ),
593
- /* @__PURE__ */ jsx3(
594
- "div",
595
- {
596
- className: "small text-center text-truncate",
597
- style: { width: "100%" },
598
- children: name
691
+ children: [
692
+ /* @__PURE__ */ jsx3(FontAwesomeIcon, { icon: iconToRender, className: "fs-2" }),
693
+ 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" }) })
694
+ ]
599
695
  }
600
696
  ),
697
+ /* @__PURE__ */ jsx3("div", { className: "small text-center text-truncate", style: { width: "100%" }, children: name }),
601
698
  formattedSize && /* @__PURE__ */ jsx3("small", { className: "text-muted mt-1", children: formattedSize })
602
699
  ]
603
700
  }
604
701
  ),
605
- contextMenuPos && /* @__PURE__ */ jsxs2(
702
+ contextMenuPos && !isDeleting && /* @__PURE__ */ jsxs2(
606
703
  "div",
607
704
  {
608
705
  ref: menuRef,
@@ -711,25 +808,59 @@ var UploadContainer = ({
711
808
  className: "w-100",
712
809
  style: { minHeight: "260px", alignItems: "stretch" },
713
810
  children: [
811
+ /* @__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 }) }) }) }),
714
812
  /* @__PURE__ */ jsx4(
715
813
  "div",
716
814
  {
717
815
  className: "w-100 d-flex flex-wrap gap-4 align-content-start",
718
816
  style: { minHeight: "140px" },
719
- children: existingFilesLoading ? /* @__PURE__ */ jsx4("div", { className: "w-100 d-flex justify-content-center align-items-center", children: /* @__PURE__ */ jsx4("div", { className: "spinner-border text-secondary", role: "status", children: /* @__PURE__ */ jsx4("span", { className: "visually-hidden", children: "Loading containers..." }) }) }) : existingFiles.map((file) => /* @__PURE__ */ jsx4(
817
+ children: existingFilesLoading ? /* @__PURE__ */ jsx4("div", { className: "w-100 d-flex justify-content-center align-items-center", children: /* @__PURE__ */ jsx4("div", { className: "spinner-border text-secondary", role: "status", children: /* @__PURE__ */ jsx4("span", { className: "visually-hidden", children: "Loading containers..." }) }) }) : existingFiles.length === 0 ? /* @__PURE__ */ jsxs3(
818
+ "div",
819
+ {
820
+ className: "w-100 d-flex flex-column align-items-center justify-content-center text-muted",
821
+ style: {
822
+ minHeight: "160px",
823
+ border: "2px dashed #ccc",
824
+ borderRadius: "10px",
825
+ padding: "20px",
826
+ cursor: "pointer",
827
+ transition: "background 0.12s, border-color 0.12s"
828
+ },
829
+ onClick: () => document.getElementById("filePicker")?.click(),
830
+ onMouseEnter: (e) => e.currentTarget.style.borderColor = "#888",
831
+ onMouseLeave: (e) => e.currentTarget.style.borderColor = "#ccc",
832
+ children: [
833
+ /* @__PURE__ */ jsx4(
834
+ "input",
835
+ {
836
+ id: "filePicker",
837
+ type: "file",
838
+ multiple: true,
839
+ hidden: true,
840
+ onChange: (e) => {
841
+ if (!e.target.files) return;
842
+ onFilesSelected?.(e.target.files);
843
+ startUploadsIfNeeded(e.target.files);
844
+ }
845
+ }
846
+ ),
847
+ /* @__PURE__ */ jsx4("strong", { children: "Drag & drop files here" }),
848
+ /* @__PURE__ */ jsx4("small", { className: "mt-1", children: "\u2026or click to browse" })
849
+ ]
850
+ }
851
+ ) : existingFiles.map((file) => /* @__PURE__ */ jsx4(
720
852
  DesktopFileIcon,
721
853
  {
722
854
  name: file.Name,
723
- sizeBytes: typeof file.FileSize === "number" ? file.FileSize : null,
724
- downloadUrl: file.PublicUrl ?? null,
855
+ sizeBytes: file.FileSize,
856
+ downloadUrl: file.PublicUrl,
725
857
  onOpen: () => handleExistingFileOpen(file),
726
- onDelete: onDeleteFile ? () => onDeleteFile(file) : void 0
858
+ onDelete: () => onDeleteFile?.(file)
727
859
  },
728
860
  file.Id
729
861
  ))
730
862
  }
731
- ),
732
- /* @__PURE__ */ jsx4("div", { className: "w-100 mt-3", children: /* @__PURE__ */ jsx4("div", { className: "d-flex flex-column flex-md-row align-items-start align-items-md-center justify-content-between gap-3", children: /* @__PURE__ */ jsx4("div", { className: "flex-grow-1 w-100", children: /* @__PURE__ */ jsx4(UploadProgressList, { uploads }) }) }) })
863
+ )
733
864
  ]
734
865
  }
735
866
  );
@@ -761,6 +892,7 @@ function UseContainers({ apiBaseUrl, parentId }) {
761
892
  }, [apiBaseUrl, parentId]);
762
893
  return {
763
894
  containers,
895
+ setContainers,
764
896
  loading,
765
897
  error,
766
898
  reload: load
@@ -774,17 +906,18 @@ var ContainerUploadPanel = ({
774
906
  storageApiBaseUrl,
775
907
  parentContainerId
776
908
  }) => {
777
- const { containers, reload, loading } = UseContainers({
909
+ const { containers, setContainers, reload, loading } = UseContainers({
778
910
  apiBaseUrl: containerApiBaseUrl,
779
911
  parentId: parentContainerId
780
912
  });
781
913
  const getPresignedUrl = async (file) => {
782
914
  const sdkDb = new SparkStudioStorageSDK(containerApiBaseUrl);
783
915
  const sdkS3 = new SparkStudioStorageSDK(storageApiBaseUrl);
916
+ const contentType = file.type || "application/octet-stream";
784
917
  const containerDTO = await sdkDb.container.CreateFileContainer(
785
918
  file.name,
786
919
  file.size,
787
- encodeURIComponent(file.type)
920
+ encodeURIComponent(contentType)
788
921
  );
789
922
  async function getPresignedUrlWithRetry(container, attempts = 3) {
790
923
  let lastError;
@@ -809,13 +942,6 @@ var ContainerUploadPanel = ({
809
942
  const handleUploadError = (file, err) => {
810
943
  console.error("Upload failed:", file.name, err);
811
944
  };
812
- const handleOnDeleteFile = async (file) => {
813
- const sdkDb = new SparkStudioStorageSDK(containerApiBaseUrl);
814
- const sdkS3 = new SparkStudioStorageSDK(storageApiBaseUrl);
815
- await sdkDb.container.DeleteContainer(file.Id);
816
- await sdkS3.s3.DeleteS3(file);
817
- await reload();
818
- };
819
945
  const handleExistingFileClick = (file) => {
820
946
  const a = document.createElement("a");
821
947
  a.href = file.PublicUrl ?? "";
@@ -826,6 +952,13 @@ var ContainerUploadPanel = ({
826
952
  a.click();
827
953
  document.body.removeChild(a);
828
954
  };
955
+ const handleDeleteFile = async (file) => {
956
+ const sdkDb = new SparkStudioStorageSDK(containerApiBaseUrl);
957
+ const sdkS3 = new SparkStudioStorageSDK(storageApiBaseUrl);
958
+ await sdkDb.container.DeleteContainer(file.Id);
959
+ await sdkS3.s3.DeleteS3(file);
960
+ setContainers((prev) => prev.filter((c) => c.Id !== file.Id));
961
+ };
829
962
  return /* @__PURE__ */ jsx5(
830
963
  UploadContainer,
831
964
  {
@@ -836,7 +969,7 @@ var ContainerUploadPanel = ({
836
969
  getPresignedUrl,
837
970
  onUploadComplete: handleUploadComplete,
838
971
  onUploadError: handleUploadError,
839
- onDeleteFile: handleOnDeleteFile
972
+ onDeleteFile: handleDeleteFile
840
973
  }
841
974
  );
842
975
  };
@@ -867,8 +1000,8 @@ function HomeContent() {
867
1000
  user && /* @__PURE__ */ jsx6(
868
1001
  ContainerUploadPanel,
869
1002
  {
870
- containerApiBaseUrl: "https://localhost:5001",
871
- storageApiBaseUrl: "https://localhost:5001"
1003
+ containerApiBaseUrl: "https://lf9zyufpuk.execute-api.us-east-2.amazonaws.com/Prod",
1004
+ storageApiBaseUrl: "https://iq0gmcn0pd.execute-api.us-east-2.amazonaws.com/Prod"
872
1005
  }
873
1006
  )
874
1007
  ] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sparkstudio/storage-ui",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
@@ -37,8 +37,10 @@
37
37
  "dependencies": {
38
38
  "@aws-sdk/client-s3": "^3.958.0",
39
39
  "@aws-sdk/s3-request-presigner": "^3.958.0",
40
+ "@fortawesome/free-solid-svg-icons": "^7.1.0",
41
+ "@fortawesome/react-fontawesome": "^3.1.1",
40
42
  "@sparkstudio/authentication-ui": "^1.0.29",
41
- "@sparkstudio/common-ui": "^1.0.5",
43
+ "@sparkstudio/common-ui": "^1.0.11",
42
44
  "barrelsby": "^2.8.1"
43
45
  },
44
46
  "devDependencies": {