@matdata/yasgui 5.14.0 → 5.16.0
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/build/ts/src/Tab.d.ts +2 -0
- package/build/ts/src/Tab.js +67 -5
- package/build/ts/src/Tab.js.map +1 -1
- package/build/ts/src/TabContextMenu.d.ts +2 -1
- package/build/ts/src/TabContextMenu.js +11 -3
- package/build/ts/src/TabContextMenu.js.map +1 -1
- package/build/ts/src/TabElements.js +13 -3
- package/build/ts/src/TabElements.js.map +1 -1
- package/build/ts/src/TabSettingsModal.d.ts +9 -0
- package/build/ts/src/TabSettingsModal.js +114 -4
- package/build/ts/src/TabSettingsModal.js.map +1 -1
- package/build/ts/src/endpointSelect.js +1 -1
- package/build/ts/src/queryManagement/QueryBrowser.d.ts +1 -0
- package/build/ts/src/queryManagement/QueryBrowser.js +62 -0
- package/build/ts/src/queryManagement/QueryBrowser.js.map +1 -1
- package/build/ts/src/queryManagement/SaveManagedQueryModal.d.ts +7 -0
- package/build/ts/src/queryManagement/SaveManagedQueryModal.js +82 -4
- package/build/ts/src/queryManagement/SaveManagedQueryModal.js.map +1 -1
- package/build/ts/src/queryManagement/WorkspaceSettingsForm.d.ts +1 -0
- package/build/ts/src/queryManagement/WorkspaceSettingsForm.js +185 -8
- package/build/ts/src/queryManagement/WorkspaceSettingsForm.js.map +1 -1
- package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.d.ts +1 -0
- package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.js +18 -0
- package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.js.map +1 -1
- package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.d.ts +1 -0
- package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.js +17 -0
- package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.js.map +1 -1
- package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.d.ts +1 -0
- package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.js +61 -0
- package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.js.map +1 -1
- package/build/ts/src/queryManagement/backends/WorkspaceBackend.d.ts +1 -0
- package/build/ts/src/urlUtils.d.ts +1 -0
- package/build/ts/src/urlUtils.js +21 -0
- package/build/ts/src/urlUtils.js.map +1 -0
- package/build/ts/src/version.d.ts +1 -1
- package/build/ts/src/version.js +1 -1
- package/build/yasgui.min.css +1 -1
- package/build/yasgui.min.css.map +3 -3
- package/build/yasgui.min.js +245 -213
- package/build/yasgui.min.js.map +4 -4
- package/package.json +1 -1
- package/src/Tab.ts +83 -5
- package/src/TabContextMenu.ts +15 -5
- package/src/TabElements.scss +17 -0
- package/src/TabElements.ts +17 -3
- package/src/TabSettingsModal.scss +73 -0
- package/src/TabSettingsModal.ts +152 -6
- package/src/endpointSelect.scss +0 -22
- package/src/endpointSelect.ts +1 -1
- package/src/queryManagement/QueryBrowser.ts +72 -0
- package/src/queryManagement/SaveManagedQueryModal.ts +102 -4
- package/src/queryManagement/WorkspaceSettingsForm.ts +215 -8
- package/src/queryManagement/backends/GitWorkspaceBackend.ts +19 -0
- package/src/queryManagement/backends/InMemoryWorkspaceBackend.ts +17 -0
- package/src/queryManagement/backends/SparqlWorkspaceBackend.ts +69 -0
- package/src/queryManagement/backends/WorkspaceBackend.ts +6 -0
- package/src/tab.scss +1 -0
- package/src/themes.scss +0 -10
- package/src/urlUtils.ts +40 -0
- package/src/version.ts +1 -1
|
@@ -8,6 +8,7 @@ import { getEndpointToAutoSwitch } from "./openManagedQuery";
|
|
|
8
8
|
import { hashQueryText } from "./textHash";
|
|
9
9
|
import type { BackendType, VersionRef, ManagedTabMetadata } from "./types";
|
|
10
10
|
import { normalizeQueryFilename } from "./normalizeQueryFilename";
|
|
11
|
+
import SaveManagedQueryModal from "./SaveManagedQueryModal";
|
|
11
12
|
|
|
12
13
|
import "./QueryBrowser.scss";
|
|
13
14
|
|
|
@@ -47,6 +48,7 @@ export default class QueryBrowser {
|
|
|
47
48
|
private lastRenderedSignature: string | undefined;
|
|
48
49
|
|
|
49
50
|
private lastPointerPos: { x: number; y: number } | undefined;
|
|
51
|
+
private folderPickerModal?: SaveManagedQueryModal;
|
|
50
52
|
|
|
51
53
|
private entrySignature(entry: FolderEntry): string {
|
|
52
54
|
const parent = entry.parentId || "";
|
|
@@ -712,6 +714,76 @@ export default class QueryBrowser {
|
|
|
712
714
|
actions.appendChild(renameBtn);
|
|
713
715
|
}
|
|
714
716
|
|
|
717
|
+
if (backend.moveQuery) {
|
|
718
|
+
const moveBtn = document.createElement("button");
|
|
719
|
+
moveBtn.type = "button";
|
|
720
|
+
addClass(moveBtn, "yasgui-query-browser__action");
|
|
721
|
+
moveBtn.textContent = "Move";
|
|
722
|
+
moveBtn.setAttribute("aria-label", `Move ${entry.label} to a different folder`);
|
|
723
|
+
moveBtn.addEventListener("click", async (e) => {
|
|
724
|
+
e.preventDefault();
|
|
725
|
+
e.stopPropagation();
|
|
726
|
+
|
|
727
|
+
const currentFolderPath = entry.parentId || "";
|
|
728
|
+
if (!this.folderPickerModal) this.folderPickerModal = new SaveManagedQueryModal(this.yasgui);
|
|
729
|
+
const newFolderPath = await this.folderPickerModal.showFolderPickerOnly(
|
|
730
|
+
this.selectedWorkspaceId!,
|
|
731
|
+
currentFolderPath,
|
|
732
|
+
);
|
|
733
|
+
if (newFolderPath === undefined) return;
|
|
734
|
+
if (newFolderPath === currentFolderPath) return;
|
|
735
|
+
|
|
736
|
+
// Show loading state
|
|
737
|
+
const originalText = moveBtn.textContent;
|
|
738
|
+
moveBtn.disabled = true;
|
|
739
|
+
moveBtn.textContent = "Moving…";
|
|
740
|
+
addClass(moveBtn, "loading");
|
|
741
|
+
|
|
742
|
+
try {
|
|
743
|
+
const newQueryId = await backend.moveQuery!(entry.id, newFolderPath);
|
|
744
|
+
|
|
745
|
+
// For git workspaces: update any already-open managed tabs referencing the old path.
|
|
746
|
+
if (backend.type === "git" && newQueryId !== entry.id) {
|
|
747
|
+
for (const tab of Object.values(this.yasgui._tabs)) {
|
|
748
|
+
const meta = (tab as any).getManagedQueryMetadata?.() as ManagedTabMetadata | undefined;
|
|
749
|
+
if (!meta) continue;
|
|
750
|
+
if (meta.backendType !== "git") continue;
|
|
751
|
+
if (meta.workspaceId !== this.selectedWorkspaceId) continue;
|
|
752
|
+
const currentPath = (meta.queryRef as any)?.path as string | undefined;
|
|
753
|
+
if (currentPath !== entry.id) continue;
|
|
754
|
+
|
|
755
|
+
try {
|
|
756
|
+
const read = await backend.readQuery(newQueryId);
|
|
757
|
+
const lastSavedTextHash = hashQueryText(read.queryText);
|
|
758
|
+
const lastSavedVersionRef = this.versionRefFromVersionTag("git", read.versionTag);
|
|
759
|
+
|
|
760
|
+
(tab as any).setManagedQueryMetadata?.({
|
|
761
|
+
...meta,
|
|
762
|
+
queryRef: { ...(meta.queryRef as any), path: newQueryId },
|
|
763
|
+
lastSavedTextHash,
|
|
764
|
+
lastSavedVersionRef,
|
|
765
|
+
});
|
|
766
|
+
} catch {
|
|
767
|
+
// Best-effort: if refreshing metadata fails, the Query Browser still reflects the move.
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
this.queryPreviewById.delete(entry.id);
|
|
773
|
+
this.folderEntriesById.clear();
|
|
774
|
+
this.invalidateRenderCache();
|
|
775
|
+
await this.refresh();
|
|
776
|
+
} catch (err) {
|
|
777
|
+
// Restore button state on error
|
|
778
|
+
moveBtn.disabled = false;
|
|
779
|
+
moveBtn.textContent = originalText || "Move";
|
|
780
|
+
removeClass(moveBtn, "loading");
|
|
781
|
+
window.alert(asWorkspaceBackendError(err).message);
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
actions.appendChild(moveBtn);
|
|
785
|
+
}
|
|
786
|
+
|
|
715
787
|
if (backend.deleteQuery) {
|
|
716
788
|
const deleteBtn = document.createElement("button");
|
|
717
789
|
deleteBtn.type = "button";
|
|
@@ -41,13 +41,18 @@ export default class SaveManagedQueryModal {
|
|
|
41
41
|
private saveBtn!: HTMLButtonElement;
|
|
42
42
|
private cancelBtn!: HTMLButtonElement;
|
|
43
43
|
private saveBtnOriginalText = "Save";
|
|
44
|
+
private titleEl!: HTMLHeadingElement;
|
|
45
|
+
private workspaceRowEl!: HTMLDivElement;
|
|
44
46
|
|
|
45
47
|
private filenameTouched = false;
|
|
46
48
|
private folderPickerOpen = false;
|
|
47
49
|
private folderBrowsePath = "";
|
|
50
|
+
private mouseDownOnOverlay = false;
|
|
48
51
|
|
|
49
52
|
private resolve?: (value: SaveManagedQueryModalResult) => void;
|
|
50
53
|
private reject?: (reason?: unknown) => void;
|
|
54
|
+
private moveResolve?: (value: string | undefined) => void;
|
|
55
|
+
private isMoveMode = false;
|
|
51
56
|
|
|
52
57
|
constructor(yasgui: Yasgui) {
|
|
53
58
|
this.yasgui = yasgui;
|
|
@@ -64,6 +69,7 @@ export default class SaveManagedQueryModal {
|
|
|
64
69
|
|
|
65
70
|
const titleEl = document.createElement("h2");
|
|
66
71
|
titleEl.textContent = "Save as managed query";
|
|
72
|
+
this.titleEl = titleEl;
|
|
67
73
|
|
|
68
74
|
const closeBtn = document.createElement("button");
|
|
69
75
|
closeBtn.type = "button";
|
|
@@ -202,6 +208,7 @@ export default class SaveManagedQueryModal {
|
|
|
202
208
|
this.messageEl.setAttribute("aria-label", "Save message");
|
|
203
209
|
|
|
204
210
|
const workspaceRow = this.row("Workspace", this.workspaceSelectEl);
|
|
211
|
+
this.workspaceRowEl = workspaceRow;
|
|
205
212
|
const folderRow = this.folderRow();
|
|
206
213
|
this.nameRowEl = this.row("Name", this.nameEl);
|
|
207
214
|
this.filenameRowEl = this.row("Filename", this.filenameEl);
|
|
@@ -247,7 +254,22 @@ export default class SaveManagedQueryModal {
|
|
|
247
254
|
|
|
248
255
|
this.overlayEl.appendChild(this.modalEl);
|
|
249
256
|
|
|
250
|
-
|
|
257
|
+
// Track mousedown on overlay to distinguish from text selection that moves outside
|
|
258
|
+
this.overlayEl.addEventListener("mousedown", (e) => {
|
|
259
|
+
// Only mark as overlay mousedown if the target is the overlay itself (not modal content)
|
|
260
|
+
if (e.target === this.overlayEl) {
|
|
261
|
+
this.mouseDownOnOverlay = true;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Only close if mousedown also happened on overlay (not during text selection)
|
|
266
|
+
this.overlayEl.addEventListener("mouseup", (e) => {
|
|
267
|
+
if (e.target === this.overlayEl && this.mouseDownOnOverlay) {
|
|
268
|
+
this.cancel();
|
|
269
|
+
}
|
|
270
|
+
this.mouseDownOnOverlay = false;
|
|
271
|
+
});
|
|
272
|
+
|
|
251
273
|
document.addEventListener("keydown", (e) => {
|
|
252
274
|
if (!this.isOpen()) return;
|
|
253
275
|
if (e.key === "Escape") {
|
|
@@ -324,11 +346,27 @@ export default class SaveManagedQueryModal {
|
|
|
324
346
|
}
|
|
325
347
|
|
|
326
348
|
private cancel() {
|
|
349
|
+
this.mouseDownOnOverlay = false;
|
|
327
350
|
this.close();
|
|
328
351
|
this.overlayEl.remove();
|
|
329
|
-
this.
|
|
330
|
-
|
|
331
|
-
|
|
352
|
+
if (this.isMoveMode) {
|
|
353
|
+
this.resetMoveMode();
|
|
354
|
+
const resolve = this.moveResolve;
|
|
355
|
+
this.moveResolve = undefined;
|
|
356
|
+
resolve?.(undefined);
|
|
357
|
+
} else {
|
|
358
|
+
this.reject?.(new Error("cancelled"));
|
|
359
|
+
this.resolve = undefined;
|
|
360
|
+
this.reject = undefined;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private resetMoveMode() {
|
|
365
|
+
this.isMoveMode = false;
|
|
366
|
+
this.titleEl.textContent = "Save as managed query";
|
|
367
|
+
this.saveBtn.textContent = "Save";
|
|
368
|
+
this.saveBtnOriginalText = "Save";
|
|
369
|
+
this.workspaceRowEl.style.display = "";
|
|
332
370
|
}
|
|
333
371
|
|
|
334
372
|
private setLoading(isLoading: boolean) {
|
|
@@ -354,6 +392,18 @@ export default class SaveManagedQueryModal {
|
|
|
354
392
|
}
|
|
355
393
|
|
|
356
394
|
private submit() {
|
|
395
|
+
if (this.isMoveMode) {
|
|
396
|
+
const folderPath = this.folderPathEl.value.trim();
|
|
397
|
+
this.mouseDownOnOverlay = false;
|
|
398
|
+
this.close();
|
|
399
|
+
this.overlayEl.remove();
|
|
400
|
+
this.resetMoveMode();
|
|
401
|
+
const resolve = this.moveResolve;
|
|
402
|
+
this.moveResolve = undefined;
|
|
403
|
+
resolve?.(folderPath);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
357
407
|
const workspaceId = this.workspaceSelectEl.value;
|
|
358
408
|
const name = this.nameEl.value.trim();
|
|
359
409
|
const filename = this.filenameEl.value.trim();
|
|
@@ -399,6 +449,7 @@ export default class SaveManagedQueryModal {
|
|
|
399
449
|
}
|
|
400
450
|
}
|
|
401
451
|
|
|
452
|
+
this.mouseDownOnOverlay = false;
|
|
402
453
|
this.close();
|
|
403
454
|
this.overlayEl.remove();
|
|
404
455
|
|
|
@@ -454,6 +505,7 @@ export default class SaveManagedQueryModal {
|
|
|
454
505
|
this.folderBrowsePath = this.folderPathEl.value.trim();
|
|
455
506
|
this.folderPickerErrorEl.textContent = "";
|
|
456
507
|
this.folderPickerListEl.innerHTML = "";
|
|
508
|
+
this.mouseDownOnOverlay = false;
|
|
457
509
|
|
|
458
510
|
document.body.appendChild(this.overlayEl);
|
|
459
511
|
this.open();
|
|
@@ -551,4 +603,50 @@ export default class SaveManagedQueryModal {
|
|
|
551
603
|
this.newFolderNameEl.value = "";
|
|
552
604
|
void this.refreshFolderPicker();
|
|
553
605
|
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Show a simplified modal containing only the folder picker, for moving an existing query.
|
|
609
|
+
* Resolves with the selected folder path (empty string = root), or `undefined` if cancelled.
|
|
610
|
+
*/
|
|
611
|
+
public async showFolderPickerOnly(workspaceId: string, currentFolderPath: string): Promise<string | undefined> {
|
|
612
|
+
const workspaces = this.yasgui.persistentConfig.getWorkspaces();
|
|
613
|
+
|
|
614
|
+
this.workspaceSelectEl.innerHTML = "";
|
|
615
|
+
for (const w of workspaces) {
|
|
616
|
+
const opt = document.createElement("option");
|
|
617
|
+
opt.value = w.id;
|
|
618
|
+
opt.textContent = w.label;
|
|
619
|
+
this.workspaceSelectEl.appendChild(opt);
|
|
620
|
+
}
|
|
621
|
+
this.workspaceSelectEl.value = workspaceId;
|
|
622
|
+
|
|
623
|
+
// Hide rows not needed for a move operation.
|
|
624
|
+
this.workspaceRowEl.style.display = "none";
|
|
625
|
+
this.nameRowEl.style.display = "none";
|
|
626
|
+
this.filenameRowEl.style.display = "none";
|
|
627
|
+
this.messageRowEl.style.display = "none";
|
|
628
|
+
|
|
629
|
+
this.folderPathEl.value = currentFolderPath;
|
|
630
|
+
this.filenameTouched = false;
|
|
631
|
+
this.folderPickerOpen = false;
|
|
632
|
+
removeClass(this.folderPickerEl, "open");
|
|
633
|
+
this.folderBrowsePath = currentFolderPath;
|
|
634
|
+
this.folderPickerErrorEl.textContent = "";
|
|
635
|
+
this.folderPickerListEl.innerHTML = "";
|
|
636
|
+
this.mouseDownOnOverlay = false;
|
|
637
|
+
|
|
638
|
+
this.titleEl.textContent = "Move to folder";
|
|
639
|
+
this.saveBtn.textContent = "Move";
|
|
640
|
+
this.saveBtnOriginalText = "Move";
|
|
641
|
+
|
|
642
|
+
this.isMoveMode = true;
|
|
643
|
+
|
|
644
|
+
document.body.appendChild(this.overlayEl);
|
|
645
|
+
this.open();
|
|
646
|
+
this.folderPickerToggleEl.focus();
|
|
647
|
+
|
|
648
|
+
return new Promise<string | undefined>((resolve) => {
|
|
649
|
+
this.moveResolve = resolve;
|
|
650
|
+
});
|
|
651
|
+
}
|
|
554
652
|
}
|
|
@@ -27,6 +27,67 @@ export class WorkspaceSettingsForm {
|
|
|
27
27
|
this.options = options;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
private async fetchExistingWorkspaces(endpoint: string): Promise<string[]> {
|
|
31
|
+
try {
|
|
32
|
+
const query = `
|
|
33
|
+
PREFIX yasgui: <https://matdata.eu/ns/yasgui#>
|
|
34
|
+
|
|
35
|
+
SELECT DISTINCT ?workspace WHERE {
|
|
36
|
+
?workspace a yasgui:Workspace .
|
|
37
|
+
}
|
|
38
|
+
ORDER BY ?workspace`;
|
|
39
|
+
|
|
40
|
+
// Get authentication for this endpoint
|
|
41
|
+
const endpointConfig = this.options.persistentConfig.getEndpointConfig(endpoint);
|
|
42
|
+
const headers: Record<string, string> = {
|
|
43
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
44
|
+
Accept: "application/sparql-results+json",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Add authentication headers if configured
|
|
48
|
+
if (endpointConfig?.authentication) {
|
|
49
|
+
const auth = endpointConfig.authentication;
|
|
50
|
+
if (auth.type === "basic") {
|
|
51
|
+
const credentials = btoa(`${auth.username}:${auth.password}`);
|
|
52
|
+
headers["Authorization"] = `Basic ${credentials}`;
|
|
53
|
+
} else if (auth.type === "bearer") {
|
|
54
|
+
headers["Authorization"] = `Bearer ${auth.token}`;
|
|
55
|
+
} else if (auth.type === "apiKey") {
|
|
56
|
+
headers[auth.headerName] = auth.apiKey;
|
|
57
|
+
} else if (auth.type === "oauth2" && auth.accessToken) {
|
|
58
|
+
headers["Authorization"] = `Bearer ${auth.accessToken}`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const response = await fetch(endpoint, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers,
|
|
65
|
+
body: new URLSearchParams({ query }),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
throw new Error(`HTTP ${response.status}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const data = await response.json();
|
|
73
|
+
const workspaces: string[] = [];
|
|
74
|
+
|
|
75
|
+
if (data.results?.bindings) {
|
|
76
|
+
for (const binding of data.results.bindings) {
|
|
77
|
+
const workspace = binding.workspace?.value;
|
|
78
|
+
if (workspace) {
|
|
79
|
+
workspaces.push(workspace);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return workspaces;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error("Failed to fetch existing workspaces:", error);
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
30
91
|
public render() {
|
|
31
92
|
this.container.innerHTML = "";
|
|
32
93
|
|
|
@@ -234,6 +295,9 @@ export class WorkspaceSettingsForm {
|
|
|
234
295
|
}
|
|
235
296
|
|
|
236
297
|
private openWorkspaceConfigModal(input: { mode: "add" } | { mode: "edit"; workspace: WorkspaceConfig }) {
|
|
298
|
+
// Track mousedown for proper modal close behavior
|
|
299
|
+
let mouseDownOnOverlay = false;
|
|
300
|
+
|
|
237
301
|
const overlay = document.createElement("div");
|
|
238
302
|
addClass(overlay, "tabSettingsModalOverlay", "workspaceConfigModalOverlay", "open");
|
|
239
303
|
|
|
@@ -242,6 +306,7 @@ export class WorkspaceSettingsForm {
|
|
|
242
306
|
modal.onclick = (e) => e.stopPropagation();
|
|
243
307
|
|
|
244
308
|
const close = () => {
|
|
309
|
+
mouseDownOnOverlay = false;
|
|
245
310
|
overlay.remove();
|
|
246
311
|
document.removeEventListener("keydown", onKeyDown);
|
|
247
312
|
};
|
|
@@ -253,7 +318,20 @@ export class WorkspaceSettingsForm {
|
|
|
253
318
|
};
|
|
254
319
|
document.addEventListener("keydown", onKeyDown);
|
|
255
320
|
|
|
256
|
-
overlay
|
|
321
|
+
// Track mousedown on overlay to distinguish from text selection that moves outside
|
|
322
|
+
overlay.addEventListener("mousedown", (e) => {
|
|
323
|
+
if (e.target === overlay) {
|
|
324
|
+
mouseDownOnOverlay = true;
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Only close if mousedown also happened on overlay (not during text selection)
|
|
329
|
+
overlay.addEventListener("mouseup", (e) => {
|
|
330
|
+
if (e.target === overlay && mouseDownOnOverlay) {
|
|
331
|
+
close();
|
|
332
|
+
}
|
|
333
|
+
mouseDownOnOverlay = false;
|
|
334
|
+
});
|
|
257
335
|
|
|
258
336
|
const header = document.createElement("div");
|
|
259
337
|
addClass(header, "modalHeader");
|
|
@@ -440,11 +518,133 @@ export class WorkspaceSettingsForm {
|
|
|
440
518
|
sparqlHelp.textContent =
|
|
441
519
|
"Tip: you can reuse an existing Workspace IRI to point to an already-populated workspace, or choose a new IRI to start a fresh workspace.";
|
|
442
520
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
521
|
+
// Workspace IRI selection (dropdown + custom input)
|
|
522
|
+
const workspaceIriSelect = document.createElement("select");
|
|
523
|
+
workspaceIriSelect.setAttribute("aria-label", "Workspace IRI");
|
|
524
|
+
addClass(workspaceIriSelect, "settingsSelect");
|
|
525
|
+
|
|
526
|
+
const loadingOption = document.createElement("option");
|
|
527
|
+
loadingOption.value = "";
|
|
528
|
+
loadingOption.textContent = "Select endpoint first";
|
|
529
|
+
workspaceIriSelect.appendChild(loadingOption);
|
|
530
|
+
|
|
531
|
+
const workspaceIriCustomInput = document.createElement("input");
|
|
532
|
+
workspaceIriCustomInput.type = "url";
|
|
533
|
+
workspaceIriCustomInput.placeholder =
|
|
534
|
+
"Enter new workspace IRI (e.g., https://example.org/workspace/my-workspace)";
|
|
535
|
+
workspaceIriCustomInput.style.display = "none";
|
|
536
|
+
workspaceIriCustomInput.style.marginTop = "5px";
|
|
537
|
+
addClass(workspaceIriCustomInput, "settingsInput");
|
|
538
|
+
|
|
539
|
+
if (existing && existing.type === "sparql") {
|
|
540
|
+
workspaceIriCustomInput.value = existing.workspaceIri;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Function to populate workspace options
|
|
544
|
+
const populateWorkspaceOptions = async (endpoint: string) => {
|
|
545
|
+
// Clear existing options
|
|
546
|
+
workspaceIriSelect.innerHTML = "";
|
|
547
|
+
|
|
548
|
+
// Add loading option
|
|
549
|
+
const loading = document.createElement("option");
|
|
550
|
+
loading.value = "";
|
|
551
|
+
loading.textContent = "Loading workspaces...";
|
|
552
|
+
workspaceIriSelect.appendChild(loading);
|
|
553
|
+
workspaceIriSelect.disabled = true;
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
const workspaces = await this.fetchExistingWorkspaces(endpoint);
|
|
557
|
+
|
|
558
|
+
workspaceIriSelect.innerHTML = "";
|
|
559
|
+
workspaceIriSelect.disabled = false;
|
|
560
|
+
|
|
561
|
+
if (workspaces.length > 0) {
|
|
562
|
+
const selectPlaceholder = document.createElement("option");
|
|
563
|
+
selectPlaceholder.value = "";
|
|
564
|
+
selectPlaceholder.textContent = "Select existing workspace";
|
|
565
|
+
workspaceIriSelect.appendChild(selectPlaceholder);
|
|
566
|
+
|
|
567
|
+
for (const workspace of workspaces) {
|
|
568
|
+
const option = document.createElement("option");
|
|
569
|
+
option.value = workspace;
|
|
570
|
+
option.textContent = workspace;
|
|
571
|
+
workspaceIriSelect.appendChild(option);
|
|
572
|
+
}
|
|
573
|
+
} else {
|
|
574
|
+
const noWorkspaces = document.createElement("option");
|
|
575
|
+
noWorkspaces.value = "";
|
|
576
|
+
noWorkspaces.textContent = "No existing workspaces found";
|
|
577
|
+
workspaceIriSelect.appendChild(noWorkspaces);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Add custom option
|
|
581
|
+
const customOption = document.createElement("option");
|
|
582
|
+
customOption.value = "__custom__";
|
|
583
|
+
customOption.textContent = "➕ Enter new workspace IRI";
|
|
584
|
+
workspaceIriSelect.appendChild(customOption);
|
|
585
|
+
|
|
586
|
+
// Set current value if editing existing
|
|
587
|
+
if (existing && existing.type === "sparql") {
|
|
588
|
+
const existingIri = existing.workspaceIri;
|
|
589
|
+
const matchingOption = workspaces.find((w) => w === existingIri);
|
|
590
|
+
if (matchingOption) {
|
|
591
|
+
workspaceIriSelect.value = existingIri;
|
|
592
|
+
} else {
|
|
593
|
+
workspaceIriSelect.value = "__custom__";
|
|
594
|
+
workspaceIriCustomInput.style.display = "";
|
|
595
|
+
workspaceIriCustomInput.value = existingIri;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
} catch (error) {
|
|
599
|
+
workspaceIriSelect.innerHTML = "";
|
|
600
|
+
workspaceIriSelect.disabled = false;
|
|
601
|
+
|
|
602
|
+
const errorOption = document.createElement("option");
|
|
603
|
+
errorOption.value = "";
|
|
604
|
+
errorOption.textContent = "Failed to load workspaces";
|
|
605
|
+
workspaceIriSelect.appendChild(errorOption);
|
|
606
|
+
|
|
607
|
+
const customOption = document.createElement("option");
|
|
608
|
+
customOption.value = "__custom__";
|
|
609
|
+
customOption.textContent = "➕ Enter new workspace IRI";
|
|
610
|
+
workspaceIriSelect.appendChild(customOption);
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
// Handle workspace selection change
|
|
615
|
+
workspaceIriSelect.onchange = () => {
|
|
616
|
+
if (workspaceIriSelect.value === "__custom__") {
|
|
617
|
+
workspaceIriCustomInput.style.display = "";
|
|
618
|
+
workspaceIriCustomInput.focus();
|
|
619
|
+
} else {
|
|
620
|
+
workspaceIriCustomInput.style.display = "none";
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
// Handle endpoint change
|
|
625
|
+
endpointSelect.onchange = () => {
|
|
626
|
+
const selectedEndpoint = endpointSelect.value.trim();
|
|
627
|
+
if (selectedEndpoint) {
|
|
628
|
+
void populateWorkspaceOptions(selectedEndpoint);
|
|
629
|
+
} else {
|
|
630
|
+
workspaceIriSelect.innerHTML = "";
|
|
631
|
+
const placeholder = document.createElement("option");
|
|
632
|
+
placeholder.value = "";
|
|
633
|
+
placeholder.textContent = "Select endpoint first";
|
|
634
|
+
workspaceIriSelect.appendChild(placeholder);
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// Initial load if endpoint is already selected
|
|
639
|
+
const initialEndpoint = endpointSelect.value.trim();
|
|
640
|
+
if (initialEndpoint) {
|
|
641
|
+
void populateWorkspaceOptions(initialEndpoint);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Wrapper for workspace IRI selection
|
|
645
|
+
const workspaceIriWrapper = document.createElement("div");
|
|
646
|
+
workspaceIriWrapper.appendChild(workspaceIriSelect);
|
|
647
|
+
workspaceIriWrapper.appendChild(workspaceIriCustomInput);
|
|
448
648
|
|
|
449
649
|
const defaultGraphInput = document.createElement("input");
|
|
450
650
|
defaultGraphInput.type = "url";
|
|
@@ -454,17 +654,24 @@ export class WorkspaceSettingsForm {
|
|
|
454
654
|
|
|
455
655
|
dynamic.appendChild(this.wrapField("SPARQL endpoint", endpointSelect));
|
|
456
656
|
dynamic.appendChild(sparqlHelp);
|
|
457
|
-
dynamic.appendChild(this.wrapField("Workspace IRI",
|
|
657
|
+
dynamic.appendChild(this.wrapField("Workspace IRI", workspaceIriWrapper));
|
|
458
658
|
dynamic.appendChild(this.wrapField("Default graph", defaultGraphInput));
|
|
459
659
|
|
|
460
660
|
(dynamic as any).__getConfig = (): WorkspaceConfig => {
|
|
661
|
+
let workspaceIri = "";
|
|
662
|
+
if (workspaceIriSelect.value === "__custom__") {
|
|
663
|
+
workspaceIri = workspaceIriCustomInput.value.trim();
|
|
664
|
+
} else {
|
|
665
|
+
workspaceIri = workspaceIriSelect.value.trim();
|
|
666
|
+
}
|
|
667
|
+
|
|
461
668
|
return {
|
|
462
669
|
id: existing?.id || newWorkspaceId(),
|
|
463
670
|
type: "sparql",
|
|
464
671
|
label: labelInput.value.trim(),
|
|
465
672
|
description: descriptionInput.value.trim() || undefined,
|
|
466
673
|
endpoint: endpointSelect.value.trim(),
|
|
467
|
-
workspaceIri
|
|
674
|
+
workspaceIri,
|
|
468
675
|
defaultGraph: defaultGraphInput.value.trim() || undefined,
|
|
469
676
|
};
|
|
470
677
|
};
|
|
@@ -69,6 +69,25 @@ export default class GitWorkspaceBackend implements WorkspaceBackend {
|
|
|
69
69
|
return this.client.deleteQuery(this.config, _queryId);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
async moveQuery(queryId: string, newFolderId: string): Promise<string> {
|
|
73
|
+
if (!this.client) throw this.missingClientError();
|
|
74
|
+
|
|
75
|
+
const parts = queryId.split("/").filter(Boolean);
|
|
76
|
+
const filename = parts[parts.length - 1] || queryId;
|
|
77
|
+
const folder = newFolderId.replace(/^\/+|\/+$/g, "");
|
|
78
|
+
|
|
79
|
+
const newPath = folder ? `${folder}/${filename}` : filename;
|
|
80
|
+
if (newPath === queryId) return queryId;
|
|
81
|
+
|
|
82
|
+
const read = await this.client.readQuery(this.config, queryId);
|
|
83
|
+
await this.client.writeQuery(this.config, newPath, read.queryText, {
|
|
84
|
+
message: `Move ${filename} to ${folder || "(root)"}`,
|
|
85
|
+
});
|
|
86
|
+
await this.client.deleteQuery(this.config, queryId);
|
|
87
|
+
|
|
88
|
+
return newPath;
|
|
89
|
+
}
|
|
90
|
+
|
|
72
91
|
async renameQuery(queryId: string, newLabel: string): Promise<void> {
|
|
73
92
|
if (!this.client) throw this.missingClientError();
|
|
74
93
|
|
|
@@ -152,6 +152,23 @@ export default class InMemoryWorkspaceBackend implements WorkspaceBackend {
|
|
|
152
152
|
return { queryText: found.queryText, versionTag: found.id };
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
async moveQuery(queryId: string, newFolderId: string): Promise<string> {
|
|
156
|
+
const versions = this.versionsByQueryId.get(queryId);
|
|
157
|
+
if (!versions || versions.length === 0) throw new WorkspaceBackendError("NOT_FOUND", "Query not found");
|
|
158
|
+
|
|
159
|
+
const filename = basename(queryId);
|
|
160
|
+
const folder = normalizeFolderId(newFolderId);
|
|
161
|
+
const newId = folder ? `${folder}/${filename}` : filename;
|
|
162
|
+
if (newId === queryId) return queryId;
|
|
163
|
+
|
|
164
|
+
if (this.versionsByQueryId.has(newId))
|
|
165
|
+
throw new WorkspaceBackendError("CONFLICT", "A query already exists at this path");
|
|
166
|
+
|
|
167
|
+
this.versionsByQueryId.delete(queryId);
|
|
168
|
+
this.versionsByQueryId.set(newId, versions);
|
|
169
|
+
return newId;
|
|
170
|
+
}
|
|
171
|
+
|
|
155
172
|
async renameQuery(queryId: string, newLabel: string): Promise<void> {
|
|
156
173
|
const trimmed = newLabel.trim();
|
|
157
174
|
if (!trimmed) throw new WorkspaceBackendError("UNKNOWN", "New name is required");
|
|
@@ -422,6 +422,75 @@ WHERE { OPTIONAL { ${iri(mqIri)} rdfs:label ?oldLabel . } }
|
|
|
422
422
|
await this.sparqlUpdate(update);
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
+
async moveQuery(queryId: string, newFolderId: string): Promise<string> {
|
|
426
|
+
const mqIriValue = this.resolveManagedQueryIri(queryId);
|
|
427
|
+
const workspaceIri = this.config.workspaceIri;
|
|
428
|
+
const folder = (newFolderId || "").replace(/^\/+|\/+$/g, "");
|
|
429
|
+
|
|
430
|
+
const newContainerIriValue = folder ? mintFolderIri(workspaceIri, folder) : workspaceIri;
|
|
431
|
+
|
|
432
|
+
// Fetch the current container to detect no-ops.
|
|
433
|
+
const currentContainerIriValue = await (async () => {
|
|
434
|
+
const q = `
|
|
435
|
+
PREFIX yasgui: <https://matdata.eu/ns/yasgui#>
|
|
436
|
+
PREFIX dcterms: <http://purl.org/dc/terms/>
|
|
437
|
+
|
|
438
|
+
SELECT ?container WHERE {
|
|
439
|
+
${iri(mqIriValue)} a yasgui:ManagedQuery ;
|
|
440
|
+
dcterms:isPartOf ?container .
|
|
441
|
+
}
|
|
442
|
+
LIMIT 1`;
|
|
443
|
+
const res = await this.sparqlQuery<SparqlJsonResults>(q);
|
|
444
|
+
const row = this.getBindings(res)[0];
|
|
445
|
+
const container = row?.container?.value;
|
|
446
|
+
if (!container) throw new WorkspaceBackendError("NOT_FOUND", "Query not found");
|
|
447
|
+
return container;
|
|
448
|
+
})();
|
|
449
|
+
|
|
450
|
+
if (currentContainerIriValue === newContainerIriValue) return queryId;
|
|
451
|
+
|
|
452
|
+
// Ensure each ancestor folder exists in the store.
|
|
453
|
+
if (folder) {
|
|
454
|
+
const parts = splitPath(folder);
|
|
455
|
+
const folderTriples: string[] = [];
|
|
456
|
+
for (let i = 0; i < parts.length; i++) {
|
|
457
|
+
const subPath = parts.slice(0, i + 1).join("/");
|
|
458
|
+
const folderIriValue = mintFolderIri(workspaceIri, subPath);
|
|
459
|
+
const folderLabel = parts[i];
|
|
460
|
+
folderTriples.push(`${iri(folderIriValue)} a <https://matdata.eu/ns/yasgui#WorkspaceFolder> ;`);
|
|
461
|
+
folderTriples.push(` <http://www.w3.org/2004/02/skos/core#inScheme> ${iri(workspaceIri)} ;`);
|
|
462
|
+
folderTriples.push(` <http://www.w3.org/2000/01/rdf-schema#label> ${sparqlStringLiteral(folderLabel)} .`);
|
|
463
|
+
|
|
464
|
+
if (i > 0) {
|
|
465
|
+
const parentPath = parts.slice(0, i).join("/");
|
|
466
|
+
const parentIri = mintFolderIri(workspaceIri, parentPath);
|
|
467
|
+
folderTriples.push(
|
|
468
|
+
`${iri(folderIriValue)} <http://www.w3.org/2004/02/skos/core#broader> ${iri(parentIri)} .`,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
await this.sparqlUpdate(`
|
|
473
|
+
PREFIX yasgui: <https://matdata.eu/ns/yasgui#>
|
|
474
|
+
|
|
475
|
+
INSERT DATA {
|
|
476
|
+
${iri(workspaceIri)} a yasgui:Workspace .
|
|
477
|
+
${folderTriples.join("\n ")}
|
|
478
|
+
}
|
|
479
|
+
`);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Update dcterms:isPartOf to point to the new container.
|
|
483
|
+
await this.sparqlUpdate(`
|
|
484
|
+
PREFIX dcterms: <http://purl.org/dc/terms/>
|
|
485
|
+
|
|
486
|
+
DELETE { ${iri(mqIriValue)} dcterms:isPartOf ${iri(currentContainerIriValue)} . }
|
|
487
|
+
INSERT { ${iri(mqIriValue)} dcterms:isPartOf ${iri(newContainerIriValue)} . }
|
|
488
|
+
WHERE { ${iri(mqIriValue)} dcterms:isPartOf ${iri(currentContainerIriValue)} . }
|
|
489
|
+
`);
|
|
490
|
+
|
|
491
|
+
return queryId;
|
|
492
|
+
}
|
|
493
|
+
|
|
425
494
|
async deleteQuery(queryId: string): Promise<void> {
|
|
426
495
|
const mqIri = this.resolveManagedQueryIri(queryId);
|
|
427
496
|
const update = `
|
|
@@ -23,6 +23,12 @@ export interface WorkspaceBackend {
|
|
|
23
23
|
*/
|
|
24
24
|
renameQuery?(queryId: string, newLabel: string): Promise<void>;
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Optional: Move a query to a different folder.
|
|
28
|
+
* Returns the new query ID (may differ from the original for Git backends where the ID encodes the path).
|
|
29
|
+
*/
|
|
30
|
+
moveQuery?(queryId: string, newFolderId: string): Promise<string>;
|
|
31
|
+
|
|
26
32
|
/**
|
|
27
33
|
* Optional: Delete a query and its version history.
|
|
28
34
|
* Implementations may not support this (e.g., some Git provider clients).
|