@eclipse-docks/core 0.7.139 → 0.7.140

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eclipse-docks/core",
3
- "version": "0.7.139",
3
+ "version": "0.7.140",
4
4
  "description": "Eclipse Docks platform core: registries, services, parts, widgets, and API",
5
5
  "type": "module",
6
6
  "license": "EPL-2.0",
@@ -77,7 +77,7 @@
77
77
  "test:coverage": "vitest run --coverage"
78
78
  },
79
79
  "dependencies": {
80
- "dompurify": "^3.2.7",
80
+ "dompurify": "^3.4.16",
81
81
  "@awesome.me/webawesome": "^3.12.0",
82
82
  "@fortawesome/fontawesome-free": "^7.2.0",
83
83
  "@fortawesome/fontawesome-svg-core": "^7.2.0",
@@ -10,5 +10,10 @@
10
10
  "DROP_FILES_HERE": "📁 Dateien hier ablegen",
11
11
  "OPEN_WITH": "Öffnen mit...",
12
12
  "FOLDER_LOAD_FAILED": "Ordner konnte nicht geladen werden: {detail}",
13
- "FOLDER_UNAVAILABLE": "Dieser Arbeitsbereichsordner ist nicht verfügbar: {detail}"
13
+ "FOLDER_UNAVAILABLE": "Dieser Arbeitsbereichsordner ist nicht verfügbar: {detail}",
14
+ "UNKNOWN_BACKEND": "unbekannt",
15
+ "DND_CROSS_CONNECTION_CONFIRM": "{count} Element(e) von \"{srcBackend}\" nach \"{destBackend}\" kopieren?",
16
+ "DROP_UPLOAD_TASK": "Abgelegte Dateien werden hochgeladen",
17
+ "DROP_SCANNING": "Wird durchsucht... {count} Datei(en) gefunden",
18
+ "DROP_COPY_TASK": "Abgelegte Elemente werden kopiert"
14
19
  }
@@ -10,5 +10,10 @@
10
10
  "DROP_FILES_HERE": "📁 Drop files here",
11
11
  "OPEN_WITH": "Open with...",
12
12
  "FOLDER_LOAD_FAILED": "Failed to load folder: {detail}",
13
- "FOLDER_UNAVAILABLE": "This workspace folder is unavailable: {detail}"
13
+ "FOLDER_UNAVAILABLE": "This workspace folder is unavailable: {detail}",
14
+ "UNKNOWN_BACKEND": "unknown",
15
+ "DND_CROSS_CONNECTION_CONFIRM": "Copy {count} item(s) from \"{srcBackend}\" to \"{destBackend}\"?",
16
+ "DROP_UPLOAD_TASK": "Uploading dropped files",
17
+ "DROP_SCANNING": "Scanning... {count} file(s) found",
18
+ "DROP_COPY_TASK": "Copying dropped items"
14
19
  }
@@ -25,6 +25,7 @@ import { icon } from '../core/icon-utils';
25
25
  import { renderDropdownItem, renderDropdownSubmenu } from '../core/dropdown-item';
26
26
  import { i18n } from '../core/i18n';
27
27
  import { createLogger } from '../core/logger';
28
+ import { taskService, type ProgressMonitor } from '../core/taskservice';
28
29
 
29
30
  const logger = createLogger('DocksFileBrowser');
30
31
  const t = await i18n(import.meta.glob('./filebrowser*.json'));
@@ -34,6 +35,94 @@ const WORKSPACE_CHANGED_DEBOUNCE_MS = 250;
34
35
  /** `wa-tree-item` exposes `.model` for the bound {@link TreeNode} (not in generated element types). */
35
36
  type WaTreeItemElement = HTMLElement & { model?: TreeNode };
36
37
 
