@securancy/file-explorer 1.0.4 → 1.0.6

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.
@@ -9,6 +9,7 @@ import { Button, Modal } from "@securancy/svelte-components";
9
9
  import FileExplorerDirectory from "./FileExplorerDirectory.svelte";
10
10
  import { Icon } from "@securancy/svelte-components";
11
11
  import { fade } from "svelte/transition";
12
+ import { pathsEqual } from "../utilities";
12
13
  export let displayMode = FileExplorerDisplayMode.Columns;
13
14
  export let filePath;
14
15
  export let documents;
@@ -29,6 +30,9 @@ let createDirectoryModalOpen = false;
29
30
  let deleteFileModalOpen = false;
30
31
  let deleteFileEvent;
31
32
  let deleting = false;
33
+ let overrideFilesModalOpen = false;
34
+ let overrideFiles = [];
35
+ let uploadFilesRequest;
32
36
  $: if (filePath) tick().then(() => scrollToRight());
33
37
  async function scrollToRight() {
34
38
  if (!fileExplorerContentContainer) return;
@@ -49,23 +53,21 @@ function handleScrollBack() {
49
53
  async function handleUploadFile(event) {
50
54
  if (uploadFiles) {
51
55
  if (event.detail.files) {
52
- await uploadFiles({
56
+ await uploadFilesInternal({
53
57
  path: event.detail.path,
54
58
  files: event.detail.files
55
59
  });
56
- documents.refresh();
57
60
  } else {
58
61
  const input = document.createElement("input");
59
62
  input.type = "file";
60
63
  input.multiple = true;
61
64
  input.click();
62
- input.addEventListener("change", async (changeEvent) => {
63
- if (input.files && uploadFiles) {
64
- await uploadFiles({
65
+ input.addEventListener("change", async () => {
66
+ if (input.files) {
67
+ await uploadFilesInternal({
65
68
  path: event.detail.path,
66
69
  files: input.files
67
70
  });
68
- documents.refresh();
69
71
  }
70
72
  });
71
73
  }
@@ -73,6 +75,30 @@ async function handleUploadFile(event) {
73
75
  dispatch("upload-file", event.detail);
74
76
  }
75
77
  }
78
+ async function uploadFilesInternal(request) {
79
+ const filesInDirectory = Array.from(flattenedFiles($documents)).filter((x) => pathsEqual(x.path, request.path));
80
+ const existingFiles = Array.from(request.files).filter((file) => filesInDirectory.some((document2) => document2.name === file.name));
81
+ if (existingFiles.length > 0) {
82
+ overrideFiles = existingFiles;
83
+ overrideFilesModalOpen = true;
84
+ uploadFilesRequest = request;
85
+ } else {
86
+ await uploadFiles(request);
87
+ documents.refresh();
88
+ }
89
+ }
90
+ async function confirmOverrideFiles() {
91
+ await uploadFiles(uploadFilesRequest);
92
+ documents.refresh();
93
+ overrideFilesModalOpen = false;
94
+ overrideFiles = [];
95
+ uploadFilesRequest = void 0;
96
+ }
97
+ async function cancelOverrideFiles() {
98
+ overrideFilesModalOpen = false;
99
+ overrideFiles = [];
100
+ uploadFilesRequest = void 0;
101
+ }
76
102
  function handleCreateDirectory(event) {
77
103
  if (createDirectory) {
78
104
  createDirectoryPath = event.detail.path;
@@ -106,12 +132,21 @@ function deleteFileInternal() {
106
132
  });
107
133
  }
108
134
  }
135
+ function* flattenedFiles(documents2) {
136
+ for (const document2 of documents2) {
137
+ if (document2.type === "file") {
138
+ yield document2;
139
+ continue;
140
+ }
141
+ yield* flattenedFiles(document2.children);
142
+ }
143
+ }
109
144
  </script>
110
145
 
111
146
  <div class="file-explorer file-explorer--display-mode-{displayMode}">
112
147
  <div class="file-explorer__controls">
113
148
  <FileExplorerBreadcrumb {filePath} {routePrefix} />
114
- <FileExplorerDisplayModeSwitcher bind:displayMode />
149
+ <FileExplorerDisplayModeSwitcher {translations} bind:displayMode />
115
150
  </div>
116
151
  {#if $$slots.search}
117
152
  <div class="file-explorer__search">
@@ -201,6 +236,32 @@ function deleteFileInternal() {
201
236
  </Modal>
202
237
  {/if}
203
238
 
239
+ {#if overrideFilesModalOpen}
240
+ <Modal
241
+ title={translations.overrideFiles}
242
+ bind:opened={overrideFilesModalOpen}
243
+ >
244
+ <div>
245
+ {translations.overrideFilesConfirmation}
246
+ </div>
247
+ <div class="file-explorer__text file-explorer__text--semi-bold">
248
+ {overrideFiles.map((file) => file.name).join(', ')}
249
+ </div>
250
+ <svelte:fragment slot="footer">
251
+ <Button
252
+ color="soft"
253
+ fill="clear"
254
+ on:click={cancelOverrideFiles}
255
+ >
256
+ {translations.cancel}
257
+ </Button>
258
+ <Button color="danger" on:click={confirmOverrideFiles}>
259
+ {translations.override}
260
+ </Button>
261
+ </svelte:fragment>
262
+ </Modal>
263
+ {/if}
264
+
204
265
  <style>/* eslint-disable */
205
266
  /**
206
267
  * 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>;
@@ -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>
@@ -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>
@@ -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
  };
@@ -7,3 +7,4 @@ export declare function isImage(path: string): boolean;
7
7
  export declare function formatBytes(bytes: number, decimals?: number): string;
8
8
  export declare function formatBytesParts(bytes: number, decimals?: number): [number, string];
9
9
  export declare function joinPaths(...paths: string[]): string;
10
+ export declare function pathsEqual(path1: string, path2: string): boolean;
@@ -48,6 +48,12 @@ export function formatBytesParts(bytes, decimals = 2) {
48
48
  }
49
49
  export function joinPaths(...paths) {
50
50
  return paths
51
+ .map((path) => path.split('/').filter((part) => part !== '').join('/'))
51
52
  .filter((path) => path !== '')
52
53
  .join('/');
53
54
  }
55
+ export function pathsEqual(path1, path2) {
56
+ const path1Parts = path1.split('/').filter((part) => part !== '');
57
+ const path2Parts = path2.split('/').filter((part) => part !== '');
58
+ return path1Parts.length === path2Parts.length && path1Parts.every((part, index) => part === path2Parts[index]);
59
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securancy/file-explorer",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "svelte": "./dist/index.js",
@@ -48,8 +48,8 @@
48
48
  "zod": "^3.23.8",
49
49
  "@securancy/eslint-config": "0.2.1",
50
50
  "@securancy/stylelint-config": "0.1.2",
51
- "@securancy/svelte-components": "4.3.0",
52
- "@securancy/svelte-utilities": "2.0.1"
51
+ "@securancy/svelte-components": "4.3.2",
52
+ "@securancy/svelte-utilities": "2.0.2"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "svelte": "^4.2.18"