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