38
+ type DroppedUpload = {
39
+ relativePath: string;
40
+ file: globalThis.File;
41
+ };
42
+
43
+ function readFileEntry(entry: FileSystemFileEntry): Promise<globalThis.File> {
44
+ return new Promise((resolve, reject) => entry.file(resolve, reject));
45
+ }
46
+
47
+ /** `readEntries` returns at most 100 entries and must be called until an empty batch. */
48
+ function readAllDirectoryEntries(reader: FileSystemDirectoryReader): Promise<FileSystemEntry[]> {
49
+ return new Promise((resolve, reject) => {
50
+ const all: FileSystemEntry[] = [];
51
+ const readBatch = () => {
52
+ reader.readEntries(batch => {
53
+ if (batch.length === 0) {
54
+ resolve(all);
55
+ return;
56
+ }
57
+ all.push(...batch);
58
+ readBatch();
59
+ }, reject);
60
+ };
61
+ readBatch();
62
+ });
63
+ }
64
+
65
+ type DroppedFileFound = (count: number) => void;
66
+
67
+ async function appendDroppedEntry(
68
+ entry: FileSystemEntry,
69
+ uploads: DroppedUpload[],
70
+ onFileFound?: DroppedFileFound,
71
+ parentPath = ''
72
+ ): Promise<void> {
73
+ const relativePath = parentPath ? `${parentPath}/${entry.name}` : entry.name;
74
+
75
+ if (entry.isFile) {
76
+ try {
77
+ uploads.push({ relativePath, file: await readFileEntry(entry as FileSystemFileEntry) });
78
+ onFileFound?.(uploads.length);
79
+ } catch (error) {
80
+ logger.error(`Failed to read dropped file "${relativePath}":`, error);
81
+ }
82
+ return;
83
+ }
84
+
85
+ if (!entry.isDirectory) return;
86
+
87
+ let children: FileSystemEntry[];
88
+ try {
89
+ children = await readAllDirectoryEntries((entry as FileSystemDirectoryEntry).createReader());
90
+ } catch (error) {
91
+ logger.error(`Failed to read dropped folder "${relativePath}":`, error);
92
+ return;
93
+ }
94
+
95
+ for (const child of children) {
96
+ await appendDroppedEntry(child, uploads, onFileFound, relativePath);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Folder structure is only exposed through `webkitGetAsEntry()`; `dataTransfer.files` lists a dropped
102
+ * folder as a single unreadable file. Entries must be captured before the first `await`, because the
103
+ * drag data store becomes inaccessible once the drop handler yields.
104
+ */
105
+ export async function collectDroppedUploads(
106
+ dataTransfer: DataTransfer,
107
+ onFileFound?: DroppedFileFound
108
+ ): Promise<DroppedUpload[]> {
109
+ const entries = Array.from(dataTransfer.items ?? [])
110
+ .filter(item => item.kind === 'file')
111
+ .map(item => item.webkitGetAsEntry?.() ?? null)
112
+ .filter((entry): entry is FileSystemEntry => entry !== null);
113
+ const files = Array.from(dataTransfer.files ?? []);
114
+
115
+ if (entries.length === 0) {
116
+ return files.map(file => ({ relativePath: file.webkitRelativePath || file.name, file }));
117
+ }
118
+
119
+ const uploads: DroppedUpload[] = [];
120
+ for (const entry of entries) {
121
+ await appendDroppedEntry(entry, uploads, onFileFound);
122
+ }
123
+ return uploads;
124
+ }
125
+
37
126
  @customElement('docks-filebrowser')
38
127
  export class DocksFileBrowser extends DocksPart {
39
128
 
@@ -648,15 +737,19 @@ export class DocksFileBrowser extends DocksPart {
648
737
  const types = e.dataTransfer.types;
649
738
 
650
739
  if (types.includes('Files')) {
651
- const files = Array.from(e.dataTransfer.files);
652
- if (files.length === 0) return;
653
- await this.handleFilesDrop(files, targetDir);
740
+ const dataTransfer = e.dataTransfer;
741
+ await taskService.runAsync(t.DROP_UPLOAD_TASK, async progress => {
742
+ const uploads = await collectDroppedUploads(dataTransfer, count => {
743
+ progress.message = t.DROP_SCANNING({ count: String(count) });
744
+ });
745
+ if (uploads.length === 0) return;
746
+ await this.handleFilesDrop(uploads, targetDir, progress);
747
+ });
654
748
  return;
655
749
  }
656
750
 
657
751
  if (types.includes('application/x-workspace-file')) {
658
752
  await this.handleWorkspaceDrop(e, targetDir);
659
- return;
660
753
  }
661
754
  };
662
755
 
@@ -767,16 +860,19 @@ export class DocksFileBrowser extends DocksPart {
767
860
  }
768
861
  }
769
862
 
770
- for (const { path, resource } of sources) {
771
- try {
772
- const move = await moveWithinSameBackend(resource);
773
- await workspaceService.copyResource(resource, targetDir, { move });
774
- processed++;
775
- } catch (error) {
776
- logger.error(`Failed to handle workspace drop for "${path}":`, error);
777
- failed++;
863
+ await taskService.runAsync(t.DROP_COPY_TASK, async progress => {
864
+ const onFile = (targetPath: string) => { progress.message = targetPath; };
865
+ for (const { path, resource } of sources) {
866
+ try {
867
+ const move = await moveWithinSameBackend(resource);
868
+ await workspaceService.copyResource(resource, targetDir, { move, onFile });
869
+ processed++;
870
+ } catch (error) {
871
+ logger.error(`Failed to handle workspace drop for "${path}":`, error);
872
+ failed++;
873
+ }
778
874
  }
779
- }
875
+ });
780
876
 
781
877
  logger.info(
782
878
  `Workspace drop completed: ${processed}/${sources.length} items ${failed > 0 ? `, ${failed} failed` : ''}`
@@ -785,27 +881,28 @@ export class DocksFileBrowser extends DocksPart {
785
881
  await this.loadWorkspace(this.workspaceDir, true);
786
882
  }
787
883
 
788
- private async handleFilesDrop(files: globalThis.File[], targetDir: Directory) {
789
- const total = files.length;
884
+ private async handleFilesDrop(uploads: DroppedUpload[], targetDir: Directory, progress: ProgressMonitor) {
885
+ const total = uploads.length;
790
886
  let processed = 0;
791
887
  let failed = 0;
792
888
  let skipped = 0;
793
889
 
794
- for (const file of files) {
890
+ progress.totalSteps = total;
891
+ for (const [index, { relativePath, file }] of uploads.entries()) {
892
+ progress.currentStep = index + 1;
893
+ progress.message = relativePath;
795
894
  try {
796
- const targetPath = this.buildTargetPath(targetDir, file.name);
797
-
798
- const existingFile = await this.workspaceDir!.getResource(targetPath);
895
+ const existingFile = await targetDir.getResource(relativePath);
799
896
  if (existingFile) {
800
- const overwrite = await confirmDialog(t.FILE_EXISTS_OVERWRITE({ fileName: file.name }));
897
+ const overwrite = await confirmDialog(t.FILE_EXISTS_OVERWRITE({ fileName: relativePath }));
801
898
  if (!overwrite) {
802
899
  skipped++;
803
900
  continue;
804
901
  }
805
902
  }
806
903
 
807
- const workspaceFile = await this.workspaceDir!.getResource(
808
- targetPath,
904
+ const workspaceFile = await targetDir.getResource(
905
+ relativePath,
809
906
  { create: true }
810
907
  ) as File;
811
908
 
@@ -813,7 +910,7 @@ export class DocksFileBrowser extends DocksPart {
813
910
 
814
911
  processed++;
815
912
  } catch (error) {
816
- logger.error(`Failed to upload ${file.name}:`, error);
913
+ logger.error(`Failed to upload ${relativePath}:`, error);
817
914
  failed++;
818
915
  }
819
916
  }
@@ -823,11 +920,6 @@ export class DocksFileBrowser extends DocksPart {
823
920
  await this.loadWorkspace(this.workspaceDir);
824
921
  }
825
922
 
826
- private buildTargetPath(targetDir: Directory, fileName: string): string {
827
- const dirPath = targetDir.getWorkspacePath();
828
- return dirPath ? `${dirPath}/${fileName}` : fileName;
829
- }
830
-
831
923
  protected renderContent() {
832
924
  return html`
833
925
  <div class="tree" ${ref(this.treeRef)} style="--drop-files-text: '${t.DROP_FILES_HERE}'">
@@ -19,6 +19,19 @@ contributionRegistry.registerContribution(TOOLBAR_BOTTOM_CENTER, {
19
19
  // Singleton dialog container for progress dialog
20
20
  let progressDialogContainer: HTMLElement | null = null;
21
21
 
22
+ /** Percentage (0-100) of a task, or `undefined` when its progress is indefinite. */
23
+ function taskPercent(task: ProgressMonitor): number | undefined {
24
+ if (task.progress >= 0) return task.progress;
25
+ if (task.totalSteps > 0) return Math.round((task.currentStep / task.totalSteps) * 100);
26
+ return undefined;
27
+ }
28
+
29
+ function renderProgressBar(task: ProgressMonitor | undefined, label = '') {
30
+ const percent = task && taskPercent(task);
31
+ if (percent === undefined) return html`<wa-progress-bar indeterminate></wa-progress-bar>`;
32
+ return html`<wa-progress-bar value="${percent}">${label}</wa-progress-bar>`;
33
+ }
34
+
22
35
  function getProgressDialogContainer(): HTMLElement {
23
36
  if (!progressDialogContainer) {
24
37
  progressDialogContainer = document.createElement('div');
@@ -153,14 +166,8 @@ function updateProgressDialog(forceOpen = false) {
153
166
 
154
167
  <div class="progress-dialog-content">
155
168
  ${tasks.map((taskProgress: ProgressMonitor) => {
156
- const hasProgress = taskProgress.progress >= 0 || taskProgress.totalSteps > 0
157
- const progress = taskProgress.progress >= 0
158
- ? taskProgress.progress
159
- : (taskProgress.totalSteps > 0
160
- ? Math.round((taskProgress.currentStep / taskProgress.totalSteps) * 100)
161
- : 0)
162
-
163
169
  const showSteps = taskProgress.progress < 0 && taskProgress.totalSteps > 0
170
+ const steps = showSteps ? `${taskProgress.currentStep}/${taskProgress.totalSteps} - ` : ''
164
171
 
165
172
  return html`
166
173
  <div class="tasitem">
@@ -174,13 +181,7 @@ function updateProgressDialog(forceOpen = false) {
174
181
  </div>
175
182
  </div>
176
183
  <div class="tasprogress">
177
- ${hasProgress ? html`
178
- <wa-progress-bar value="${progress}">
179
- ${showSteps ? `${taskProgress.currentStep}/${taskProgress.totalSteps} - ` : ''}${progress}%
180
- </wa-progress-bar>
181
- ` : html`
182
- <wa-progress-bar indeterminate></wa-progress-bar>
183
- `}
184
+ ${renderProgressBar(taskProgress, `${steps}${taskPercent(taskProgress)}%`)}
184
185
  </div>
185
186
  </div>
186
187
  `
@@ -264,7 +265,7 @@ export class DocksTasks extends DocksElement {
264
265
  label="${t.ACTIVE_TASKS}"
265
266
  ></wa-spinner>
266
267
  <wa-badge appearance="outlined" variant="neutral" pill>${taskCount}</wa-badge>
267
- <div class="tasbar-wrap"><wa-progress-bar indeterminate></wa-progress-bar></div>
268
+ <div class="tasbar-wrap">${renderProgressBar(taskCount === 1 ? tasks[0] : undefined)}</div>
268
269
  </div>
269
270
  `;
270
271
  }
@@ -784,7 +784,7 @@ export class WorkspaceService {
784
784
  public async copyResource(
785
785
  src: Resource,
786
786
  destDir: Directory,
787
- options?: { move?: boolean; newName?: string }
787
+ options?: { move?: boolean; newName?: string; onFile?: (targetPath: string) => void }
788
788
  ): Promise<void> {
789
789
  await this.initPromise;
790
790
  const workspace = this._currentWorkspace;
@@ -797,6 +797,7 @@ export class WorkspaceService {
797
797
  const targetBasePath = destDirPath ? `${destDirPath}/${targetName}` : targetName;
798
798
 
799
799
  const copyFileToPath = async (file: File, targetPath: string): Promise<void> => {
800
+ options?.onFile?.(targetPath);
800
801
  const contents = await file.getContents({ blob: true });
801
802
  const targetFile = await workspace.getResource(targetPath, { create: true }) as File | null;
802
803
  if (!targetFile) {