@securancy/file-explorer 1.0.3 → 1.0.5

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.
@@ -29,6 +29,9 @@ let createDirectoryModalOpen = false;
29
29
  let deleteFileModalOpen = false;
30
30
  let deleteFileEvent;
31
31
  let deleting = false;
32
+ let overrideFilesModalOpen = false;
33
+ let overrideFiles = [];
34
+ let uploadFilesRequest;
32
35
  $: if (filePath) tick().then(() => scrollToRight());
33
36
  async function scrollToRight() {
34
37
  if (!fileExplorerContentContainer) return;
@@ -49,23 +52,21 @@ function handleScrollBack() {
49
52
  async function handleUploadFile(event) {
50
53
  if (uploadFiles) {
51
54
  if (event.detail.files) {
52
- await uploadFiles({
55
+ await uploadFilesInternal({
53
56
  path: event.detail.path,
54
57
  files: event.detail.files
55
58
  });
56
- documents.refresh();
57
59
  } else {
58
60
  const input = document.createElement("input");
59
61
  input.type = "file";
60
62
  input.multiple = true;
61
63
  input.click();
62
- input.addEventListener("change", async (changeEvent) => {
63
- if (input.files && uploadFiles) {
64
- await uploadFiles({
64
+ input.addEventListener("change", async () => {
65
+ if (input.files) {
66
+ await uploadFilesInternal({
65
67
  path: event.detail.path,
66
68
  files: input.files
67
69
  });
68
- documents.refresh();
69
70
  }
70
71
  });
71
72
  }
@@ -73,6 +74,30 @@ async function handleUploadFile(event) {
73
74
  dispatch("upload-file", event.detail);
74
75
  }
75
76
  }
77
+ async function uploadFilesInternal(request) {
78
+ const filesInDirectory = Array.from(flattenedFiles($documents)).filter((x) => x.path === request.path);
79
+ const existingFiles = Array.from(request.files).filter((file) => filesInDirectory.some((document2) => document2.name === file.name));
80
+ if (existingFiles.length > 0) {
81
+ overrideFiles = existingFiles;
82
+ overrideFilesModalOpen = true;
83
+ uploadFilesRequest = request;
84
+ } else {
85
+ await uploadFiles(request);
86
+ documents.refresh();
87
+ }
88
+ }
89
+ async function confirmOverrideFiles() {
90
+ await uploadFiles(uploadFilesRequest);
91
+ documents.refresh();
92
+ overrideFilesModalOpen = false;
93
+ overrideFiles = [];
94
+ uploadFilesRequest = void 0;
95
+ }
96
+ async function cancelOverrideFiles() {
97
+ overrideFilesModalOpen = false;
98
+ overrideFiles = [];
99
+ uploadFilesRequest = void 0;
100
+ }
76
101
  function handleCreateDirectory(event) {
77
102
  if (createDirectory) {
78
103
  createDirectoryPath = event.detail.path;
@@ -106,12 +131,21 @@ function deleteFileInternal() {
106
131
  });
107
132
  }
108
133
  }
134
+ function* flattenedFiles(documents2) {
135
+ for (const document2 of documents2) {
136
+ if (document2.type === "file") {
137
+ yield document2;
138
+ continue;
139
+ }
140
+ yield* flattenedFiles(document2.children);
141
+ }
142
+ }
109
143
  </script>
110
144
 
111
145
  <div class="file-explorer file-explorer--display-mode-{displayMode}">
112
146
  <div class="file-explorer__controls">
113
147
  <FileExplorerBreadcrumb {filePath} {routePrefix} />
114
- <FileExplorerDisplayModeSwitcher bind:displayMode />
148
+ <FileExplorerDisplayModeSwitcher {translations} bind:displayMode />
115
149
  </div>
116
150
  {#if $$slots.search}
117
151
  <div class="file-explorer__search">
@@ -201,6 +235,32 @@ function deleteFileInternal() {
201
235
  </Modal>
202
236
  {/if}
203
237
 
238
+ {#if overrideFilesModalOpen}
239
+ <Modal
240
+ title={translations.overrideFiles}
241
+ bind:opened={overrideFilesModalOpen}
242
+ >
243
+ <div>
244
+ {translations.overrideFilesConfirmation}
245
+ </div>
246
+ <div class="file-explorer__text file-explorer__text--semi-bold">
247
+ {overrideFiles.map((file) => file.name).join(', ')}
248
+ </div>
249
+ <svelte:fragment slot="footer">
250
+ <Button
251
+ color="soft"
252
+ fill="clear"
253
+ on:click={cancelOverrideFiles}
254
+ >
255
+ {translations.cancel}
256
+ </Button>
257
+ <Button color="danger" on:click={confirmOverrideFiles}>
258
+ {translations.override}
259
+ </Button>
260
+ </svelte:fragment>
261
+ </Modal>
262
+ {/if}
263
+
204
264
  <style>/* eslint-disable */
205
265
  /**
206
266
  * Media queries and devices
@@ -21,24 +21,34 @@ declare const __propDef: {
21
21
  routePrefix: string;
22
22
  canEdit: boolean | ((fileItem: FileSystemItem) => boolean);
23
23
  translations?: {
24
- createDirectoryModalTitle: string;
25
24
  cancel: string;
26
25
  create: string;
27
- creating: string;
28
- itemTitle: (item: FileSystemItem) => string;
29
- emptyDirectory: string;
30
- previewFile: string;
26
+ createDirectory: string;
27
+ createDirectoryModalTitle: string;
28
+ createDirectoryPlaceholder: string;
29
+ createItemTitle: string;
31
30
  createdAt: string;
32
- modifiedAt: string;
31
+ creating: string;
32
+ delete: string;
33
33
  deleteFile: string;
34
34
  deleteFileConfirmation: string;
35
35
  deleting: string;
36
- delete: string;
37
- createDirectory: string;
38
- uploadFile: string;
39
- size: string;
40
- items: string;
41
36
  directoryAlreadyExists: string;
37
+ displayModeLabels: {
38
+ columns: string;
39
+ index: string;
40
+ };
41
+ displayModeSwitch: string;
42
+ emptyDirectory: string;
43
+ itemTitle: (item: FileSystemItem) => string;
44
+ items: string;
45
+ modifiedAt: string;
46
+ override: string;
47
+ overrideFiles: string;
48
+ overrideFilesConfirmation: string;
49
+ previewFile: string;
50
+ size: string;
51
+ uploadFile: string;
42
52
  };
43
53
  getFilePreview: (fileItem: FileItem) => Promise<FilePreview>;
44
54
  createDirectory?: ((request: CreateDirectoryRequest) => Promise<void>) | undefined;
@@ -4,20 +4,13 @@ export let createDirectoryModalOpen = false;
4
4
  export let documents;
5
5
  export let createDirectory;
6
6
  export let translations;
7
- $: existingFoldersInPath = getExistingFoldersInPath(createDirectoryPath, $documents);
8
- $: directoryAlreadyExists = existingFoldersInPath.some((x) => x.name === request.name);
9
- let request;
7
+ let request = { path: "", name: "" };
10
8
  let submitting = false;
11
9
  let input = void 0;
12
- initDocumentUpload();
13
- $: if (createDirectoryModalOpen) initDocumentUpload();
10
+ $: existingFoldersInPath = getExistingFoldersInPath(createDirectoryPath, $documents);
11
+ $: directoryAlreadyExists = existingFoldersInPath.some((x) => x.name === request.name);
12
+ $: request.path = createDirectoryPath ?? "";
14
13
  $: if (input) input.focus();
15
- function initDocumentUpload() {
16
- request = {
17
- path: createDirectoryPath,
18
- name: ""
19
- };
20
- }
21
14
  async function submitFiles() {
22
15
  submitting = true;
23
16
  await createDirectory(request).then(() => {
@@ -28,8 +21,7 @@ async function submitFiles() {
28
21
  documents.refresh();
29
22
  }
30
23
  function getExistingFoldersInPath(path, documents2) {
31
- if (!path || !documents2) return [];
32
- const pathParts = path.split("/");
24
+ const pathParts = path ? path.split("/") : [];
33
25
  let existingFoldersInPath2 = documents2.filter((x) => x.type === "directory");
34
26
  for (const pathPart of pathParts) {
35
27
  const currentFolder = existingFoldersInPath2.find((document) => document.name === pathPart);
@@ -50,7 +42,13 @@ function getExistingFoldersInPath(path, documents2) {
50
42
  error={directoryAlreadyExists ? translations.directoryAlreadyExists : undefined}
51
43
  floatError
52
44
  >
53
- <input bind:this={input} type="text" bind:value={request.name}>
45
+ <input
46
+ bind:this={input}
47
+ id="file-explorer-create-directory-name"
48
+ placeholder={translations.createDirectoryPlaceholder}
49
+ type="text"
50
+ bind:value={request.name}
51
+ />
54
52
  </FormElement>
55
53
  </div>
56
54
  <svelte:fragment slot="footer">
@@ -3,7 +3,7 @@ import type { Refreshable } from '@securancy/svelte-utilities';
3
3
  import type { CreateDirectoryRequest, DocumentTranslations, FileSystemItem } from '../models/index.js';
4
4
  declare const __propDef: {
5
5
  props: {
6
- createDirectoryPath: string;
6
+ createDirectoryPath: string | undefined;
7
7
  createDirectoryModalOpen?: boolean;
8
8
  documents: Refreshable<FileSystemItem[]>;
9
9
  createDirectory: (request: CreateDirectoryRequest) => Promise<void>;
@@ -2,7 +2,6 @@
2
2
  import FileExplorerDirectoryColumn from "./FileExplorerDirectoryColumn.svelte";
3
3
  import FileExplorerFileDetailColumn from "./FileExplorerFileDetailColumn.svelte";
4
4
  import FileExplorerDirectoryIndex from "./FileExplorerDirectoryIndex.svelte";
5
- import { joinPaths } from "../utilities";
6
5
  export let item;
7
6
  export let translations;
8
7
  export let routePrefix;
@@ -10,7 +9,14 @@ export let currentPath;
10
9
  export let canEdit;
11
10
  export let displayMode;
12
11
  export let getFilePreview;
13
- $: activeFileItem = item.children.find((fi) => currentPath.startsWith(joinPaths(fi.path, fi.name)));
12
+ $: currentPathParts = currentPath.split("/");
13
+ $: activeFileItem = item.children.find((fileItem) => {
14
+ const parts = [
15
+ ...fileItem.path.split("/").filter((part) => part !== ""),
16
+ fileItem.name
17
+ ];
18
+ return parts.every((part, index) => currentPathParts[index] === part);
19
+ });
14
20
  function canEditItem(item2, canEdit2) {
15
21
  return typeof canEdit2 === "function" ? canEdit2(item2) : canEdit2 === true;
16
22
  }
@@ -2,7 +2,7 @@
2
2
  import {} from "..";
3
3
  import { resize, ResizeDirections } from "@securancy/svelte-utilities";
4
4
  import { Icon, Popover, PopoverItem } from "@securancy/svelte-components";
5
- import { getExtension, openItem } from "../utilities/index.js";
5
+ import { getExtension, getItemHref } from "../utilities/index.js";
6
6
  import FileIcon from "./FileIcon.svelte";
7
7
  import { joinPaths } from "../utilities/index.js";
8
8
  const dispatch = createEventDispatcher();
@@ -79,7 +79,7 @@ function handleDragOver(event) {
79
79
  </span>
80
80
  {#if canEdit}
81
81
  <Popover placement="bottom-end" trigger="click">
82
- <button slot="trigger" type="button">
82
+ <button slot="trigger" title={translations.createItemTitle} type="button">
83
83
  <Icon class="fa-solid fa-add" />
84
84
  </button>
85
85
  <PopoverItem closeOnClick on:click={createDirectory}>{translations.createDirectory}</PopoverItem>
@@ -89,12 +89,11 @@ function handleDragOver(event) {
89
89
  </div>
90
90
  {#if item.children.length > 0}
91
91
  {#each item.children as child}
92
- <button
92
+ <a
93
93
  class="file-explorer__list-item"
94
94
  class:file-explorer__list-item--active={activeFileItem === child}
95
+ href={getItemHref(child, routePrefix)}
95
96
  title={translations.itemTitle(child)}
96
- type="button"
97
- on:click={() => openItem(child, routePrefix)}
98
97
  >
99
98
  {#if child.type === 'directory'}
100
99
  <Icon class="fa-light fa-folder{activeFileItem === child ? '-open' : ''}" />
@@ -105,7 +104,7 @@ function handleDragOver(event) {
105
104
  {#if child.type === 'directory'}
106
105
  <Icon class="fa-light fa-sm fa-chevron-right" size="custom" />
107
106
  {/if}
108
- </button>
107
+ </a>
109
108
  {/each}
110
109
  {:else}
111
110
  <p class="file-explorer__message">
@@ -2,7 +2,7 @@
2
2
  import { createEventDispatcher } from "svelte";
3
3
  import { resize, ResizeDirections } from "@securancy/svelte-utilities";
4
4
  import { Icon, Popover, PopoverItem } from "@securancy/svelte-components";
5
- import { getExtension, joinPaths, openItem } from "../utilities/index.js";
5
+ import { getExtension, getItemHref, joinPaths } from "../utilities/index.js";
6
6
  const dispatch = createEventDispatcher();
7
7
  export let item;
8
8
  export let routePrefix;
@@ -38,7 +38,7 @@ function createDirectory() {
38
38
  {#if canEdit}
39
39
  <div class="inline-block">
40
40
  <Popover placement="bottom-end" trigger="click">
41
- <button slot="trigger" type="button">
41
+ <button slot="trigger" title={translations.createItemTitle} type="button">
42
42
  <Icon class="fa-solid fa-add" />
43
43
  </button>
44
44
  <PopoverItem closeOnClick on:click={createDirectory}>{translations.createDirectory}</PopoverItem>
@@ -48,12 +48,11 @@ function createDirectory() {
48
48
  {/if}
49
49
  </div>
50
50
  {#each item.children as child}
51
- <button
51
+ <a
52
52
  class="file-explorer__grid-item"
53
53
  class:file-explorer__grid-item--active={activeFileItem === child}
54
+ href={getItemHref(child, routePrefix)}
54
55
  title={translations.itemTitle(child)}
55
- type="button"
56
- on:click={() => openItem(child, routePrefix)}
57
56
  >
58
57
  {#if child.type === 'directory'}
59
58
  <Icon
@@ -64,7 +63,7 @@ function createDirectory() {
64
63
  <FileIcon extension={getExtension(child.name)} size="grid" />
65
64
  {/if}
66
65
  <span>{child.name}</span>
67
- </button>
66
+ </a>
68
67
  {:else}
69
68
  <p class="file-explorer__message">
70
69
  {translations.emptyDirectory}
@@ -1,11 +1,8 @@
1
1
  <script>import { FileExplorerDisplayMode } from "../index.js";
2
2
  import { Icon } from "@securancy/svelte-components";
3
3
  export let displayMode;
4
+ export let translations;
4
5
  let modes = Object.values(FileExplorerDisplayMode);
5
- const modeLabels = {
6
- columns: "Columns",
7
- index: "Full directory"
8
- };
9
6
  const modeIcons = {
10
7
  columns: "fa-columns-3",
11
8
  index: "fa-grid"
@@ -15,8 +12,8 @@ const modeIcons = {
15
12
  <div class="file-explorer__display-mode-switcher">
16
13
  {#each modes as mode}
17
14
  <label
18
- aria-label={modeLabels[mode]}
19
- title="Switch display mode to '{modeLabels[mode].toLowerCase()}'"
15
+ aria-label={translations.displayModeLabels[mode]}
16
+ title="{translations.displayModeSwitch} {translations.displayModeLabels[mode].toLowerCase()}"
20
17
  >
21
18
  <input
22
19
  name="displayMode"
@@ -1,8 +1,9 @@
1
1
  import { SvelteComponent } from "svelte";
2
- import { FileExplorerDisplayMode } from '../index.js';
2
+ import { type DocumentTranslations, FileExplorerDisplayMode } from '../index.js';
3
3
  declare const __propDef: {
4
4
  props: {
5
5
  displayMode: FileExplorerDisplayMode;
6
+ translations: DocumentTranslations;
6
7
  };
7
8
  events: {
8
9
  [evt: string]: CustomEvent<any>;
@@ -74,7 +74,7 @@ async function refresh(_fileItem) {
74
74
  {/if}
75
75
  {#if canEdit}
76
76
  <div class="file-explorer-preview__info">
77
- <button type="button" on:click={() => dispatch('delete-file', fileItem)}>
77
+ <button id="file-explorer-delete-file" type="button" on:click={() => dispatch('delete-file', fileItem)}>
78
78
  <Icon class="fa-solid fa-trash" />
79
79
  </button>
80
80
  </div>
@@ -1,22 +1,32 @@
1
1
  import type { FileSystemItem } from './file-item.js';
2
2
  export declare const defaultTranslations: {
3
- createDirectoryModalTitle: string;
4
3
  cancel: string;
5
4
  create: string;
6
- creating: string;
7
- itemTitle: (item: FileSystemItem) => string;
8
- emptyDirectory: string;
9
- previewFile: string;
5
+ createDirectory: string;
6
+ createDirectoryModalTitle: string;
7
+ createDirectoryPlaceholder: string;
8
+ createItemTitle: string;
10
9
  createdAt: string;
11
- modifiedAt: string;
10
+ creating: string;
11
+ delete: string;
12
12
  deleteFile: string;
13
13
  deleteFileConfirmation: string;
14
14
  deleting: string;
15
- delete: string;
16
- createDirectory: string;
17
- uploadFile: string;
18
- size: string;
19
- items: string;
20
15
  directoryAlreadyExists: string;
16
+ displayModeLabels: {
17
+ columns: string;
18
+ index: string;
19
+ };
20
+ displayModeSwitch: string;
21
+ emptyDirectory: string;
22
+ itemTitle: (item: FileSystemItem) => string;
23
+ items: string;
24
+ modifiedAt: string;
25
+ override: string;
26
+ overrideFiles: string;
27
+ overrideFilesConfirmation: string;
28
+ previewFile: string;
29
+ size: string;
30
+ uploadFile: string;
21
31
  };
22
32
  export type DocumentTranslations = typeof defaultTranslations;
@@ -1,20 +1,30 @@
1
1
  export const defaultTranslations = {
2
- createDirectoryModalTitle: 'Create new directory',
3
2
  cancel: 'Cancel',
4
3
  create: 'Create',
5
- creating: 'Creating...',
6
- itemTitle: (item) => `Open ${item.type === 'directory' ? 'directory' : 'file'} ${item.name}`,
7
- emptyDirectory: 'Empty directory',
8
- previewFile: 'Preview file',
4
+ createDirectory: 'Create directory',
5
+ createDirectoryModalTitle: 'Create new directory',
6
+ createDirectoryPlaceholder: 'Name',
7
+ createItemTitle: 'Create directory or upload file',
9
8
  createdAt: 'Created at',
10
- modifiedAt: 'Last modified at',
9
+ creating: 'Creating...',
10
+ delete: 'Delete',
11
11
  deleteFile: 'Delete file',
12
12
  deleteFileConfirmation: 'Are you sure you want to delete:',
13
13
  deleting: 'Deleting...',
14
- delete: 'Delete',
15
- createDirectory: 'Create directory',
16
- uploadFile: 'Upload file',
17
- size: 'Size',
18
- items: 'Items',
19
14
  directoryAlreadyExists: 'Directory already exists',
15
+ displayModeLabels: {
16
+ columns: 'Columns',
17
+ index: 'Full directory',
18
+ },
19
+ displayModeSwitch: 'Switch display mode to',
20
+ emptyDirectory: 'Empty directory',
21
+ itemTitle: (item) => `Open ${item.type === 'directory' ? 'directory' : 'file'} ${item.name}`,
22
+ items: 'Items',
23
+ modifiedAt: 'Last modified at',
24
+ override: 'Override',
25
+ overrideFiles: 'Override files',
26
+ overrideFilesConfirmation: 'These files already exist, do you want to override them?',
27
+ previewFile: 'Preview file',
28
+ size: 'Size',
29
+ uploadFile: 'Upload file',
20
30
  };
@@ -1,5 +1,5 @@
1
1
  import type { FileSystemItem } from '../models/file-item.js';
2
- export declare function openItem(item: FileSystemItem, routePrefix: string): Promise<void>;
2
+ export declare function getItemHref(item: FileSystemItem, routePrefix: string): string;
3
3
  export declare function getExtension(path: string): string;
4
4
  export declare const imageExtensions: string[];
5
5
  export declare const videoExtensions: string[];
@@ -1,5 +1,4 @@
1
- import { goto } from '$app/navigation';
2
- export async function openItem(item, routePrefix) {
1
+ export function getItemHref(item, routePrefix) {
3
2
  const itemPath = [
4
3
  ...routePrefix.split('/'),
5
4
  ...item.path.split('/'),
@@ -8,7 +7,7 @@ export async function openItem(item, routePrefix) {
8
7
  .filter((node) => node !== '')
9
8
  .map(encodeURIComponent)
10
9
  .join('/');
11
- await goto(itemPath.startsWith('/') ? itemPath : `/${itemPath}`);
10
+ return itemPath.startsWith('/') ? itemPath : `/${itemPath}`;
12
11
  }
13
12
  export function getExtension(path) {
14
13
  // extract file name from full path (supports `\\` and `/` separators)
@@ -48,7 +47,7 @@ export function formatBytesParts(bytes, decimals = 2) {
48
47
  return [Number.parseFloat((bytes / Math.pow(k, index)).toFixed(dm)), sizes[index]];
49
48
  }
50
49
  export function joinPaths(...paths) {
51
- return paths
52
- .filter((path) => path !== '')
50
+ return '/' + paths
51
+ .map((path) => path.split('/').filter((part) => part !== '').join('/'))
53
52
  .join('/');
54
53
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securancy/file-explorer",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "svelte": "./dist/index.js",
@@ -47,13 +47,12 @@
47
47
  "vite": "^5.3.4",
48
48
  "zod": "^3.23.8",
49
49
  "@securancy/eslint-config": "0.2.1",
50
+ "@securancy/svelte-components": "4.3.1",
50
51
  "@securancy/stylelint-config": "0.1.2",
51
- "@securancy/svelte-components": "4.3.0",
52
- "@securancy/svelte-utilities": "2.0.1"
52
+ "@securancy/svelte-utilities": "2.0.2"
53
53
  },
54
54
  "peerDependencies": {
55
- "svelte": "^4.2.18",
56
- "@sveltejs/kit": "^2.5.18"
55
+ "svelte": "^4.2.18"
57
56
  },
58
57
  "scripts": {
59
58
  "dev": "vite dev",