@putkoff/abstract-securefiles 0.0.47 → 0.0.48

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
@@ -844,12 +844,45 @@ function FileTable({ files, loading, error, onToggleShare, onPatchShare, onDelet
844
844
  } })] }));
845
845
  }
846
846
 
847
+ // ── helpers ─────────────────────────────────────────────────────────────────
848
+ /** Strip filename from a webkitRelativePath, keep the trailing slash. */
849
+ function deriveRelPath(f) {
850
+ const wrp = f.webkitRelativePath;
851
+ if (!wrp)
852
+ return '';
853
+ const idx = wrp.lastIndexOf('/');
854
+ return idx >= 0 ? wrp.slice(0, idx + 1) : '';
855
+ }
856
+ /**
857
+ * Drain a DirectoryReader completely. The browser only returns ~100 entries
858
+ * per readEntries() call; you must call it repeatedly until it returns [].
859
+ */
860
+ function readAllEntries(reader) {
861
+ return new Promise((resolve, reject) => {
862
+ const all = [];
863
+ const pump = () => {
864
+ reader.readEntries((batch) => {
865
+ if (batch.length === 0)
866
+ resolve(all);
867
+ else {
868
+ all.push(...batch);
869
+ pump();
870
+ }
871
+ }, reject);
872
+ };
873
+ pump();
874
+ });
875
+ }
876
+ // ── component ───────────────────────────────────────────────────────────────
847
877
  function Upload({ onUploaded }) {
848
878
  const [queue, setQueue] = useState([]);
849
879
  const [busy, setBusy] = useState(false);
850
880
  const [message, setMessage] = useState(null);
851
881
  const [drag, setDrag] = useState(false);
852
- const inputRef = useRef(null);
882
+ // Two refs: one for plain file picking, one for folder picking.
883
+ const fileInputRef = useRef(null);
884
+ const dirInputRef = useRef(null);
885
+ const [pickMode, setPickMode] = useState('files');
853
886
  const [dirs, setDirs] = useState([]);
854
887
  // — existing files → directory list —
855
888
  const { files, load } = useFiles();
@@ -869,56 +902,76 @@ function Upload({ onUploaded }) {
869
902
  const [creatingNew, setCreatingNew] = useState(false);
870
903
  const [newDirName, setNewDirName] = useState('');
871
904
  // — queue logic —
872
- const enqueue = useCallback((files) => {
905
+ const enqueue = useCallback((incoming) => {
873
906
  setQueue(prev => {
874
907
  const map = new Map(prev.map(q => [`${q.file.name}-${q.file.size}`, q]));
875
- files.forEach(q => map.set(`${q.file.name}-${q.file.size}`, q));
908
+ incoming.forEach(q => map.set(`${q.file.name}-${q.file.size}`, q));
876
909
  return Array.from(map.values());
877
910
  });
878
911
  }, []);
879
912
  const extractFromDataTransfer = useCallback(async (dt) => {
913
+ const items = Array.from(dt.items ?? []);
914
+ const hasEntryApi = items.length > 0 &&
915
+ typeof items[0].webkitGetAsEntry === 'function';
916
+ // Fallback: no entry API → flat files only, no folder traversal.
917
+ if (!hasEntryApi) {
918
+ return Array.from(dt.files ?? []).map(file => ({
919
+ file,
920
+ relPath: '',
921
+ progress: 0,
922
+ }));
923
+ }
880
924
  const out = [];
881
- const walk = async (entry, path = "") => {
925
+ const walk = async (entry, path = '') => {
882
926
  if (!entry)
883
927
  return;
884
928
  if (entry.isFile) {
885
- const file = await new Promise(res => entry.file(res));
929
+ const file = await new Promise((res, rej) => entry.file(res, rej));
886
930
  out.push({ file, relPath: path, progress: 0 });
931
+ return;
887
932
  }
888
- else if (entry.isDirectory) {
933
+ if (entry.isDirectory) {
889
934
  const reader = entry.createReader();
890
- const entries = await new Promise(r => reader.readEntries(r));
935
+ const entries = await readAllEntries(reader);
891
936
  await Promise.all(entries.map(e => walk(e, `${path}${entry.name}/`)));
892
937
  }
893
938
  };
894
- await Promise.all(Array.from(dt.items).map(i => walk(i.webkitGetAsEntry())));
939
+ await Promise.all(items.map(i => walk(i.webkitGetAsEntry?.())));
895
940
  return out;
896
941
  }, []);
897
942
  const handleSelect = (e) => {
898
- const files = Array.from(e.target.files || []);
899
- const queued = files.map(f => ({
943
+ const picked = Array.from(e.target.files ?? []);
944
+ const queued = picked.map(f => ({
900
945
  file: f,
901
- relPath: f.webkitRelativePath.replace(/[^/]*$/, ""),
946
+ relPath: deriveRelPath(f),
902
947
  progress: 0,
903
948
  }));
904
949
  enqueue(queued);
905
- e.target.value = "";
950
+ e.target.value = '';
906
951
  };
907
952
  const handleDrop = async (e) => {
908
953
  e.preventDefault();
909
- const files = await extractFromDataTransfer(e.dataTransfer);
910
- enqueue(files.length ? files : []);
954
+ e.stopPropagation();
955
+ const dropped = await extractFromDataTransfer(e.dataTransfer);
956
+ if (dropped.length)
957
+ enqueue(dropped);
911
958
  setDrag(false);
912
959
  };
960
+ const openPicker = (e) => {
961
+ // Only fire when the dropzone background itself is clicked, not children.
962
+ if (e.target !== e.currentTarget && !e.target.closest('[data-dz-passthrough]')) ;
963
+ const ref = pickMode === 'folder' ? dirInputRef : fileInputRef;
964
+ ref.current?.click();
965
+ };
913
966
  const handleUploadAll = async () => {
914
967
  if (!queue.length) {
915
- setMessage("No files queued.");
968
+ setMessage('No files queued.');
916
969
  return;
917
970
  }
918
971
  setBusy(true);
919
972
  setMessage(null);
920
973
  const targetDir = creatingNew
921
- ? (newDirName.trim().replace(/^\/+|\/+$/g, "") + "/")
974
+ ? (newDirName.trim().replace(/^\/+|\/+$/g, '') + '/')
922
975
  : uploadDir;
923
976
  try {
924
977
  for (const { file, relPath } of queue) {
@@ -926,12 +979,12 @@ function Upload({ onUploaded }) {
926
979
  setQueue(cur => cur.map(item => item.file === file ? { ...item, progress: 100 } : item));
927
980
  }
928
981
  setQueue([]);
929
- await onUploaded();
930
- setMessage("Upload complete");
931
- load();
982
+ await onUploaded(); // SecureFiles passes filesHook.load here
983
+ setMessage('Upload complete');
984
+ // (removed the duplicate load() — onUploaded already does it)
932
985
  }
933
986
  catch {
934
- setMessage("Upload failed");
987
+ setMessage('Upload failed');
935
988
  }
936
989
  finally {
937
990
  setBusy(false);
@@ -940,9 +993,9 @@ function Upload({ onUploaded }) {
940
993
  // — directory selector handlers —
941
994
  const handleDirChange = (e) => {
942
995
  const v = e.target.value;
943
- if (v === "__new__") {
996
+ if (v === '__new__') {
944
997
  setCreatingNew(true);
945
- setNewDirName("");
998
+ setNewDirName('');
946
999
  }
947
1000
  else {
948
1001
  setCreatingNew(false);
@@ -950,22 +1003,19 @@ function Upload({ onUploaded }) {
950
1003
  }
951
1004
  };
952
1005
  const acceptNewDir = () => {
953
- // normalize like before
954
1006
  const cleanName = newDirName.trim().replace(/^\/+|\/+$/g, '');
1007
+ if (!cleanName) {
1008
+ setCreatingNew(false);
1009
+ return;
1010
+ }
955
1011
  const normalized = `${uploadDir}${cleanName}/`;
956
- // ① add it to our dirs list if it isn't there already
957
- setDirs(prev => {
958
- if (prev.includes(normalized))
959
- return prev;
960
- return [...prev, normalized].sort();
961
- });
962
- // ② select it & flip back to the <select>
1012
+ setDirs(prev => prev.includes(normalized) ? prev : [...prev, normalized].sort());
963
1013
  setUploadDir(normalized);
964
1014
  setCreatingNew(false);
965
1015
  setNewDirName('');
966
1016
  };
967
- return (jsxs("div", { className: "form-container max-w-2xl mx-auto", children: [jsxs("div", { className: `flex items-center border-2 border-dashed rounded-lg p-6 bg-white shadow-md space-x-4 transition-shadow duration-150
968
- ${drag ? 'border-indigo-500 bg-indigo-50 shadow-lg' : 'border-gray-300 hover:shadow-lg'}`, onClick: () => inputRef.current?.click(), onDragEnter: e => { e.preventDefault(); setDrag(true); }, onDragOver: e => e.preventDefault(), onDragLeave: () => setDrag(false), onDrop: handleDrop, role: "region", "aria-label": "File upload area", children: [jsx(ForwardRef, { style: { width: 300, height: 300 }, className: "text-gray-500 mb-3", "aria-hidden": "true" }), jsxs("div", { className: "flex-1", children: [jsx("p", { className: "text-base font-semibold text-gray-700", children: "Drag & drop files or folders here" }), queue.length > 0 && (jsxs("p", { className: "mt-1 text-sm text-gray-600", children: [queue.length, " item", queue.length > 1 && 's', " queued"] }))] })] }), jsx("input", { ref: inputRef, type: "file", multiple: true, className: "hidden", ...{ webkitdirectory: true, directory: '' }, onChange: handleSelect }), jsxs("div", { className: "flex items-center justify-between mt-4", children: [jsx("div", { className: "flex-1 mr-4", children: !creatingNew ? (jsxs("select", { value: uploadDir, onChange: handleDirChange, className: "form-select w-full", children: [dirs.map(d => (jsx("option", { value: d, children: d }, d))), jsx("option", { value: "__new__", children: "+ Create new directory\u2026" })] })) : (jsxs("div", { className: "flex", children: [jsx("input", { type: "text", value: newDirName, placeholder: "new-folder-name", onChange: e => setNewDirName(e.target.value), onBlur: acceptNewDir, onKeyDown: e => e.key === 'Enter' && acceptNewDir(), className: "form-input flex-1" }), jsx(Button, { onClick: acceptNewDir, className: "ml-2", children: "OK" })] })) }), jsx(Button, { color: "blue", variant: "primary", onClick: handleUploadAll, disabled: busy || !queue.length, children: busy ? 'Uploading…' : `Upload ${queue.length}` })] }), message && (jsx("p", { className: `text-center text-sm ${message.includes('failed') ? 'text-red-600' : 'text-gray-600'}`, role: "alert", children: message })), queue.length > 0 && (jsxs("div", { className: "wm-container mt-4", children: [jsx("h3", { className: "mb-2 text-sm font-semibold text-gray-700", children: "Queued Files" }), jsx("ul", { className: "max-h-64 space-y-4 overflow-y-auto", children: queue.map(({ file, relPath, progress }) => (jsxs("li", { className: "space-y-1", children: [jsxs("div", { className: "flex justify-between items-center", children: [jsxs("span", { className: "truncate text-sm text-gray-800", children: [relPath, file.name] }), jsxs("span", { className: "text-xs text-gray-500", children: [progress, "%"] })] }), jsx("div", { className: "w-full bg-gray-200 rounded-full h-2 overflow-hidden", children: jsx("div", { className: "bg-indigo-600 h-2", style: { width: `${progress}%` } }) })] }, `${file.name}-${file.size}`))) })] }))] }));
1017
+ return (jsxs("div", { className: "form-container max-w-2xl mx-auto", children: [jsxs("div", { className: "flex gap-4 text-xs text-gray-700 mb-1", children: [jsxs("label", { className: "flex items-center gap-1", children: [jsx("input", { type: "radio", name: "pick-mode", checked: pickMode === 'files', onChange: () => setPickMode('files') }), "Files"] }), jsxs("label", { className: "flex items-center gap-1", children: [jsx("input", { type: "radio", name: "pick-mode", checked: pickMode === 'folder', onChange: () => setPickMode('folder') }), "Folder"] }), jsx("span", { className: "text-gray-400", children: "(drag-and-drop accepts either)" })] }), jsxs("div", { className: `flex items-center border-2 border-dashed rounded-lg p-6 bg-white shadow-md space-x-4 transition-shadow duration-150
1018
+ ${drag ? 'border-indigo-500 bg-indigo-50 shadow-lg' : 'border-gray-300 hover:shadow-lg'}`, onClick: openPicker, onDragEnter: e => { e.preventDefault(); e.stopPropagation(); setDrag(true); }, onDragOver: e => { e.preventDefault(); e.stopPropagation(); }, onDragLeave: e => { e.preventDefault(); e.stopPropagation(); setDrag(false); }, onDrop: handleDrop, role: "region", "aria-label": "File upload area", children: [jsx(ForwardRef, { style: { width: 300, height: 300 }, className: "text-gray-500 mb-3", "aria-hidden": "true" }), jsxs("div", { className: "flex-1", children: [jsx("p", { className: "text-base font-semibold text-gray-700", children: "Drag & drop files or folders here" }), jsxs("p", { className: "text-xs text-gray-500 mt-1", children: ["Click to ", pickMode === 'folder' ? 'choose a folder' : 'choose files'] }), queue.length > 0 && (jsxs("p", { className: "mt-1 text-sm text-gray-600", children: [queue.length, " item", queue.length > 1 && 's', " queued"] }))] })] }), jsx("input", { ref: fileInputRef, type: "file", multiple: true, className: "hidden", onChange: handleSelect }), jsx("input", { ref: dirInputRef, type: "file", multiple: true, className: "hidden", ...{ webkitdirectory: '', directory: '' }, onChange: handleSelect }), jsxs("div", { className: "flex items-center justify-between mt-4", children: [jsx("div", { className: "flex-1 mr-4", children: !creatingNew ? (jsxs("select", { value: uploadDir, onChange: handleDirChange, className: "form-select w-full", children: [dirs.map(d => (jsx("option", { value: d, children: d }, d))), jsx("option", { value: "__new__", children: "+ Create new directory\u2026" })] })) : (jsxs("div", { className: "flex", children: [jsx("input", { type: "text", value: newDirName, placeholder: "new-folder-name", onChange: e => setNewDirName(e.target.value), onBlur: acceptNewDir, onKeyDown: e => e.key === 'Enter' && acceptNewDir(), className: "form-input flex-1" }), jsx(Button, { onClick: acceptNewDir, className: "ml-2", children: "OK" })] })) }), jsx(Button, { color: "blue", variant: "primary", onClick: handleUploadAll, disabled: busy || !queue.length, children: busy ? 'Uploading…' : `Upload ${queue.length}` })] }), message && (jsx("p", { className: `text-center text-sm ${message.includes('failed') ? 'text-red-600' : 'text-gray-600'}`, role: "alert", children: message })), queue.length > 0 && (jsxs("div", { className: "wm-container mt-4", children: [jsx("h3", { className: "mb-2 text-sm font-semibold text-gray-700", children: "Queued Files" }), jsx("ul", { className: "max-h-64 space-y-4 overflow-y-auto", children: queue.map(({ file, relPath, progress }) => (jsxs("li", { className: "space-y-1", children: [jsxs("div", { className: "flex justify-between items-center", children: [jsxs("span", { className: "truncate text-sm text-gray-800", children: [relPath, file.name] }), jsxs("span", { className: "text-xs text-gray-500", children: [progress, "%"] })] }), jsx("div", { className: "w-full bg-gray-200 rounded-full h-2 overflow-hidden", children: jsx("div", { className: "bg-indigo-600 h-2", style: { width: `${progress}%` } }) })] }, `${file.name}-${file.size}`))) })] }))] }));
969
1019
  }
970
1020
 
971
1021
  const SecureFiles = () => {