@putkoff/abstract-securefiles 0.0.45 → 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/FileHandlers/FileTable/imports.d.ts +1 -1
- package/dist/FileHandlers/imports.d.ts +1 -1
- package/dist/file_utils/imports.d.ts +1 -1
- package/dist/file_utils/src/delete_utils/imports.d.ts +1 -1
- package/dist/file_utils/src/download_utils/imports.d.ts +1 -1
- package/dist/file_utils/src/download_utils/src/download_select.d.ts +1 -1
- package/dist/file_utils/src/imports.d.ts +1 -1
- package/dist/file_utils/src/list_utils/imports.d.ts +0 -1
- package/dist/file_utils/src/share_utils/imports.d.ts +0 -1
- package/dist/hooks/imports.d.ts +1 -1
- package/dist/hooks/useFiles/imports.d.ts +1 -1
- package/dist/imports.d.ts +0 -1
- package/dist/index.cjs +196 -139
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +197 -140
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/FileHandlers/FileTable/imports.ts +0 -1
- package/src/FileHandlers/Upload/src/upload_utils.tsx +146 -44
- package/src/FileHandlers/imports.ts +0 -1
- package/src/file_utils/imports.ts +1 -1
- package/src/file_utils/src/delete_utils/imports.ts +1 -1
- package/src/file_utils/src/download_utils/imports.ts +1 -1
- package/src/file_utils/src/download_utils/src/download_select.ts +18 -13
- package/src/file_utils/src/imports.ts +1 -1
- package/src/file_utils/src/list_utils/imports.ts +1 -1
- package/src/file_utils/src/share_utils/imports.ts +0 -1
- package/src/hooks/imports.ts +1 -1
- package/src/hooks/useFiles/imports.ts +1 -1
- package/src/imports.ts +2 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { FileRow } from './../FileRow';
|
|
2
2
|
export { ShareModal } from './../ShareModal';
|
|
3
3
|
export type { FileItem, FileTableProps } from './../imports';
|
|
4
|
-
export { useState, useFiles, downloadFile, Spinner,
|
|
4
|
+
export { useState, useFiles, downloadFile, Spinner, Input, currentUsername, Button } from './../imports';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export type { QueuedFile, FileItem, FileTableProps, UploadProps, ShareModalProps, FileRowProps } from './../imports';
|
|
2
|
-
export { CloudArrowUpIcon, currentUsername, Button, Checkbox, Input, Spinner, useEffect, useRef, useState, useCallback, ClipboardIcon, createPortal,
|
|
2
|
+
export { CloudArrowUpIcon, currentUsername, Button, Checkbox, Input, Spinner, useEffect, useRef, useState, useCallback, ClipboardIcon, createPortal, } from './../imports';
|
|
3
3
|
export { useFiles } from './../hooks';
|
|
4
4
|
export { downloadFile, uploadFiles } from './../file_utils';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { requireToken, alertit, fetchIt, stripPrefixes, make_path } from "./../imports";
|
|
2
2
|
export type { FileItem, ApiRow } from "./../types";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { requireToken, alertit } from "./../imports";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { requireToken } from './../imports';
|
|
2
2
|
export type { FileItem } from './../imports';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { FileItem } from './../imports';
|
|
1
|
+
import type { FileItem } from './../imports';
|
|
2
2
|
export declare function downloadSelect(item: FileItem): Promise<void>;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export type { FileItem, ApiRow } from './../../types/src/types';
|
|
2
|
-
export { alertit, requireToken
|
|
2
|
+
export { alertit, requireToken } from './../imports';
|
package/dist/hooks/imports.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export type { FileItem, ApiRow } from './../imports';
|
|
2
|
-
export {
|
|
2
|
+
export { alertit, useCallback, isLoggedIn } from './../imports';
|
|
3
3
|
export { toggleShare, removeFile, listFiles, downloadFile } from './../file_utils';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export type { FileItem, ApiRow } from './../imports';
|
|
2
|
-
export {
|
|
2
|
+
export { alertit, toggleShare, removeFile, listFiles, useCallback, downloadFile, isLoggedIn } from './../imports';
|
package/dist/imports.d.ts
CHANGED
|
@@ -4,4 +4,3 @@ export { useEffect, useRef, useState, useCallback } from 'react';
|
|
|
4
4
|
export type { QueuedFile, FileItem, FileTableProps, UploadProps, ShareModalProps, FileRowProps, ApiRow, EnhancedInputProps } from './types';
|
|
5
5
|
export { currentUsername, alertit, requireToken, isLoggedIn, fetchIt, stripPrefixes, make_path } from "@putkoff/abstract-utilities";
|
|
6
6
|
export { Button, Checkbox, Input, Spinner } from './UI';
|
|
7
|
-
export { secureFetchIt, fetchSharePatch } from "@putkoff/abstract-apis";
|
package/dist/index.cjs
CHANGED
|
@@ -4,7 +4,6 @@ var jsxRuntime = require('react/jsx-runtime');
|
|
|
4
4
|
var reactDom = require('react-dom');
|
|
5
5
|
var React = require('react');
|
|
6
6
|
var abstractUtilities = require('@putkoff/abstract-utilities');
|
|
7
|
-
var abstractApis = require('@putkoff/abstract-apis');
|
|
8
7
|
|
|
9
8
|
function _interopNamespaceDefault(e) {
|
|
10
9
|
var n = Object.create(null);
|
|
@@ -184,110 +183,6 @@ async function downloadFile(item) {
|
|
|
184
183
|
triggerDownload(blob, item.filename || "download");
|
|
185
184
|
}
|
|
186
185
|
|
|
187
|
-
async function downloadSelect(item) {
|
|
188
|
-
const token = abstractUtilities.requireToken();
|
|
189
|
-
const body = item;
|
|
190
|
-
const headers = {
|
|
191
|
-
Authorization: `Bearer ${token}`,
|
|
192
|
-
Accept: "application/octet-stream",
|
|
193
|
-
"Content-Type": "application/json"
|
|
194
|
-
};
|
|
195
|
-
const blob = await abstractApis.secureFetchIt("download", body, "POST", headers, true, true);
|
|
196
|
-
if (!blob || blob.size === 0) {
|
|
197
|
-
throw new Error(`Received empty file data for “${item.filename}”`);
|
|
198
|
-
}
|
|
199
|
-
triggerDownload(blob, item.filename);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** Given a checkbox inside a table-row → download that row’s file. */
|
|
203
|
-
async function downloadRow(cb) {
|
|
204
|
-
const row = cb.closest("tr");
|
|
205
|
-
const filepath = cb.value;
|
|
206
|
-
const filename = row?.children[1]?.textContent?.trim() || "downloaded_file";
|
|
207
|
-
if (filepath)
|
|
208
|
-
await downloadSelect({ filepath, filename });
|
|
209
|
-
}
|
|
210
|
-
/** Bulk download: pass a NodeList or array of checkboxes. */
|
|
211
|
-
async function downloadSelected(checkboxes) {
|
|
212
|
-
const list = Array.from(checkboxes);
|
|
213
|
-
if (!list.length) {
|
|
214
|
-
alert("No files selected.");
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
for (const cb of list) {
|
|
218
|
-
await downloadRow(cb);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Classic “select all rows” logic (still handy in non-React views
|
|
224
|
-
* such as the admin dashboard).
|
|
225
|
-
*/
|
|
226
|
-
function syncHeaderCheckbox() {
|
|
227
|
-
const all = document.querySelectorAll(".file-checkbox");
|
|
228
|
-
const checked = document.querySelectorAll(".file-checkbox:checked");
|
|
229
|
-
const header = document.getElementById("select-all-checkbox");
|
|
230
|
-
header.indeterminate =
|
|
231
|
-
checked.length > 0 && checked.length < all.length;
|
|
232
|
-
header.checked = checked.length === all.length;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/** Wire up header + row checkboxes once the table is in the DOM. */
|
|
236
|
-
function initSelectAll() {
|
|
237
|
-
const header = document.getElementById("select-all-checkbox");
|
|
238
|
-
const btn = document.getElementById("select-all");
|
|
239
|
-
// 1) header → toggle all
|
|
240
|
-
header.addEventListener("change", (e) => {
|
|
241
|
-
const on = e.target.checked;
|
|
242
|
-
document
|
|
243
|
-
.querySelectorAll(".file-checkbox")
|
|
244
|
-
.forEach((cb) => (cb.checked = on));
|
|
245
|
-
});
|
|
246
|
-
// 2) button → toggle all
|
|
247
|
-
btn.addEventListener("click", () => {
|
|
248
|
-
const cbs = Array.from(document.querySelectorAll(".file-checkbox"));
|
|
249
|
-
if (!cbs.length) {
|
|
250
|
-
alert("No files to select.");
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
const anyUnchecked = cbs.some((cb) => !cb.checked);
|
|
254
|
-
cbs.forEach((cb) => (cb.checked = anyUnchecked));
|
|
255
|
-
syncHeaderCheckbox();
|
|
256
|
-
});
|
|
257
|
-
// 3) row boxes → keep header state
|
|
258
|
-
document
|
|
259
|
-
.querySelectorAll(".file-checkbox")
|
|
260
|
-
.forEach((cb) => cb.addEventListener("change", syncHeaderCheckbox));
|
|
261
|
-
syncHeaderCheckbox();
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/** Utility for other screens: toggle a list of checkboxes on/off. */
|
|
265
|
-
function toggleCheckboxes(cbs) {
|
|
266
|
-
if (!cbs.length) {
|
|
267
|
-
alert("No files to select.");
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
const anyUnchecked = cbs.some((cb) => !cb.checked);
|
|
271
|
-
cbs.forEach((cb) => (cb.checked = anyUnchecked));
|
|
272
|
-
syncHeaderCheckbox();
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const API_BASE$1 = '/api/secure-files';
|
|
276
|
-
/** Generic PATCH helper for file-level admin settings. */
|
|
277
|
-
async function patchFileSetting(fileId, field, value) {
|
|
278
|
-
const res = await fetch(`${API_BASE$1}/files/${fileId}/settings/${field}`, {
|
|
279
|
-
method: 'PATCH',
|
|
280
|
-
credentials: 'include',
|
|
281
|
-
headers: { 'Content-Type': 'application/json' },
|
|
282
|
-
body: JSON.stringify({ value }),
|
|
283
|
-
});
|
|
284
|
-
if (!res.ok) {
|
|
285
|
-
const err = await res.json().catch(() => ({}));
|
|
286
|
-
throw new Error(err.error || res.statusText);
|
|
287
|
-
}
|
|
288
|
-
return (await res.json());
|
|
289
|
-
}
|
|
290
|
-
|
|
291
186
|
/**
|
|
292
187
|
* Cookie-authenticated API helpers.
|
|
293
188
|
* All requests include credentials: 'include' so the browser sends the
|
|
@@ -324,10 +219,10 @@ function resolveApiBase() {
|
|
|
324
219
|
// 4. Same-origin default
|
|
325
220
|
return "/api/secure-files";
|
|
326
221
|
}
|
|
327
|
-
const API_BASE = resolveApiBase();
|
|
222
|
+
const API_BASE$1 = resolveApiBase();
|
|
328
223
|
function apiUrl(path) {
|
|
329
224
|
const p = path.startsWith("/") ? path : `/${path}`;
|
|
330
|
-
return `${API_BASE}${p}`;
|
|
225
|
+
return `${API_BASE$1}${p}`;
|
|
331
226
|
}
|
|
332
227
|
/**
|
|
333
228
|
* Generic JSON fetch with session-cookie auth.
|
|
@@ -388,6 +283,118 @@ async function cookieUpload(file, relPath = "") {
|
|
|
388
283
|
throw new Error(`Upload failed: HTTP ${res.status}`);
|
|
389
284
|
return res.json();
|
|
390
285
|
}
|
|
286
|
+
/** The resolved API base URL, exported for debugging / display purposes. */
|
|
287
|
+
const SECURE_FILES_API_BASE = API_BASE$1;
|
|
288
|
+
|
|
289
|
+
async function downloadSelect(item) {
|
|
290
|
+
const res = await fetch(`${SECURE_FILES_API_BASE}/download`, {
|
|
291
|
+
method: "POST",
|
|
292
|
+
credentials: "include",
|
|
293
|
+
headers: {
|
|
294
|
+
"Content-Type": "application/json",
|
|
295
|
+
Accept: "application/octet-stream",
|
|
296
|
+
},
|
|
297
|
+
body: JSON.stringify(item),
|
|
298
|
+
});
|
|
299
|
+
if (res.status === 401)
|
|
300
|
+
throw new Error("Not authenticated – please log in.");
|
|
301
|
+
if (!res.ok)
|
|
302
|
+
throw new Error(`Download failed: HTTP ${res.status}`);
|
|
303
|
+
const blob = await res.blob();
|
|
304
|
+
if (!blob || blob.size === 0) {
|
|
305
|
+
throw new Error(`Received empty file data for "${item.filename}"`);
|
|
306
|
+
}
|
|
307
|
+
triggerDownload(blob, item.filename);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Given a checkbox inside a table-row → download that row’s file. */
|
|
311
|
+
async function downloadRow(cb) {
|
|
312
|
+
const row = cb.closest("tr");
|
|
313
|
+
const filepath = cb.value;
|
|
314
|
+
const filename = row?.children[1]?.textContent?.trim() || "downloaded_file";
|
|
315
|
+
if (filepath)
|
|
316
|
+
await downloadSelect({ filepath, filename });
|
|
317
|
+
}
|
|
318
|
+
/** Bulk download: pass a NodeList or array of checkboxes. */
|
|
319
|
+
async function downloadSelected(checkboxes) {
|
|
320
|
+
const list = Array.from(checkboxes);
|
|
321
|
+
if (!list.length) {
|
|
322
|
+
alert("No files selected.");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
for (const cb of list) {
|
|
326
|
+
await downloadRow(cb);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Classic “select all rows” logic (still handy in non-React views
|
|
332
|
+
* such as the admin dashboard).
|
|
333
|
+
*/
|
|
334
|
+
function syncHeaderCheckbox() {
|
|
335
|
+
const all = document.querySelectorAll(".file-checkbox");
|
|
336
|
+
const checked = document.querySelectorAll(".file-checkbox:checked");
|
|
337
|
+
const header = document.getElementById("select-all-checkbox");
|
|
338
|
+
header.indeterminate =
|
|
339
|
+
checked.length > 0 && checked.length < all.length;
|
|
340
|
+
header.checked = checked.length === all.length;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Wire up header + row checkboxes once the table is in the DOM. */
|
|
344
|
+
function initSelectAll() {
|
|
345
|
+
const header = document.getElementById("select-all-checkbox");
|
|
346
|
+
const btn = document.getElementById("select-all");
|
|
347
|
+
// 1) header → toggle all
|
|
348
|
+
header.addEventListener("change", (e) => {
|
|
349
|
+
const on = e.target.checked;
|
|
350
|
+
document
|
|
351
|
+
.querySelectorAll(".file-checkbox")
|
|
352
|
+
.forEach((cb) => (cb.checked = on));
|
|
353
|
+
});
|
|
354
|
+
// 2) button → toggle all
|
|
355
|
+
btn.addEventListener("click", () => {
|
|
356
|
+
const cbs = Array.from(document.querySelectorAll(".file-checkbox"));
|
|
357
|
+
if (!cbs.length) {
|
|
358
|
+
alert("No files to select.");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const anyUnchecked = cbs.some((cb) => !cb.checked);
|
|
362
|
+
cbs.forEach((cb) => (cb.checked = anyUnchecked));
|
|
363
|
+
syncHeaderCheckbox();
|
|
364
|
+
});
|
|
365
|
+
// 3) row boxes → keep header state
|
|
366
|
+
document
|
|
367
|
+
.querySelectorAll(".file-checkbox")
|
|
368
|
+
.forEach((cb) => cb.addEventListener("change", syncHeaderCheckbox));
|
|
369
|
+
syncHeaderCheckbox();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Utility for other screens: toggle a list of checkboxes on/off. */
|
|
373
|
+
function toggleCheckboxes(cbs) {
|
|
374
|
+
if (!cbs.length) {
|
|
375
|
+
alert("No files to select.");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const anyUnchecked = cbs.some((cb) => !cb.checked);
|
|
379
|
+
cbs.forEach((cb) => (cb.checked = anyUnchecked));
|
|
380
|
+
syncHeaderCheckbox();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const API_BASE = '/api/secure-files';
|
|
384
|
+
/** Generic PATCH helper for file-level admin settings. */
|
|
385
|
+
async function patchFileSetting(fileId, field, value) {
|
|
386
|
+
const res = await fetch(`${API_BASE}/files/${fileId}/settings/${field}`, {
|
|
387
|
+
method: 'PATCH',
|
|
388
|
+
credentials: 'include',
|
|
389
|
+
headers: { 'Content-Type': 'application/json' },
|
|
390
|
+
body: JSON.stringify({ value }),
|
|
391
|
+
});
|
|
392
|
+
if (!res.ok) {
|
|
393
|
+
const err = await res.json().catch(() => ({}));
|
|
394
|
+
throw new Error(err.error || res.statusText);
|
|
395
|
+
}
|
|
396
|
+
return (await res.json());
|
|
397
|
+
}
|
|
391
398
|
|
|
392
399
|
/** File-upload helper (used by non-React fallback pages). */
|
|
393
400
|
async function uploadFile(file, subdir) {
|
|
@@ -857,12 +864,45 @@ function FileTable({ files, loading, error, onToggleShare, onPatchShare, onDelet
|
|
|
857
864
|
} })] }));
|
|
858
865
|
}
|
|
859
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 ───────────────────────────────────────────────────────────────
|
|
860
897
|
function Upload({ onUploaded }) {
|
|
861
898
|
const [queue, setQueue] = React.useState([]);
|
|
862
899
|
const [busy, setBusy] = React.useState(false);
|
|
863
900
|
const [message, setMessage] = React.useState(null);
|
|
864
901
|
const [drag, setDrag] = React.useState(false);
|
|
865
|
-
|
|
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');
|
|
866
906
|
const [dirs, setDirs] = React.useState([]);
|
|
867
907
|
// — existing files → directory list —
|
|
868
908
|
const { files, load } = useFiles();
|
|
@@ -882,56 +922,76 @@ function Upload({ onUploaded }) {
|
|
|
882
922
|
const [creatingNew, setCreatingNew] = React.useState(false);
|
|
883
923
|
const [newDirName, setNewDirName] = React.useState('');
|
|
884
924
|
// — queue logic —
|
|
885
|
-
const enqueue = React.useCallback((
|
|
925
|
+
const enqueue = React.useCallback((incoming) => {
|
|
886
926
|
setQueue(prev => {
|
|
887
927
|
const map = new Map(prev.map(q => [`${q.file.name}-${q.file.size}`, q]));
|
|
888
|
-
|
|
928
|
+
incoming.forEach(q => map.set(`${q.file.name}-${q.file.size}`, q));
|
|
889
929
|
return Array.from(map.values());
|
|
890
930
|
});
|
|
891
931
|
}, []);
|
|
892
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
|
+
}
|
|
893
944
|
const out = [];
|
|
894
|
-
const walk = async (entry, path =
|
|
945
|
+
const walk = async (entry, path = '') => {
|
|
895
946
|
if (!entry)
|
|
896
947
|
return;
|
|
897
948
|
if (entry.isFile) {
|
|
898
|
-
const file = await new Promise(res => entry.file(res));
|
|
949
|
+
const file = await new Promise((res, rej) => entry.file(res, rej));
|
|
899
950
|
out.push({ file, relPath: path, progress: 0 });
|
|
951
|
+
return;
|
|
900
952
|
}
|
|
901
|
-
|
|
953
|
+
if (entry.isDirectory) {
|
|
902
954
|
const reader = entry.createReader();
|
|
903
|
-
const entries = await
|
|
955
|
+
const entries = await readAllEntries(reader);
|
|
904
956
|
await Promise.all(entries.map(e => walk(e, `${path}${entry.name}/`)));
|
|
905
957
|
}
|
|
906
958
|
};
|
|
907
|
-
await Promise.all(
|
|
959
|
+
await Promise.all(items.map(i => walk(i.webkitGetAsEntry?.())));
|
|
908
960
|
return out;
|
|
909
961
|
}, []);
|
|
910
962
|
const handleSelect = (e) => {
|
|
911
|
-
const
|
|
912
|
-
const queued =
|
|
963
|
+
const picked = Array.from(e.target.files ?? []);
|
|
964
|
+
const queued = picked.map(f => ({
|
|
913
965
|
file: f,
|
|
914
|
-
relPath: f
|
|
966
|
+
relPath: deriveRelPath(f),
|
|
915
967
|
progress: 0,
|
|
916
968
|
}));
|
|
917
969
|
enqueue(queued);
|
|
918
|
-
e.target.value =
|
|
970
|
+
e.target.value = '';
|
|
919
971
|
};
|
|
920
972
|
const handleDrop = async (e) => {
|
|
921
973
|
e.preventDefault();
|
|
922
|
-
|
|
923
|
-
|
|
974
|
+
e.stopPropagation();
|
|
975
|
+
const dropped = await extractFromDataTransfer(e.dataTransfer);
|
|
976
|
+
if (dropped.length)
|
|
977
|
+
enqueue(dropped);
|
|
924
978
|
setDrag(false);
|
|
925
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
|
+
};
|
|
926
986
|
const handleUploadAll = async () => {
|
|
927
987
|
if (!queue.length) {
|
|
928
|
-
setMessage(
|
|
988
|
+
setMessage('No files queued.');
|
|
929
989
|
return;
|
|
930
990
|
}
|
|
931
991
|
setBusy(true);
|
|
932
992
|
setMessage(null);
|
|
933
993
|
const targetDir = creatingNew
|
|
934
|
-
? (newDirName.trim().replace(/^\/+|\/+$/g,
|
|
994
|
+
? (newDirName.trim().replace(/^\/+|\/+$/g, '') + '/')
|
|
935
995
|
: uploadDir;
|
|
936
996
|
try {
|
|
937
997
|
for (const { file, relPath } of queue) {
|
|
@@ -939,12 +999,12 @@ function Upload({ onUploaded }) {
|
|
|
939
999
|
setQueue(cur => cur.map(item => item.file === file ? { ...item, progress: 100 } : item));
|
|
940
1000
|
}
|
|
941
1001
|
setQueue([]);
|
|
942
|
-
await onUploaded();
|
|
943
|
-
setMessage(
|
|
944
|
-
load()
|
|
1002
|
+
await onUploaded(); // SecureFiles passes filesHook.load here
|
|
1003
|
+
setMessage('Upload complete');
|
|
1004
|
+
// (removed the duplicate load() — onUploaded already does it)
|
|
945
1005
|
}
|
|
946
1006
|
catch {
|
|
947
|
-
setMessage(
|
|
1007
|
+
setMessage('Upload failed');
|
|
948
1008
|
}
|
|
949
1009
|
finally {
|
|
950
1010
|
setBusy(false);
|
|
@@ -953,9 +1013,9 @@ function Upload({ onUploaded }) {
|
|
|
953
1013
|
// — directory selector handlers —
|
|
954
1014
|
const handleDirChange = (e) => {
|
|
955
1015
|
const v = e.target.value;
|
|
956
|
-
if (v ===
|
|
1016
|
+
if (v === '__new__') {
|
|
957
1017
|
setCreatingNew(true);
|
|
958
|
-
setNewDirName(
|
|
1018
|
+
setNewDirName('');
|
|
959
1019
|
}
|
|
960
1020
|
else {
|
|
961
1021
|
setCreatingNew(false);
|
|
@@ -963,22 +1023,19 @@ function Upload({ onUploaded }) {
|
|
|
963
1023
|
}
|
|
964
1024
|
};
|
|
965
1025
|
const acceptNewDir = () => {
|
|
966
|
-
// normalize like before
|
|
967
1026
|
const cleanName = newDirName.trim().replace(/^\/+|\/+$/g, '');
|
|
1027
|
+
if (!cleanName) {
|
|
1028
|
+
setCreatingNew(false);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
968
1031
|
const normalized = `${uploadDir}${cleanName}/`;
|
|
969
|
-
|
|
970
|
-
setDirs(prev => {
|
|
971
|
-
if (prev.includes(normalized))
|
|
972
|
-
return prev;
|
|
973
|
-
return [...prev, normalized].sort();
|
|
974
|
-
});
|
|
975
|
-
// ② select it & flip back to the <select>
|
|
1032
|
+
setDirs(prev => prev.includes(normalized) ? prev : [...prev, normalized].sort());
|
|
976
1033
|
setUploadDir(normalized);
|
|
977
1034
|
setCreatingNew(false);
|
|
978
1035
|
setNewDirName('');
|
|
979
1036
|
};
|
|
980
|
-
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
|
|
981
|
-
${drag ? 'border-indigo-500 bg-indigo-50 shadow-lg' : 'border-gray-300 hover:shadow-lg'}`, onClick:
|
|
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}`))) })] }))] }));
|
|
982
1039
|
}
|
|
983
1040
|
|
|
984
1041
|
const SecureFiles = () => {
|