@saltcorn/filemanager 0.8.7-beta.2 → 0.8.7-beta.4
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/package.json +1 -1
- package/public/build/bundle.js +2 -2
- package/public/build/bundle.js.map +1 -1
- package/src/App.svelte +197 -50
package/src/App.svelte
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { onMount } from "svelte";
|
|
3
3
|
import Fa from "svelte-fa";
|
|
4
4
|
import {
|
|
5
|
-
faTrashAlt,
|
|
6
5
|
faFileImage,
|
|
7
6
|
faFile,
|
|
8
7
|
faFolder,
|
|
@@ -22,14 +21,45 @@
|
|
|
22
21
|
export let directories = [];
|
|
23
22
|
export let roles = {};
|
|
24
23
|
export let currentFolder = "/";
|
|
24
|
+
let noSelectAll = false;
|
|
25
25
|
let selectedList = [];
|
|
26
26
|
let selectedFiles = {};
|
|
27
27
|
let rolesList;
|
|
28
28
|
let lastSelected;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
let sortBy;
|
|
30
|
+
let sortDesc = false;
|
|
31
|
+
let search = "";
|
|
32
|
+
|
|
33
|
+
const updateDirState = () => {
|
|
34
|
+
const url = new URL(window.location);
|
|
35
|
+
if (url.searchParams.get("dir") !== currentFolder) {
|
|
36
|
+
url.searchParams.set("dir", currentFolder);
|
|
37
|
+
window.history.replaceState(null, "", url.toString());
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const updateSortState = () => {
|
|
41
|
+
const url = new URL(window.location);
|
|
42
|
+
url.searchParams.set("sortBy", sortBy);
|
|
43
|
+
if (sortDesc) url.searchParams.set("sortDesc", "on");
|
|
44
|
+
else url.searchParams.delete("sortDesc");
|
|
45
|
+
window.history.replaceState(null, "", url.toString());
|
|
46
|
+
};
|
|
47
|
+
const readState = () => {
|
|
48
|
+
const url = new URL(window.location);
|
|
49
|
+
sortBy = url.searchParams.get("sortBy");
|
|
50
|
+
sortDesc = url.searchParams.get("sortDesc") === "on";
|
|
51
|
+
const dirParam = url.searchParams.get("dir");
|
|
52
|
+
if (dirParam) currentFolder = dirParam;
|
|
53
|
+
};
|
|
54
|
+
const fetchAndReset = async function (keepSelection, keepAlerts) {
|
|
55
|
+
const response = await fetch(
|
|
56
|
+
`/files?dir=${encodeURIComponent(currentFolder)}${
|
|
57
|
+
search ? `&search=${encodeURIComponent(search)}` : ""
|
|
58
|
+
}`,
|
|
59
|
+
{
|
|
60
|
+
headers: { "X-Requested-With": "XMLHttpRequest" },
|
|
61
|
+
}
|
|
62
|
+
);
|
|
33
63
|
const data = await response.json();
|
|
34
64
|
files = data.files;
|
|
35
65
|
for (const file of files) {
|
|
@@ -50,9 +80,13 @@
|
|
|
50
80
|
} else if (lastSelected) {
|
|
51
81
|
lastSelected = files.find((f) => f.filename === lastSelected.filename);
|
|
52
82
|
}
|
|
53
|
-
|
|
83
|
+
if (!keepAlerts) emptyAlerts();
|
|
84
|
+
clickHeader(sortBy || "filename", true);
|
|
54
85
|
};
|
|
55
|
-
onMount(
|
|
86
|
+
onMount(async () => {
|
|
87
|
+
readState();
|
|
88
|
+
await fetchAndReset(false, true);
|
|
89
|
+
});
|
|
56
90
|
function rowClick(file, e) {
|
|
57
91
|
file.selected = true;
|
|
58
92
|
const prev = selectedFiles[file.filename];
|
|
@@ -68,39 +102,62 @@
|
|
|
68
102
|
else lastSelected = null;
|
|
69
103
|
}
|
|
70
104
|
document.getSelection().removeAllRanges();
|
|
105
|
+
const select = document.getElementById("setRoleSelectId");
|
|
106
|
+
if (select) select.value = "";
|
|
71
107
|
console.log(lastSelected);
|
|
72
108
|
}
|
|
109
|
+
|
|
110
|
+
let ctrlDown = false;
|
|
111
|
+
function onKeyDown(e) {
|
|
112
|
+
if (e.keyCode === 17) ctrlDown = true;
|
|
113
|
+
else if (ctrlDown && e.keyCode === 65 && !noSelectAll) {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
const selectedLength = Object.values(selectedFiles).filter(
|
|
116
|
+
(v) => v
|
|
117
|
+
).length;
|
|
118
|
+
const select = selectedLength !== files.length;
|
|
119
|
+
if (!select) lastSelected = undefined;
|
|
120
|
+
for (const file of files) {
|
|
121
|
+
file.selected = select;
|
|
122
|
+
selectedFiles[file.filename] = select;
|
|
123
|
+
}
|
|
124
|
+
if (select && !lastSelected) lastSelected = files[files.length - 1];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function onKeyUp(e) {
|
|
129
|
+
if (e.keyCode === 17) ctrlDown = false;
|
|
130
|
+
}
|
|
73
131
|
$: selectedList = Object.entries(selectedFiles)
|
|
74
132
|
.filter(([k, v]) => v)
|
|
75
133
|
.map(([k, v]) => k);
|
|
76
134
|
|
|
77
|
-
async function POST(url, body, isDownload) {
|
|
78
|
-
const go=fetch(url, {
|
|
135
|
+
async function POST(url, body, isDownload, isFormData) {
|
|
136
|
+
const go = fetch(url, {
|
|
79
137
|
headers: {
|
|
80
138
|
"X-Requested-With": "XMLHttpRequest",
|
|
81
139
|
"CSRF-Token": window._sc_globalCsrf,
|
|
82
|
-
"Content-Type": "application/json",
|
|
140
|
+
...(!isFormData ? { "Content-Type": "application/json" } : {}),
|
|
83
141
|
},
|
|
84
142
|
method: "POST",
|
|
85
|
-
body: JSON.stringify(body || {}),
|
|
143
|
+
body: isFormData ? body : JSON.stringify(body || {}),
|
|
86
144
|
});
|
|
87
|
-
if(isDownload){
|
|
88
|
-
const res = await go
|
|
89
|
-
const blob = await res.blob()
|
|
145
|
+
if (isDownload) {
|
|
146
|
+
const res = await go;
|
|
147
|
+
const blob = await res.blob();
|
|
90
148
|
|
|
91
149
|
const link = document.createElement("a");
|
|
92
150
|
link.href = window.URL.createObjectURL(blob);
|
|
93
|
-
const header = res.headers.get(
|
|
94
|
-
if(header){
|
|
95
|
-
const parts = header.split(
|
|
96
|
-
let filename = parts[1].split(
|
|
151
|
+
const header = res.headers.get("Content-Disposition");
|
|
152
|
+
if (header) {
|
|
153
|
+
const parts = header.split(";");
|
|
154
|
+
let filename = parts[1].split("=")[1].replaceAll('"', "");
|
|
97
155
|
link.download = filename;
|
|
98
156
|
} else link.target = "_blank";
|
|
99
157
|
link.click();
|
|
100
|
-
|
|
101
|
-
return
|
|
102
|
-
|
|
103
|
-
return await go;
|
|
158
|
+
|
|
159
|
+
return;
|
|
160
|
+
} else return await go;
|
|
104
161
|
}
|
|
105
162
|
|
|
106
163
|
async function goAction(e) {
|
|
@@ -109,15 +166,16 @@
|
|
|
109
166
|
switch (action) {
|
|
110
167
|
case "Delete":
|
|
111
168
|
if (!confirm(`Delete files: ${selectedList.join()}`)) return;
|
|
169
|
+
const alerts = [];
|
|
112
170
|
for (const fileNm of selectedList) {
|
|
113
171
|
const file = files.find((f) => f.filename === fileNm);
|
|
114
|
-
const delres=await POST(`/files/delete/${file.location}`);
|
|
115
|
-
const deljson = await delres.json()
|
|
116
|
-
if(deljson.error)
|
|
117
|
-
|
|
118
|
-
}
|
|
172
|
+
const delres = await POST(`/files/delete/${file.location}`);
|
|
173
|
+
const deljson = await delres.json();
|
|
174
|
+
if (deljson.error)
|
|
175
|
+
alerts.push({ type: "danger", text: deljson.error });
|
|
119
176
|
}
|
|
120
177
|
await fetchAndReset();
|
|
178
|
+
for (const alert of alerts) notifyAlert(alert);
|
|
121
179
|
break;
|
|
122
180
|
case "Rename":
|
|
123
181
|
const newName = window.prompt(
|
|
@@ -134,7 +192,6 @@
|
|
|
134
192
|
await POST(`/files/unzip/${lastSelected.location}`, {});
|
|
135
193
|
await fetchAndReset();
|
|
136
194
|
break;
|
|
137
|
-
|
|
138
195
|
}
|
|
139
196
|
}
|
|
140
197
|
async function changeAccessRole(e) {
|
|
@@ -146,15 +203,18 @@
|
|
|
146
203
|
await fetchAndReset(true);
|
|
147
204
|
}
|
|
148
205
|
async function downloadZip() {
|
|
149
|
-
const filesToZip=[]
|
|
206
|
+
const filesToZip = [];
|
|
150
207
|
for (const fileNm of selectedList) {
|
|
151
|
-
filesToZip.push(fileNm)
|
|
152
|
-
|
|
208
|
+
filesToZip.push(fileNm);
|
|
153
209
|
}
|
|
154
|
-
await POST(
|
|
155
|
-
files
|
|
156
|
-
|
|
157
|
-
|
|
210
|
+
await POST(
|
|
211
|
+
`/files/download-zip`,
|
|
212
|
+
{
|
|
213
|
+
files: filesToZip,
|
|
214
|
+
location: currentFolder,
|
|
215
|
+
},
|
|
216
|
+
true
|
|
217
|
+
);
|
|
158
218
|
}
|
|
159
219
|
async function moveDirectory(e) {
|
|
160
220
|
for (const fileNm of selectedList) {
|
|
@@ -168,6 +228,7 @@
|
|
|
168
228
|
|
|
169
229
|
function gotoFolder(folder) {
|
|
170
230
|
currentFolder = folder;
|
|
231
|
+
updateDirState();
|
|
171
232
|
fetchAndReset();
|
|
172
233
|
}
|
|
173
234
|
|
|
@@ -204,11 +265,12 @@
|
|
|
204
265
|
return faFile;
|
|
205
266
|
}
|
|
206
267
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
268
|
+
function clickHeader(varNm, isInit) {
|
|
269
|
+
if (sortBy === varNm && !isInit) sortDesc = !sortDesc;
|
|
270
|
+
else if (sortBy !== varNm) {
|
|
271
|
+
sortBy = varNm;
|
|
272
|
+
sortDesc = false;
|
|
273
|
+
}
|
|
212
274
|
let getter = (x) => x[sortBy];
|
|
213
275
|
if (sortBy === "uploaded_at") getter = (x) => new Date(x[sortBy]);
|
|
214
276
|
if (sortBy === "filename") getter = (x) => (x[sortBy] || "").toLowerCase();
|
|
@@ -218,15 +280,56 @@
|
|
|
218
280
|
return 0;
|
|
219
281
|
};
|
|
220
282
|
files = files.sort(cmp);
|
|
283
|
+
updateSortState();
|
|
221
284
|
}
|
|
222
|
-
function getSorterIcon(varNm) {
|
|
285
|
+
function getSorterIcon(varNm) {
|
|
223
286
|
if (varNm !== sortBy) return null;
|
|
224
287
|
return sortDesc ? faCaretDown : faCaretUp;
|
|
225
288
|
}
|
|
289
|
+
|
|
290
|
+
function formatLocation(file) {
|
|
291
|
+
let relative =
|
|
292
|
+
currentFolder === "/"
|
|
293
|
+
? file.location
|
|
294
|
+
: file.location.substr(currentFolder.length);
|
|
295
|
+
if (relative.startsWith("/")) relative = relative.substr(1);
|
|
296
|
+
return relative.substr(0, relative.length - file.filename.length);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function uploadFiles(files) {
|
|
300
|
+
try {
|
|
301
|
+
const body = new FormData();
|
|
302
|
+
for (const file of files) {
|
|
303
|
+
body.append("file", file);
|
|
304
|
+
}
|
|
305
|
+
body.append("folder", currentFolder);
|
|
306
|
+
const resp = await POST("/files/upload", body, false, true);
|
|
307
|
+
if (resp?.status === 200) {
|
|
308
|
+
await fetchAndReset();
|
|
309
|
+
const data = await resp.json();
|
|
310
|
+
notifyAlert({ type: "success", text: data?.success?.msg || "Success" });
|
|
311
|
+
} else notifyAlert({ type: "warning", text: "Unable to upload" });
|
|
312
|
+
} catch (error) {
|
|
313
|
+
notifyAlert({
|
|
314
|
+
type: "danger",
|
|
315
|
+
text: error.message ? error.message : "An error occured.",
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function handleDrop(e) {
|
|
321
|
+
e.preventDefault();
|
|
322
|
+
if (e.dataTransfer?.files?.length > 0) uploadFiles(e.dataTransfer.files);
|
|
323
|
+
}
|
|
226
324
|
</script>
|
|
227
325
|
|
|
228
326
|
<main>
|
|
229
|
-
<div
|
|
327
|
+
<div
|
|
328
|
+
id="drop-zone"
|
|
329
|
+
on:drop={handleDrop}
|
|
330
|
+
ondragover="return false"
|
|
331
|
+
class="row"
|
|
332
|
+
>
|
|
230
333
|
<div class="col-8">
|
|
231
334
|
<div>
|
|
232
335
|
<nav aria-label="breadcrumb">
|
|
@@ -246,6 +349,37 @@
|
|
|
246
349
|
</ol>
|
|
247
350
|
</nav>
|
|
248
351
|
</div>
|
|
352
|
+
<div class="input-group search-bar mb-3">
|
|
353
|
+
<button
|
|
354
|
+
on:click={async (e) => {
|
|
355
|
+
await fetchAndReset();
|
|
356
|
+
}}
|
|
357
|
+
class="btn btn-outline-secondary search-bar"
|
|
358
|
+
type="submit"
|
|
359
|
+
id="button-search-submit"
|
|
360
|
+
>
|
|
361
|
+
<i class="fas fa-search" />
|
|
362
|
+
</button>
|
|
363
|
+
|
|
364
|
+
<input
|
|
365
|
+
on:change={async (e) => {
|
|
366
|
+
search = e.target.value;
|
|
367
|
+
await fetchAndReset();
|
|
368
|
+
}}
|
|
369
|
+
on:focus={() => {
|
|
370
|
+
noSelectAll = true;
|
|
371
|
+
}}
|
|
372
|
+
on:blur={() => {
|
|
373
|
+
noSelectAll = false;
|
|
374
|
+
}}
|
|
375
|
+
type="search"
|
|
376
|
+
class="form-control search-bar"
|
|
377
|
+
placeholder="Search Files"
|
|
378
|
+
aria-label="Search"
|
|
379
|
+
aria-describedby="button-search-submit"
|
|
380
|
+
/>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
249
383
|
<div class="filelist">
|
|
250
384
|
<table class="table table-sm">
|
|
251
385
|
<thead>
|
|
@@ -255,6 +389,9 @@
|
|
|
255
389
|
Filename
|
|
256
390
|
<Fa icon={getSorterIcon("filename", sortBy, sortDesc)} />
|
|
257
391
|
</th>
|
|
392
|
+
{#if search}
|
|
393
|
+
<th>Location</th>
|
|
394
|
+
{/if}
|
|
258
395
|
<th on:click={() => clickHeader("mimetype")}>
|
|
259
396
|
Media type
|
|
260
397
|
<Fa icon={getSorterIcon("mimetype", sortBy, sortDesc)} />
|
|
@@ -296,6 +433,11 @@
|
|
|
296
433
|
{file.filename}
|
|
297
434
|
{/if}
|
|
298
435
|
</td>
|
|
436
|
+
{#if search}
|
|
437
|
+
<td>
|
|
438
|
+
{formatLocation(file)}
|
|
439
|
+
</td>
|
|
440
|
+
{/if}
|
|
299
441
|
<td>
|
|
300
442
|
{file.mimetype}
|
|
301
443
|
</td>
|
|
@@ -315,6 +457,7 @@
|
|
|
315
457
|
<Fa size="lg" icon={faFolderPlus} />
|
|
316
458
|
</td>
|
|
317
459
|
<td>Create new folder...</td>
|
|
460
|
+
{#if search}<td />{/if}
|
|
318
461
|
<td />
|
|
319
462
|
<td />
|
|
320
463
|
<td />
|
|
@@ -377,7 +520,11 @@
|
|
|
377
520
|
</strong>
|
|
378
521
|
{/if}
|
|
379
522
|
<div class="file-actions d-flex">
|
|
380
|
-
<select
|
|
523
|
+
<select
|
|
524
|
+
id="setRoleSelectId"
|
|
525
|
+
class="form-select"
|
|
526
|
+
on:change={changeAccessRole}
|
|
527
|
+
>
|
|
381
528
|
<option value="" disabled selected>Set access</option>
|
|
382
529
|
{#each rolesList as role}
|
|
383
530
|
<option value={role.id}>{role.role}</option>
|
|
@@ -397,23 +544,23 @@
|
|
|
397
544
|
<option>Rename</option>
|
|
398
545
|
{/if}
|
|
399
546
|
{#if selectedList.length === 1 && lastSelected.filename.endsWith(".zip")}
|
|
400
|
-
|
|
401
|
-
|
|
547
|
+
<option>Unzip</option>
|
|
548
|
+
{/if}
|
|
402
549
|
</select>
|
|
403
550
|
</div>
|
|
404
|
-
{#if selectedList.length > 1}
|
|
405
|
-
<button class="btn btn-outline-secondary mt-2"
|
|
406
|
-
|
|
407
|
-
<i class="fas fa-file-archive"></i>
|
|
551
|
+
{#if selectedList.length > 1}
|
|
552
|
+
<button class="btn btn-outline-secondary mt-2" on:click={downloadZip}>
|
|
553
|
+
<i class="fas fa-file-archive" />
|
|
408
554
|
Download Zip Archive
|
|
409
555
|
</button>
|
|
410
556
|
{/if}
|
|
411
|
-
|
|
412
557
|
{/if}
|
|
413
558
|
</div>
|
|
414
559
|
</div>
|
|
415
560
|
</main>
|
|
416
561
|
|
|
562
|
+
<svelte:window on:keydown={onKeyDown} on:keyup={onKeyUp} />
|
|
563
|
+
|
|
417
564
|
<style>
|
|
418
565
|
tr.selected {
|
|
419
566
|
background-color: rgb(213, 237, 255);
|