@matdata/yasgui 5.14.0 → 5.15.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.js +21 -4
- package/build/ts/src/Tab.js.map +1 -1
- package/build/ts/src/TabSettingsModal.d.ts +1 -0
- package/build/ts/src/TabSettingsModal.js +25 -1
- package/build/ts/src/TabSettingsModal.js.map +1 -1
- package/build/ts/src/queryManagement/SaveManagedQueryModal.d.ts +1 -0
- package/build/ts/src/queryManagement/SaveManagedQueryModal.js +15 -1
- 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/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 +141 -135
- package/build/yasgui.min.js.map +4 -4
- package/package.json +1 -1
- package/src/Tab.ts +30 -4
- package/src/TabSettingsModal.ts +35 -3
- package/src/queryManagement/SaveManagedQueryModal.ts +20 -1
- package/src/queryManagement/WorkspaceSettingsForm.ts +215 -8
- package/src/urlUtils.ts +40 -0
- package/src/version.ts +1 -1
package/package.json
CHANGED
package/src/Tab.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { saveManagedQuery } from "./queryManagement/saveManagedQuery";
|
|
|
17
17
|
import { getWorkspaceBackend } from "./queryManagement/backends/getWorkspaceBackend";
|
|
18
18
|
import { asWorkspaceBackendError } from "./queryManagement/backends/errors";
|
|
19
19
|
import { normalizeQueryFilename } from "./queryManagement/normalizeQueryFilename";
|
|
20
|
+
import { resolveEndpointUrl } from "./urlUtils";
|
|
20
21
|
|
|
21
22
|
export interface PersistedJsonYasr extends YasrPersistentConfig {
|
|
22
23
|
responseSummary: Parser.ResponseSummary;
|
|
@@ -304,7 +305,7 @@ export class Tab extends EventEmitter {
|
|
|
304
305
|
name: result.name,
|
|
305
306
|
filename: result.filename,
|
|
306
307
|
queryText: this.getQueryTextForSave(),
|
|
307
|
-
associatedEndpoint: workspace.type === "sparql" ? this.getEndpoint() : undefined,
|
|
308
|
+
associatedEndpoint: workspace.type === "sparql" ? resolveEndpointUrl(this.getEndpoint()) : undefined,
|
|
308
309
|
message: result.message,
|
|
309
310
|
expectedVersionTag,
|
|
310
311
|
});
|
|
@@ -1445,14 +1446,38 @@ export class Tab extends EventEmitter {
|
|
|
1445
1446
|
|
|
1446
1447
|
// Check if token is a URI (not a variable)
|
|
1447
1448
|
// URIs typically have token.type of 'string-2' or might be in angle brackets
|
|
1448
|
-
|
|
1449
|
+
let tokenString = token.string.trim();
|
|
1449
1450
|
|
|
1450
1451
|
// Skip if it's a variable (starts with ? or $)
|
|
1451
1452
|
if (tokenString.startsWith("?") || tokenString.startsWith("$")) return;
|
|
1452
1453
|
|
|
1454
|
+
// Handle prefixed names that may be split into multiple tokens by the tokenizer
|
|
1455
|
+
// The tokenizer splits "prefix:localname" into two tokens:
|
|
1456
|
+
// - PNAME_LN_PREFIX (e.g., "bnd:") with type "string-2"
|
|
1457
|
+
// - PNAME_LN_LOCAL (e.g., "_subnetwork_bane_KVGB") with type "string"
|
|
1458
|
+
// We need to combine them to get the full prefixed name
|
|
1459
|
+
if (token.type === "string-2" && tokenString.endsWith(":")) {
|
|
1460
|
+
// This is a prefix token, get the next token for the local name
|
|
1461
|
+
const nextToken = this.yasqe.getTokenAt({ line: pos.line, ch: token.end + 1 });
|
|
1462
|
+
if (nextToken && nextToken.type === "string" && nextToken.start === token.end) {
|
|
1463
|
+
tokenString = `${tokenString}${nextToken.string.trim()}`;
|
|
1464
|
+
}
|
|
1465
|
+
} else if (token.type === "string" && token.start > 0) {
|
|
1466
|
+
// This might be a local name token, check if previous token is a prefix
|
|
1467
|
+
const prevToken = this.yasqe.getTokenAt({ line: pos.line, ch: token.start - 1 });
|
|
1468
|
+
if (
|
|
1469
|
+
prevToken &&
|
|
1470
|
+
prevToken.type === "string-2" &&
|
|
1471
|
+
prevToken.string.trim().endsWith(":") &&
|
|
1472
|
+
prevToken.end === token.start
|
|
1473
|
+
) {
|
|
1474
|
+
tokenString = `${prevToken.string.trim()}${tokenString}`;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1453
1478
|
// Check if it's a URI - either in angle brackets or a prefixed name
|
|
1454
1479
|
const isFullUri = tokenString.startsWith("<") && tokenString.endsWith(">");
|
|
1455
|
-
const isPrefixedName = /^[\w-]
|
|
1480
|
+
const isPrefixedName = /^[\w-]+:/.test(tokenString);
|
|
1456
1481
|
|
|
1457
1482
|
if (!isFullUri && !isPrefixedName) return;
|
|
1458
1483
|
|
|
@@ -1467,7 +1492,8 @@ export class Tab extends EventEmitter {
|
|
|
1467
1492
|
} else if (isPrefixedName) {
|
|
1468
1493
|
// Expand prefixed name to full URI
|
|
1469
1494
|
const prefixes = this.yasqe.getPrefixesFromQuery();
|
|
1470
|
-
const [prefix,
|
|
1495
|
+
const [prefix, ...localParts] = tokenString.split(":");
|
|
1496
|
+
const localName = localParts.join(":");
|
|
1471
1497
|
const prefixUri = prefixes[prefix];
|
|
1472
1498
|
if (prefixUri) {
|
|
1473
1499
|
uri = prefixUri + localName;
|
package/src/TabSettingsModal.ts
CHANGED
|
@@ -39,6 +39,7 @@ export default class TabSettingsModal {
|
|
|
39
39
|
private prefixButton!: HTMLButtonElement;
|
|
40
40
|
private prefixTextarea!: HTMLTextAreaElement;
|
|
41
41
|
private autoCaptureCheckbox!: HTMLInputElement;
|
|
42
|
+
private mouseDownOnOverlay = false;
|
|
42
43
|
private handleKeyDown = (e: KeyboardEvent) => {
|
|
43
44
|
if (e.key !== "Escape") return;
|
|
44
45
|
if (!this.modalOverlay.classList.contains("open")) return;
|
|
@@ -91,8 +92,21 @@ export default class TabSettingsModal {
|
|
|
91
92
|
// Modal overlay
|
|
92
93
|
this.modalOverlay = document.createElement("div");
|
|
93
94
|
addClass(this.modalOverlay, "tabSettingsModalOverlay");
|
|
94
|
-
|
|
95
|
-
//
|
|
95
|
+
|
|
96
|
+
// Track mousedown on overlay to distinguish from text selection that moves outside
|
|
97
|
+
this.modalOverlay.addEventListener("mousedown", (e) => {
|
|
98
|
+
if (e.target === this.modalOverlay) {
|
|
99
|
+
this.mouseDownOnOverlay = true;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Only close if mousedown also happened on overlay (not during text selection)
|
|
104
|
+
this.modalOverlay.addEventListener("mouseup", (e) => {
|
|
105
|
+
if (e.target === this.modalOverlay && this.mouseDownOnOverlay) {
|
|
106
|
+
this.close();
|
|
107
|
+
}
|
|
108
|
+
this.mouseDownOnOverlay = false;
|
|
109
|
+
});
|
|
96
110
|
|
|
97
111
|
// Modal content
|
|
98
112
|
this.modalContent = document.createElement("div");
|
|
@@ -912,10 +926,27 @@ export default class TabSettingsModal {
|
|
|
912
926
|
const config = this.tab.yasgui.persistentConfig.getEndpointConfig(endpoint);
|
|
913
927
|
const existingAuth = config?.authentication;
|
|
914
928
|
|
|
929
|
+
// Track mousedown for proper modal close behavior
|
|
930
|
+
let mouseDownOnOverlay = false;
|
|
931
|
+
|
|
915
932
|
// Create modal overlay
|
|
916
933
|
const authModalOverlay = document.createElement("div");
|
|
917
934
|
addClass(authModalOverlay, "authModalOverlay");
|
|
918
|
-
|
|
935
|
+
|
|
936
|
+
// Track mousedown on overlay to distinguish from text selection that moves outside
|
|
937
|
+
authModalOverlay.addEventListener("mousedown", (e) => {
|
|
938
|
+
if (e.target === authModalOverlay) {
|
|
939
|
+
mouseDownOnOverlay = true;
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
// Only close if mousedown also happened on overlay (not during text selection)
|
|
944
|
+
authModalOverlay.addEventListener("mouseup", (e) => {
|
|
945
|
+
if (e.target === authModalOverlay && mouseDownOnOverlay) {
|
|
946
|
+
authModalOverlay.remove();
|
|
947
|
+
}
|
|
948
|
+
mouseDownOnOverlay = false;
|
|
949
|
+
});
|
|
919
950
|
|
|
920
951
|
// Create modal content
|
|
921
952
|
const authModal = document.createElement("div");
|
|
@@ -1516,6 +1547,7 @@ export default class TabSettingsModal {
|
|
|
1516
1547
|
}
|
|
1517
1548
|
|
|
1518
1549
|
public close() {
|
|
1550
|
+
this.mouseDownOnOverlay = false;
|
|
1519
1551
|
removeClass(this.modalOverlay, "open");
|
|
1520
1552
|
document.removeEventListener("keydown", this.handleKeyDown);
|
|
1521
1553
|
}
|
|
@@ -45,6 +45,7 @@ export default class SaveManagedQueryModal {
|
|
|
45
45
|
private filenameTouched = false;
|
|
46
46
|
private folderPickerOpen = false;
|
|
47
47
|
private folderBrowsePath = "";
|
|
48
|
+
private mouseDownOnOverlay = false;
|
|
48
49
|
|
|
49
50
|
private resolve?: (value: SaveManagedQueryModalResult) => void;
|
|
50
51
|
private reject?: (reason?: unknown) => void;
|
|
@@ -247,7 +248,22 @@ export default class SaveManagedQueryModal {
|
|
|
247
248
|
|
|
248
249
|
this.overlayEl.appendChild(this.modalEl);
|
|
249
250
|
|
|
250
|
-
|
|
251
|
+
// Track mousedown on overlay to distinguish from text selection that moves outside
|
|
252
|
+
this.overlayEl.addEventListener("mousedown", (e) => {
|
|
253
|
+
// Only mark as overlay mousedown if the target is the overlay itself (not modal content)
|
|
254
|
+
if (e.target === this.overlayEl) {
|
|
255
|
+
this.mouseDownOnOverlay = true;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Only close if mousedown also happened on overlay (not during text selection)
|
|
260
|
+
this.overlayEl.addEventListener("mouseup", (e) => {
|
|
261
|
+
if (e.target === this.overlayEl && this.mouseDownOnOverlay) {
|
|
262
|
+
this.cancel();
|
|
263
|
+
}
|
|
264
|
+
this.mouseDownOnOverlay = false;
|
|
265
|
+
});
|
|
266
|
+
|
|
251
267
|
document.addEventListener("keydown", (e) => {
|
|
252
268
|
if (!this.isOpen()) return;
|
|
253
269
|
if (e.key === "Escape") {
|
|
@@ -324,6 +340,7 @@ export default class SaveManagedQueryModal {
|
|
|
324
340
|
}
|
|
325
341
|
|
|
326
342
|
private cancel() {
|
|
343
|
+
this.mouseDownOnOverlay = false;
|
|
327
344
|
this.close();
|
|
328
345
|
this.overlayEl.remove();
|
|
329
346
|
this.reject?.(new Error("cancelled"));
|
|
@@ -399,6 +416,7 @@ export default class SaveManagedQueryModal {
|
|
|
399
416
|
}
|
|
400
417
|
}
|
|
401
418
|
|
|
419
|
+
this.mouseDownOnOverlay = false;
|
|
402
420
|
this.close();
|
|
403
421
|
this.overlayEl.remove();
|
|
404
422
|
|
|
@@ -454,6 +472,7 @@ export default class SaveManagedQueryModal {
|
|
|
454
472
|
this.folderBrowsePath = this.folderPathEl.value.trim();
|
|
455
473
|
this.folderPickerErrorEl.textContent = "";
|
|
456
474
|
this.folderPickerListEl.innerHTML = "";
|
|
475
|
+
this.mouseDownOnOverlay = false;
|
|
457
476
|
|
|
458
477
|
document.body.appendChild(this.overlayEl);
|
|
459
478
|
this.open();
|
|
@@ -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
|
};
|
package/src/urlUtils.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a relative or absolute URL to a fully qualified URL with protocol and host.
|
|
3
|
+
* Uses the current page's protocol and host for relative URLs.
|
|
4
|
+
*
|
|
5
|
+
* @param url - The URL to resolve (can be relative like "/sparql", or absolute like "http://example.com/sparql")
|
|
6
|
+
* @returns The fully qualified URL with protocol and host
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // On page https://example.com/yasgui/
|
|
10
|
+
* resolveEndpointUrl("/sparql") // returns "https://example.com/sparql"
|
|
11
|
+
* resolveEndpointUrl("sparql") // returns "https://example.com/yasgui/sparql"
|
|
12
|
+
* resolveEndpointUrl("http://other.com/sparql") // returns "http://other.com/sparql"
|
|
13
|
+
*/
|
|
14
|
+
export function resolveEndpointUrl(url: string): string {
|
|
15
|
+
if (!url) return url;
|
|
16
|
+
|
|
17
|
+
// If URL already has a protocol (http: or https:), return as-is
|
|
18
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
19
|
+
return url;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Build the base URL using current page's protocol and host
|
|
23
|
+
let fullUrl = `${window.location.protocol}//${window.location.host}`;
|
|
24
|
+
|
|
25
|
+
if (url.startsWith("/")) {
|
|
26
|
+
// Absolute path (starts with /)
|
|
27
|
+
fullUrl += url;
|
|
28
|
+
} else {
|
|
29
|
+
// Relative path - join with current page's directory
|
|
30
|
+
let currentDirectory = window.location.pathname;
|
|
31
|
+
// If pathname does not end with "/", treat it as a file and use its directory
|
|
32
|
+
if (!currentDirectory.endsWith("/")) {
|
|
33
|
+
const lastSlashIndex = currentDirectory.lastIndexOf("/");
|
|
34
|
+
currentDirectory = lastSlashIndex >= 0 ? currentDirectory.substring(0, lastSlashIndex + 1) : "/";
|
|
35
|
+
}
|
|
36
|
+
fullUrl += currentDirectory + url;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return fullUrl;
|
|
40
|
+
}
|
package/src/version.ts
CHANGED