@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/dist/api/index.js +3 -3
- package/dist/{api-3h__hDci.js → api-BZXW896G.js} +122 -49
- package/dist/api-BZXW896G.js.map +1 -0
- package/dist/components/filebrowser.d.ts +12 -1
- package/dist/components/filebrowser.d.ts.map +1 -1
- package/dist/components/tasks.d.ts.map +1 -1
- package/dist/core/filesys/common.d.ts +1 -0
- package/dist/core/filesys/common.d.ts.map +1 -1
- package/dist/{filebrowser.de-LWsrI9xj.js → filebrowser.de-BfFcNr19.js} +13 -3
- package/dist/{filebrowser.de-LWsrI9xj.js.map → filebrowser.de-BfFcNr19.js.map} +1 -1
- package/dist/{filebrowser.en-C02vpzAs.js → filebrowser.en-CLd5Mccb.js} +13 -3
- package/dist/{filebrowser.en-C02vpzAs.js.map → filebrowser.en-CLd5Mccb.js.map} +1 -1
- package/dist/fs-access-B1bmJ5U0.js +2 -0
- package/dist/{fs-access-Cjcg0_Me.js → fs-access-CrktZo-_.js} +2 -1
- package/dist/fs-access-CrktZo-_.js.map +1 -0
- package/dist/index.js +3 -3
- package/dist/parts/index.js +1 -1
- package/dist/{parts-DMjNqL4o.js → parts-KvjEFJ7v.js} +4 -4
- package/dist/{parts-DMjNqL4o.js.map → parts-KvjEFJ7v.js.map} +1 -1
- package/package.json +2 -2
- package/src/components/filebrowser.de.json +6 -1
- package/src/components/filebrowser.en.json +6 -1
- package/src/components/filebrowser.ts +120 -28
- package/src/components/tasks.ts +16 -15
- package/src/core/filesys/common.ts +2 -1
- package/dist/api-3h__hDci.js.map +0 -1
- package/dist/fs-access-Cjcg0_Me.js.map +0 -1
- package/dist/fs-access-Dp9Fwsxk.js +0 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eclipse-docks/core",
|
|
3
|
-
"version": "0.7.
|
|
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.
|
|
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
|
|
652
|
-
|
|
653
|
-
|
|
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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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(
|
|
789
|
-
const total =
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
808
|
-
|
|
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 ${
|
|
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}'">
|
package/src/components/tasks.ts
CHANGED
|
@@ -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
|
-
${
|
|
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"
|
|
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) {
|