@matdata/yasgui 5.13.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 +2 -0
- package/build/ts/src/TabSettingsModal.js +92 -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 +142 -136
- package/build/yasgui.min.js.map +4 -4
- package/package.json +1 -1
- package/src/Tab.ts +30 -4
- package/src/TabSettingsModal.scss +57 -0
- package/src/TabSettingsModal.ts +129 -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;
|
|
@@ -290,6 +290,63 @@
|
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
.keyValueContainer {
|
|
294
|
+
display: flex;
|
|
295
|
+
flex-direction: column;
|
|
296
|
+
gap: 8px;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.keyValueRow {
|
|
300
|
+
display: grid;
|
|
301
|
+
grid-template-columns: 1fr 1fr auto;
|
|
302
|
+
gap: 8px;
|
|
303
|
+
align-items: center;
|
|
304
|
+
|
|
305
|
+
.keyInput,
|
|
306
|
+
.valueInput {
|
|
307
|
+
padding: 8px;
|
|
308
|
+
border: 1px solid var(--yasgui-input-border, #ccc);
|
|
309
|
+
border-radius: 4px;
|
|
310
|
+
font-size: 14px;
|
|
311
|
+
background-color: var(--yasgui-bg-secondary, white);
|
|
312
|
+
color: var(--yasgui-text-primary, #000);
|
|
313
|
+
|
|
314
|
+
&:focus {
|
|
315
|
+
outline: none;
|
|
316
|
+
border-color: var(--yasgui-input-focus, #337ab7);
|
|
317
|
+
box-shadow: 0 0 0 2px rgba(51, 122, 183, 0.1);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
&::placeholder {
|
|
321
|
+
color: var(--yasgui-text-secondary, #999);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.removeButton {
|
|
326
|
+
padding: 6px 10px;
|
|
327
|
+
background-color: var(--yasgui-danger-color, #d9534f);
|
|
328
|
+
color: white;
|
|
329
|
+
border: none;
|
|
330
|
+
border-radius: 4px;
|
|
331
|
+
cursor: pointer;
|
|
332
|
+
font-size: 14px;
|
|
333
|
+
transition: background-color 0.2s;
|
|
334
|
+
|
|
335
|
+
&:hover {
|
|
336
|
+
background-color: var(--yasgui-danger-hover, #c9302c);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
&:focus {
|
|
340
|
+
outline: none;
|
|
341
|
+
box-shadow: 0 0 0 2px rgba(217, 83, 79, 0.3);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
i {
|
|
345
|
+
pointer-events: none;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
293
350
|
.prefixTextarea {
|
|
294
351
|
width: 100%;
|
|
295
352
|
padding: 10px;
|
package/src/TabSettingsModal.ts
CHANGED
|
@@ -24,6 +24,9 @@ const AcceptHeaderGraphMap: { key: string; value: string }[] = [
|
|
|
24
24
|
{ key: "TSV", value: "text/tab-separated-values,*/*;q=0.9" },
|
|
25
25
|
];
|
|
26
26
|
|
|
27
|
+
// Type for key-value pairs (headers, args, etc.)
|
|
28
|
+
type KeyValuePair = { name: string; value: string };
|
|
29
|
+
|
|
27
30
|
// Default API Key header name
|
|
28
31
|
const DEFAULT_API_KEY_HEADER = "X-API-Key";
|
|
29
32
|
|
|
@@ -36,6 +39,7 @@ export default class TabSettingsModal {
|
|
|
36
39
|
private prefixButton!: HTMLButtonElement;
|
|
37
40
|
private prefixTextarea!: HTMLTextAreaElement;
|
|
38
41
|
private autoCaptureCheckbox!: HTMLInputElement;
|
|
42
|
+
private mouseDownOnOverlay = false;
|
|
39
43
|
private handleKeyDown = (e: KeyboardEvent) => {
|
|
40
44
|
if (e.key !== "Escape") return;
|
|
41
45
|
if (!this.modalOverlay.classList.contains("open")) return;
|
|
@@ -88,8 +92,21 @@ export default class TabSettingsModal {
|
|
|
88
92
|
// Modal overlay
|
|
89
93
|
this.modalOverlay = document.createElement("div");
|
|
90
94
|
addClass(this.modalOverlay, "tabSettingsModalOverlay");
|
|
91
|
-
|
|
92
|
-
//
|
|
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
|
+
});
|
|
93
110
|
|
|
94
111
|
// Modal content
|
|
95
112
|
this.modalContent = document.createElement("div");
|
|
@@ -909,10 +926,27 @@ export default class TabSettingsModal {
|
|
|
909
926
|
const config = this.tab.yasgui.persistentConfig.getEndpointConfig(endpoint);
|
|
910
927
|
const existingAuth = config?.authentication;
|
|
911
928
|
|
|
929
|
+
// Track mousedown for proper modal close behavior
|
|
930
|
+
let mouseDownOnOverlay = false;
|
|
931
|
+
|
|
912
932
|
// Create modal overlay
|
|
913
933
|
const authModalOverlay = document.createElement("div");
|
|
914
934
|
addClass(authModalOverlay, "authModalOverlay");
|
|
915
|
-
|
|
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
|
+
});
|
|
916
950
|
|
|
917
951
|
// Create modal content
|
|
918
952
|
const authModal = document.createElement("div");
|
|
@@ -1380,6 +1414,81 @@ export default class TabSettingsModal {
|
|
|
1380
1414
|
acceptGraphSelect.setAttribute("data-config", "acceptHeaderGraph");
|
|
1381
1415
|
acceptGraphSection.appendChild(acceptGraphSelect);
|
|
1382
1416
|
container.appendChild(acceptGraphSection);
|
|
1417
|
+
|
|
1418
|
+
// Custom Headers section
|
|
1419
|
+
const headersSection = this.createSection("Custom HTTP Headers");
|
|
1420
|
+
const headersContainer = document.createElement("div");
|
|
1421
|
+
addClass(headersContainer, "keyValueContainer");
|
|
1422
|
+
headersContainer.setAttribute("data-config", "headers");
|
|
1423
|
+
|
|
1424
|
+
// Get existing headers
|
|
1425
|
+
const existingHeaders = typeof reqConfig.headers === "function" ? {} : reqConfig.headers || {};
|
|
1426
|
+
const headerPairs: KeyValuePair[] = Object.entries(existingHeaders).map(([name, value]) => ({
|
|
1427
|
+
name,
|
|
1428
|
+
value,
|
|
1429
|
+
}));
|
|
1430
|
+
|
|
1431
|
+
// Draw existing headers
|
|
1432
|
+
headerPairs.forEach((pair, index) => {
|
|
1433
|
+
this.createKeyValueRow(headersContainer, pair, index);
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
// Add empty row for adding new headers
|
|
1437
|
+
this.createKeyValueRow(headersContainer, { name: "", value: "" }, headerPairs.length);
|
|
1438
|
+
|
|
1439
|
+
headersSection.appendChild(headersContainer);
|
|
1440
|
+
container.appendChild(headersSection);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
private createKeyValueRow(container: HTMLElement, pair: KeyValuePair, index: number) {
|
|
1444
|
+
const row = document.createElement("div");
|
|
1445
|
+
addClass(row, "keyValueRow");
|
|
1446
|
+
|
|
1447
|
+
const nameInput = document.createElement("input");
|
|
1448
|
+
nameInput.type = "text";
|
|
1449
|
+
nameInput.placeholder = "Header Name";
|
|
1450
|
+
nameInput.value = pair.name;
|
|
1451
|
+
addClass(nameInput, "keyInput");
|
|
1452
|
+
|
|
1453
|
+
const valueInput = document.createElement("input");
|
|
1454
|
+
valueInput.type = "text";
|
|
1455
|
+
valueInput.placeholder = "Header Value";
|
|
1456
|
+
valueInput.value = pair.value;
|
|
1457
|
+
addClass(valueInput, "valueInput");
|
|
1458
|
+
|
|
1459
|
+
const removeButton = document.createElement("button");
|
|
1460
|
+
removeButton.type = "button";
|
|
1461
|
+
removeButton.innerHTML = '<i class="fas fa-times"></i>';
|
|
1462
|
+
removeButton.title = "Remove header";
|
|
1463
|
+
addClass(removeButton, "removeButton");
|
|
1464
|
+
|
|
1465
|
+
// Auto-add new empty row when typing in the last row
|
|
1466
|
+
const onInput = () => {
|
|
1467
|
+
const allRows = container.querySelectorAll(".keyValueRow");
|
|
1468
|
+
const isLastRow = row === allRows[allRows.length - 1];
|
|
1469
|
+
|
|
1470
|
+
if (isLastRow && (nameInput.value.trim() || valueInput.value.trim())) {
|
|
1471
|
+
// Add a new empty row
|
|
1472
|
+
this.createKeyValueRow(container, { name: "", value: "" }, allRows.length);
|
|
1473
|
+
}
|
|
1474
|
+
};
|
|
1475
|
+
|
|
1476
|
+
nameInput.addEventListener("input", onInput);
|
|
1477
|
+
valueInput.addEventListener("input", onInput);
|
|
1478
|
+
|
|
1479
|
+
removeButton.onclick = () => {
|
|
1480
|
+
row.remove();
|
|
1481
|
+
// Ensure there's always at least one empty row
|
|
1482
|
+
const remainingRows = container.querySelectorAll(".keyValueRow");
|
|
1483
|
+
if (remainingRows.length === 0) {
|
|
1484
|
+
this.createKeyValueRow(container, { name: "", value: "" }, 0);
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
|
|
1488
|
+
row.appendChild(nameInput);
|
|
1489
|
+
row.appendChild(valueInput);
|
|
1490
|
+
row.appendChild(removeButton);
|
|
1491
|
+
container.appendChild(row);
|
|
1383
1492
|
}
|
|
1384
1493
|
|
|
1385
1494
|
private createSection(title: string): HTMLElement {
|
|
@@ -1438,6 +1547,7 @@ export default class TabSettingsModal {
|
|
|
1438
1547
|
}
|
|
1439
1548
|
|
|
1440
1549
|
public close() {
|
|
1550
|
+
this.mouseDownOnOverlay = false;
|
|
1441
1551
|
removeClass(this.modalOverlay, "open");
|
|
1442
1552
|
document.removeEventListener("keydown", this.handleKeyDown);
|
|
1443
1553
|
}
|
|
@@ -1546,6 +1656,22 @@ export default class TabSettingsModal {
|
|
|
1546
1656
|
updates[config] = (select as HTMLSelectElement).value;
|
|
1547
1657
|
}
|
|
1548
1658
|
});
|
|
1659
|
+
|
|
1660
|
+
// Save custom headers
|
|
1661
|
+
const headersContainer = requestContent.querySelector(".keyValueContainer[data-config='headers']");
|
|
1662
|
+
if (headersContainer) {
|
|
1663
|
+
const headers: { [key: string]: string } = {};
|
|
1664
|
+
const rows = headersContainer.querySelectorAll(".keyValueRow");
|
|
1665
|
+
rows.forEach((row) => {
|
|
1666
|
+
const nameInput = row.querySelector(".keyInput") as HTMLInputElement;
|
|
1667
|
+
const valueInput = row.querySelector(".valueInput") as HTMLInputElement;
|
|
1668
|
+
if (nameInput && valueInput && nameInput.value.trim()) {
|
|
1669
|
+
headers[nameInput.value.trim()] = valueInput.value;
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
updates.headers = headers;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1549
1675
|
this.tab.setRequestConfig(updates);
|
|
1550
1676
|
}
|
|
1551
1677
|
|
|
@@ -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
|
};
|