@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/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
- const fetchAndReset = async function (keepSelection) {
30
- const response = await fetch(`/files?dir=${currentFolder}`, {
31
- headers: { "X-Requested-With": "XMLHttpRequest" },
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
- clickHeader("filename");
83
+ if (!keepAlerts) emptyAlerts();
84
+ clickHeader(sortBy || "filename", true);
54
85
  };
55
- onMount(fetchAndReset);
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('Content-Disposition');
94
- if(header){
95
- const parts = header.split(';');
96
- let filename = parts[1].split('=')[1].replaceAll('"', "");
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
- } else
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
- window.notifyAlert({ type: "danger", text: deljson.error })
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(`/files/download-zip`, {
155
- files: filesToZip,
156
- location: currentFolder
157
- }, true);
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
- let sortBy;
208
- let sortDesc = false;
209
- function clickHeader(varNm) {
210
- if (sortBy === varNm) sortDesc = !sortDesc;
211
- else sortBy = varNm;
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 class="row">
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 class="form-select" on:change={changeAccessRole}>
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
- <option>Unzip</option>
401
- {/if}
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
- on:click={downloadZip}>
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);