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