@matdata/yasgui 5.16.0 → 5.18.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.
Files changed (59) hide show
  1. package/build/ts/src/Tab.js +12 -2
  2. package/build/ts/src/Tab.js.map +1 -1
  3. package/build/ts/src/TabContextMenu.d.ts +1 -0
  4. package/build/ts/src/TabContextMenu.js +42 -0
  5. package/build/ts/src/TabContextMenu.js.map +1 -1
  6. package/build/ts/src/TabSettingsModal.d.ts +0 -1
  7. package/build/ts/src/TabSettingsModal.js +22 -25
  8. package/build/ts/src/TabSettingsModal.js.map +1 -1
  9. package/build/ts/src/index.d.ts +6 -0
  10. package/build/ts/src/index.js +42 -0
  11. package/build/ts/src/index.js.map +1 -1
  12. package/build/ts/src/prefixUtils.d.ts +1 -0
  13. package/build/ts/src/prefixUtils.js +23 -0
  14. package/build/ts/src/prefixUtils.js.map +1 -0
  15. package/build/ts/src/queryManagement/QueryBrowser.d.ts +8 -0
  16. package/build/ts/src/queryManagement/QueryBrowser.js +283 -116
  17. package/build/ts/src/queryManagement/QueryBrowser.js.map +1 -1
  18. package/build/ts/src/queryManagement/QueryHistoryModal.d.ts +35 -0
  19. package/build/ts/src/queryManagement/QueryHistoryModal.js +266 -0
  20. package/build/ts/src/queryManagement/QueryHistoryModal.js.map +1 -0
  21. package/build/ts/src/queryManagement/WorkspaceSettingsForm.js +8 -5
  22. package/build/ts/src/queryManagement/WorkspaceSettingsForm.js.map +1 -1
  23. package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.d.ts +1 -0
  24. package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.js +33 -0
  25. package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.js.map +1 -1
  26. package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.d.ts +1 -0
  27. package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.js +3 -0
  28. package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.js.map +1 -1
  29. package/build/ts/src/queryManagement/backends/WorkspaceBackend.d.ts +1 -0
  30. package/build/ts/src/queryManagement/countManagedQueries.d.ts +2 -0
  31. package/build/ts/src/queryManagement/countManagedQueries.js +35 -0
  32. package/build/ts/src/queryManagement/countManagedQueries.js.map +1 -0
  33. package/build/ts/src/tabNavigationHistory.d.ts +3 -0
  34. package/build/ts/src/tabNavigationHistory.js +13 -0
  35. package/build/ts/src/tabNavigationHistory.js.map +1 -0
  36. package/build/ts/src/version.d.ts +1 -1
  37. package/build/ts/src/version.js +1 -1
  38. package/build/yasgui.min.css +1 -1
  39. package/build/yasgui.min.css.map +3 -3
  40. package/build/yasgui.min.js +244 -232
  41. package/build/yasgui.min.js.map +4 -4
  42. package/package.json +3 -3
  43. package/src/Tab.ts +22 -3
  44. package/src/TabContextMenu.ts +42 -0
  45. package/src/TabSettingsModal.ts +22 -29
  46. package/src/index.ts +50 -0
  47. package/src/prefixUtils.ts +26 -0
  48. package/src/queryManagement/QueryBrowser.scss +85 -0
  49. package/src/queryManagement/QueryBrowser.ts +429 -213
  50. package/src/queryManagement/QueryHistoryModal.scss +239 -0
  51. package/src/queryManagement/QueryHistoryModal.ts +346 -0
  52. package/src/queryManagement/WorkspaceSettingsForm.ts +8 -5
  53. package/src/queryManagement/backends/GitWorkspaceBackend.ts +39 -0
  54. package/src/queryManagement/backends/SparqlWorkspaceBackend.ts +5 -0
  55. package/src/queryManagement/backends/WorkspaceBackend.ts +8 -0
  56. package/src/queryManagement/countManagedQueries.ts +28 -0
  57. package/src/tabNavigationHistory.ts +17 -0
  58. package/src/themes.scss +2 -0
  59. package/src/version.ts +1 -1
@@ -0,0 +1,239 @@
1
+ .queryHistoryModalOverlay {
2
+ display: none;
3
+ position: fixed;
4
+ top: 0;
5
+ left: 0;
6
+ right: 0;
7
+ bottom: 0;
8
+ background: var(--yasgui-overlay-bg, rgba(0, 0, 0, 0.5));
9
+ z-index: 10000;
10
+ align-items: center;
11
+ justify-content: center;
12
+
13
+ &.open {
14
+ display: flex;
15
+ }
16
+ }
17
+
18
+ .queryHistoryModal {
19
+ background: var(--yasgui-bg-primary, white);
20
+ border-radius: 8px;
21
+ width: 860px;
22
+ max-width: 95vw;
23
+ max-height: 90vh;
24
+ display: flex;
25
+ flex-direction: column;
26
+ overflow: hidden;
27
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
28
+ }
29
+
30
+ .queryHistoryModalHeader {
31
+ display: flex;
32
+ justify-content: space-between;
33
+ align-items: center;
34
+ padding: 16px;
35
+ border-bottom: 1px solid var(--yasgui-border-color, #e0e0e0);
36
+ flex-shrink: 0;
37
+
38
+ h2 {
39
+ margin: 0;
40
+ font-size: 18px;
41
+ font-weight: 600;
42
+ color: var(--yasgui-text-primary, #000);
43
+ overflow: hidden;
44
+ text-overflow: ellipsis;
45
+ white-space: nowrap;
46
+ }
47
+
48
+ .closeButton {
49
+ background: none;
50
+ border: none;
51
+ font-size: 28px;
52
+ line-height: 1;
53
+ cursor: pointer;
54
+ color: var(--yasgui-text-secondary, #666);
55
+ padding: 0;
56
+ width: 32px;
57
+ height: 32px;
58
+ flex-shrink: 0;
59
+
60
+ &:hover {
61
+ color: var(--yasgui-text-primary, #333);
62
+ }
63
+ }
64
+ }
65
+
66
+ .queryHistoryModalBody {
67
+ display: flex;
68
+ flex: 1;
69
+ overflow: hidden;
70
+ min-height: 0;
71
+ }
72
+
73
+ .queryHistoryVersionsPanel {
74
+ width: 280px;
75
+ min-width: 200px;
76
+ flex-shrink: 0;
77
+ border-right: 1px solid var(--yasgui-border-color, #e0e0e0);
78
+ display: flex;
79
+ flex-direction: column;
80
+ overflow: hidden;
81
+ }
82
+
83
+ .queryHistoryPreviewPanel {
84
+ flex: 1;
85
+ display: flex;
86
+ flex-direction: column;
87
+ overflow: hidden;
88
+ min-width: 0;
89
+ }
90
+
91
+ .queryHistoryPanelTitle {
92
+ padding: 10px 12px 8px;
93
+ font-size: 12px;
94
+ font-weight: 600;
95
+ text-transform: uppercase;
96
+ letter-spacing: 0.05em;
97
+ color: var(--yasgui-text-secondary, #666);
98
+ border-bottom: 1px solid var(--yasgui-border-color, #e0e0e0);
99
+ flex-shrink: 0;
100
+ }
101
+
102
+ .queryHistoryLoading,
103
+ .queryHistoryError,
104
+ .queryHistoryEmpty {
105
+ padding: 12px;
106
+ font-size: 13px;
107
+ color: var(--yasgui-text-secondary, #666);
108
+ }
109
+
110
+ .queryHistoryError {
111
+ color: var(--yasgui-error-color, #c00);
112
+ }
113
+
114
+ .queryHistoryVersionsList {
115
+ flex: 1;
116
+ overflow-y: auto;
117
+ padding: 6px 0;
118
+ }
119
+
120
+ .queryHistoryVersionItem {
121
+ padding: 10px 12px;
122
+ cursor: pointer;
123
+ border-bottom: 1px solid var(--yasgui-border-color, #e0e0e0);
124
+ outline: none;
125
+
126
+ &:last-child {
127
+ border-bottom: none;
128
+ }
129
+
130
+ &:hover {
131
+ background: var(--yasgui-bg-secondary, #f5f5f5);
132
+ }
133
+
134
+ &:focus-visible {
135
+ background: var(--yasgui-bg-secondary, #f5f5f5);
136
+ box-shadow: inset 0 0 0 2px var(--yasgui-accent-color, #337ab7);
137
+ }
138
+
139
+ &--selected {
140
+ background: var(--yasgui-accent-bg, #e8f0fe);
141
+ border-left: 3px solid var(--yasgui-accent-color, #337ab7);
142
+ padding-left: 9px;
143
+
144
+ &:hover {
145
+ background: var(--yasgui-accent-bg, #e8f0fe);
146
+ }
147
+ }
148
+ }
149
+
150
+ .queryHistoryVersionDate {
151
+ font-size: 13px;
152
+ font-weight: 600;
153
+ color: var(--yasgui-text-primary, #000);
154
+ }
155
+
156
+ .queryHistoryVersionMeta {
157
+ font-size: 12px;
158
+ color: var(--yasgui-text-secondary, #666);
159
+ margin-top: 2px;
160
+ overflow: hidden;
161
+ text-overflow: ellipsis;
162
+ white-space: nowrap;
163
+ }
164
+
165
+ .queryHistoryVersionMessage {
166
+ font-size: 12px;
167
+ color: var(--yasgui-text-secondary, #555);
168
+ margin-top: 2px;
169
+ overflow: hidden;
170
+ text-overflow: ellipsis;
171
+ white-space: nowrap;
172
+ font-style: italic;
173
+ }
174
+
175
+ .queryHistoryPreviewEmpty {
176
+ padding: 24px 16px;
177
+ font-size: 13px;
178
+ color: var(--yasgui-text-secondary, #999);
179
+ text-align: center;
180
+ flex: 1;
181
+ display: flex;
182
+ align-items: center;
183
+ justify-content: center;
184
+ }
185
+
186
+ .queryHistoryPreviewContent {
187
+ flex: 1;
188
+ margin: 0;
189
+ padding: 12px 16px;
190
+ overflow: auto;
191
+ font-family: "SFMono-Regular", "Consolas", "Liberation Mono", monospace;
192
+ font-size: 12px;
193
+ line-height: 1.5;
194
+ color: var(--yasgui-text-primary, #000);
195
+ background: var(--yasgui-bg-secondary, #f7f7f7);
196
+ white-space: pre-wrap;
197
+ word-break: break-word;
198
+ border-top: 1px solid var(--yasgui-border-color, #e0e0e0);
199
+ }
200
+
201
+ .queryHistoryModalFooter {
202
+ display: flex;
203
+ justify-content: flex-end;
204
+ gap: 8px;
205
+ padding: 12px 16px;
206
+ border-top: 1px solid var(--yasgui-border-color, #e0e0e0);
207
+ flex-shrink: 0;
208
+
209
+ button {
210
+ padding: 8px 16px;
211
+ border-radius: 4px;
212
+ border: 1px solid var(--yasgui-border-color, #e0e0e0);
213
+ background: var(--yasgui-bg-primary, white);
214
+ color: var(--yasgui-text-primary, #000);
215
+ font-size: 14px;
216
+ cursor: pointer;
217
+
218
+ &:hover:not(:disabled) {
219
+ background: var(--yasgui-bg-secondary, #f5f5f5);
220
+ }
221
+
222
+ &:disabled {
223
+ opacity: 0.5;
224
+ cursor: not-allowed;
225
+ }
226
+
227
+ &.primary {
228
+ background: var(--yasgui-accent-color, #337ab7);
229
+ color: #fff;
230
+ border-color: var(--yasgui-accent-color, #337ab7);
231
+ font-weight: 600;
232
+
233
+ &:hover:not(:disabled) {
234
+ background: var(--yasgui-accent-color-hover, #2a6496);
235
+ border-color: var(--yasgui-accent-color-hover, #2a6496);
236
+ }
237
+ }
238
+ }
239
+ }
@@ -0,0 +1,346 @@
1
+ import type Yasgui from "../index";
2
+ import { addClass, removeClass } from "@matdata/yasgui-utils";
3
+ import { getWorkspaceBackend } from "./backends/getWorkspaceBackend";
4
+ import type { BackendType, ManagedTabMetadata, ReadResult, VersionInfo, VersionRef } from "./types";
5
+ import { hashQueryText } from "./textHash";
6
+ import { getEndpointToAutoSwitch } from "./openManagedQuery";
7
+ import { asWorkspaceBackendError } from "./backends/errors";
8
+
9
+ import "./QueryHistoryModal.scss";
10
+
11
+ export default class QueryHistoryModal {
12
+ private yasgui: Yasgui;
13
+
14
+ private overlayEl: HTMLDivElement;
15
+ private modalEl: HTMLDivElement;
16
+ private titleEl: HTMLHeadingElement;
17
+ private versionsListEl: HTMLDivElement;
18
+ private loadingEl: HTMLDivElement;
19
+ private errorEl: HTMLDivElement;
20
+ private previewEmptyEl: HTMLDivElement;
21
+ private previewContentEl: HTMLPreElement;
22
+ private openTabBtn: HTMLButtonElement;
23
+
24
+ private workspaceId?: string;
25
+ private backend?: ReturnType<typeof getWorkspaceBackend>;
26
+ private backendType?: BackendType;
27
+ private queryId?: string;
28
+ private queryLabel?: string;
29
+
30
+ private selectedVersionId?: string;
31
+ private selectedVersionRead?: ReadResult;
32
+
33
+ private mouseDownOnOverlay = false;
34
+ private resolve?: () => void;
35
+
36
+ constructor(yasgui: Yasgui) {
37
+ this.yasgui = yasgui;
38
+
39
+ this.overlayEl = document.createElement("div");
40
+ addClass(this.overlayEl, "queryHistoryModalOverlay");
41
+
42
+ this.modalEl = document.createElement("div");
43
+ addClass(this.modalEl, "queryHistoryModal");
44
+ this.modalEl.setAttribute("role", "dialog");
45
+ this.modalEl.setAttribute("aria-modal", "true");
46
+ this.modalEl.addEventListener("click", (e) => e.stopPropagation());
47
+
48
+ // ── Header ───────────────────────────────────────────────────────────────
49
+ const headerEl = document.createElement("div");
50
+ addClass(headerEl, "queryHistoryModalHeader");
51
+
52
+ this.titleEl = document.createElement("h2");
53
+ this.titleEl.textContent = "Query History";
54
+
55
+ const closeBtn = document.createElement("button");
56
+ closeBtn.type = "button";
57
+ closeBtn.className = "closeButton";
58
+ closeBtn.setAttribute("aria-label", "Close");
59
+ closeBtn.innerHTML = "&times;";
60
+ closeBtn.addEventListener("click", () => this.close());
61
+
62
+ headerEl.appendChild(this.titleEl);
63
+ headerEl.appendChild(closeBtn);
64
+
65
+ // ── Body ─────────────────────────────────────────────────────────────────
66
+ const bodyEl = document.createElement("div");
67
+ addClass(bodyEl, "queryHistoryModalBody");
68
+
69
+ // Left panel – version list
70
+ const versionsPanel = document.createElement("div");
71
+ addClass(versionsPanel, "queryHistoryVersionsPanel");
72
+
73
+ const versionsPanelTitle = document.createElement("div");
74
+ addClass(versionsPanelTitle, "queryHistoryPanelTitle");
75
+ versionsPanelTitle.textContent = "Versions";
76
+
77
+ this.loadingEl = document.createElement("div");
78
+ addClass(this.loadingEl, "queryHistoryLoading");
79
+ this.loadingEl.textContent = "Loading versions…";
80
+
81
+ this.errorEl = document.createElement("div");
82
+ addClass(this.errorEl, "queryHistoryError");
83
+
84
+ this.versionsListEl = document.createElement("div");
85
+ addClass(this.versionsListEl, "queryHistoryVersionsList");
86
+
87
+ versionsPanel.appendChild(versionsPanelTitle);
88
+ versionsPanel.appendChild(this.loadingEl);
89
+ versionsPanel.appendChild(this.errorEl);
90
+ versionsPanel.appendChild(this.versionsListEl);
91
+
92
+ // Right panel – preview
93
+ const previewPanel = document.createElement("div");
94
+ addClass(previewPanel, "queryHistoryPreviewPanel");
95
+
96
+ const previewPanelTitle = document.createElement("div");
97
+ addClass(previewPanelTitle, "queryHistoryPanelTitle");
98
+ previewPanelTitle.textContent = "Preview";
99
+
100
+ this.previewEmptyEl = document.createElement("div");
101
+ addClass(this.previewEmptyEl, "queryHistoryPreviewEmpty");
102
+ this.previewEmptyEl.textContent = "Select a version to preview";
103
+
104
+ this.previewContentEl = document.createElement("pre");
105
+ addClass(this.previewContentEl, "queryHistoryPreviewContent");
106
+
107
+ previewPanel.appendChild(previewPanelTitle);
108
+ previewPanel.appendChild(this.previewEmptyEl);
109
+ previewPanel.appendChild(this.previewContentEl);
110
+
111
+ bodyEl.appendChild(versionsPanel);
112
+ bodyEl.appendChild(previewPanel);
113
+
114
+ // ── Footer ────────────────────────────────────────────────────────────────
115
+ const footerEl = document.createElement("div");
116
+ addClass(footerEl, "queryHistoryModalFooter");
117
+
118
+ const cancelBtn = document.createElement("button");
119
+ cancelBtn.type = "button";
120
+ cancelBtn.textContent = "Close";
121
+ cancelBtn.addEventListener("click", () => this.close());
122
+
123
+ this.openTabBtn = document.createElement("button");
124
+ this.openTabBtn.type = "button";
125
+ this.openTabBtn.textContent = "Open in tab";
126
+ addClass(this.openTabBtn, "primary");
127
+ this.openTabBtn.disabled = true;
128
+ this.openTabBtn.addEventListener("click", () => void this.openSelectedVersionInTab());
129
+
130
+ footerEl.appendChild(cancelBtn);
131
+ footerEl.appendChild(this.openTabBtn);
132
+
133
+ // ── Assemble ──────────────────────────────────────────────────────────────
134
+ this.modalEl.appendChild(headerEl);
135
+ this.modalEl.appendChild(bodyEl);
136
+ this.modalEl.appendChild(footerEl);
137
+
138
+ this.overlayEl.appendChild(this.modalEl);
139
+
140
+ this.overlayEl.addEventListener("mousedown", (e) => {
141
+ if (e.target === this.overlayEl) this.mouseDownOnOverlay = true;
142
+ });
143
+ this.overlayEl.addEventListener("mouseup", (e) => {
144
+ if (e.target === this.overlayEl && this.mouseDownOnOverlay) this.close();
145
+ this.mouseDownOnOverlay = false;
146
+ });
147
+ }
148
+
149
+ /** Bound keydown handler stored so it can be removed on close. */
150
+ private readonly onKeyDown = (e: KeyboardEvent) => {
151
+ if (e.key === "Escape") {
152
+ e.stopPropagation();
153
+ this.close();
154
+ }
155
+ };
156
+
157
+ /**
158
+ * Show the history modal for the given managed query.
159
+ * Returns a promise that resolves when the modal is closed.
160
+ */
161
+ public show(
162
+ workspaceId: string,
163
+ backend: ReturnType<typeof getWorkspaceBackend>,
164
+ backendType: BackendType,
165
+ queryId: string,
166
+ queryLabel: string,
167
+ ): Promise<void> {
168
+ this.workspaceId = workspaceId;
169
+ this.backend = backend;
170
+ this.backendType = backendType;
171
+ this.queryId = queryId;
172
+ this.queryLabel = queryLabel;
173
+
174
+ // Reset state
175
+ this.selectedVersionId = undefined;
176
+ this.selectedVersionRead = undefined;
177
+ this.versionsListEl.innerHTML = "";
178
+ this.previewContentEl.textContent = "";
179
+ this.previewContentEl.style.display = "none";
180
+ this.previewEmptyEl.style.display = "block";
181
+ this.loadingEl.style.display = "block";
182
+ this.errorEl.textContent = "";
183
+ this.errorEl.style.display = "none";
184
+ this.openTabBtn.disabled = true;
185
+ this.titleEl.textContent = `History: ${queryLabel}`;
186
+ this.mouseDownOnOverlay = false;
187
+
188
+ document.body.appendChild(this.overlayEl);
189
+ addClass(this.overlayEl, "open");
190
+ document.addEventListener("keydown", this.onKeyDown);
191
+
192
+ void this.loadVersions();
193
+
194
+ return new Promise<void>((resolve) => {
195
+ this.resolve = resolve;
196
+ });
197
+ }
198
+
199
+ private close() {
200
+ document.removeEventListener("keydown", this.onKeyDown);
201
+ removeClass(this.overlayEl, "open");
202
+ this.overlayEl.remove();
203
+ const resolve = this.resolve;
204
+ this.resolve = undefined;
205
+ resolve?.();
206
+ }
207
+
208
+ private async loadVersions() {
209
+ if (!this.backend || !this.queryId) return;
210
+
211
+ try {
212
+ const versions = await this.backend.listVersions(this.queryId);
213
+ this.loadingEl.style.display = "none";
214
+ this.renderVersions(versions);
215
+ } catch (err) {
216
+ this.loadingEl.style.display = "none";
217
+ this.errorEl.textContent = `Failed to load versions: ${asWorkspaceBackendError(err).message}`;
218
+ this.errorEl.style.display = "block";
219
+ }
220
+ }
221
+
222
+ private renderVersions(versions: VersionInfo[]) {
223
+ this.versionsListEl.innerHTML = "";
224
+
225
+ if (versions.length === 0) {
226
+ const emptyEl = document.createElement("div");
227
+ addClass(emptyEl, "queryHistoryEmpty");
228
+ emptyEl.textContent = "No version history found.";
229
+ this.versionsListEl.appendChild(emptyEl);
230
+ return;
231
+ }
232
+
233
+ for (const version of versions) {
234
+ const item = document.createElement("div");
235
+ addClass(item, "queryHistoryVersionItem");
236
+ item.setAttribute("role", "button");
237
+ item.setAttribute("tabindex", "0");
238
+
239
+ const dateEl = document.createElement("div");
240
+ addClass(dateEl, "queryHistoryVersionDate");
241
+ dateEl.textContent = this.formatDate(version.createdAt);
242
+ item.appendChild(dateEl);
243
+
244
+ if (version.author) {
245
+ const authorEl = document.createElement("div");
246
+ addClass(authorEl, "queryHistoryVersionMeta");
247
+ authorEl.textContent = version.author;
248
+ item.appendChild(authorEl);
249
+ }
250
+
251
+ if (version.message) {
252
+ const msgEl = document.createElement("div");
253
+ addClass(msgEl, "queryHistoryVersionMessage");
254
+ msgEl.textContent = version.message;
255
+ item.appendChild(msgEl);
256
+ }
257
+
258
+ const selectFn = () => void this.selectVersion(version.id, item);
259
+ item.addEventListener("click", selectFn);
260
+ item.addEventListener("keydown", (e) => {
261
+ if (e.key === "Enter" || e.key === " ") {
262
+ e.preventDefault();
263
+ selectFn();
264
+ }
265
+ });
266
+
267
+ this.versionsListEl.appendChild(item);
268
+ }
269
+ }
270
+
271
+ private async selectVersion(versionId: string, itemEl: HTMLElement) {
272
+ // Highlight the selected item
273
+ this.versionsListEl.querySelectorAll(".queryHistoryVersionItem").forEach((el) => {
274
+ removeClass(el as HTMLElement, "queryHistoryVersionItem--selected");
275
+ });
276
+ addClass(itemEl, "queryHistoryVersionItem--selected");
277
+
278
+ // Reset selection state and show loading in preview
279
+ this.selectedVersionId = undefined;
280
+ this.selectedVersionRead = undefined;
281
+ this.openTabBtn.disabled = true;
282
+ this.previewEmptyEl.style.display = "none";
283
+ this.previewContentEl.style.display = "block";
284
+ this.previewContentEl.textContent = "Loading…";
285
+
286
+ if (!this.backend || !this.queryId) return;
287
+
288
+ try {
289
+ const read = await this.backend.readVersion(this.queryId, versionId);
290
+ this.selectedVersionId = versionId;
291
+ this.selectedVersionRead = read;
292
+ this.previewContentEl.textContent = read.queryText;
293
+ this.openTabBtn.disabled = false;
294
+ } catch (err) {
295
+ this.previewContentEl.textContent = `Error: ${asWorkspaceBackendError(err).message}`;
296
+ }
297
+ }
298
+
299
+ private async openSelectedVersionInTab() {
300
+ if (
301
+ !this.selectedVersionId ||
302
+ !this.selectedVersionRead ||
303
+ !this.workspaceId ||
304
+ !this.backendType ||
305
+ !this.queryId
306
+ ) {
307
+ return;
308
+ }
309
+
310
+ const { workspaceId, backendType, queryId, queryLabel } = this;
311
+ const read = this.selectedVersionRead;
312
+
313
+ const tab = this.yasgui.addTab(true);
314
+ if (queryLabel) tab.setName(queryLabel);
315
+
316
+ const endpoint = getEndpointToAutoSwitch(backendType, read);
317
+ if (endpoint) tab.setEndpoint(endpoint);
318
+
319
+ tab.setQuery(read.queryText);
320
+
321
+ const versionRef: VersionRef | undefined = this.versionRefFromVersionTag(backendType, read.versionTag);
322
+
323
+ const managedMetadata: ManagedTabMetadata = {
324
+ workspaceId,
325
+ backendType,
326
+ queryRef: backendType === "git" ? { path: queryId } : { managedQueryIri: queryId },
327
+ lastSavedVersionRef: versionRef,
328
+ lastSavedTextHash: hashQueryText(read.queryText),
329
+ };
330
+ tab.setManagedQueryMetadata(managedMetadata);
331
+
332
+ this.close();
333
+ }
334
+
335
+ private versionRefFromVersionTag(backendType: BackendType, versionTag: string | undefined): VersionRef | undefined {
336
+ if (!versionTag) return undefined;
337
+ if (backendType === "git") return { commitSha: versionTag };
338
+ return { managedQueryVersionIri: versionTag };
339
+ }
340
+
341
+ private formatDate(isoDate: string): string {
342
+ const d = new Date(isoDate);
343
+ if (isNaN(d.getTime())) return "Unknown date";
344
+ return d.toLocaleString();
345
+ }
346
+ }
@@ -4,6 +4,7 @@ import { addClass } from "@matdata/yasgui-utils";
4
4
  import { validateWorkspaceConfig } from "./validateWorkspaceConfig";
5
5
  import { getWorkspaceBackend } from "./backends/getWorkspaceBackend";
6
6
  import { asWorkspaceBackendError } from "./backends/errors";
7
+ import { countManagedQueries } from "./countManagedQueries";
7
8
 
8
9
  export interface WorkspaceSettingsFormOptions {
9
10
  persistentConfig: PersistentConfig;
@@ -202,7 +203,9 @@ ORDER BY ?workspace`;
202
203
  status.textContent = "Validating…";
203
204
  const backend = getWorkspaceBackend(workspace, { persistentConfig: this.options.persistentConfig });
204
205
  await backend.validateAccess();
205
- status.textContent = "✓ Access OK";
206
+ const queryCount = await countManagedQueries(backend);
207
+ const queryLabel = queryCount === 1 ? "query" : "queries";
208
+ status.textContent = `✓ Access OK (${queryCount} saved managed ${queryLabel})`;
206
209
  } catch (e) {
207
210
  const err = asWorkspaceBackendError(e);
208
211
  status.textContent = err.message;
@@ -359,14 +362,14 @@ ORDER BY ?workspace`;
359
362
  const typeSelect = document.createElement("select");
360
363
  typeSelect.setAttribute("aria-label", "Workspace type");
361
364
  addClass(typeSelect, "settingsSelect");
362
- const optGit = document.createElement("option");
363
- optGit.value = "git";
364
- optGit.textContent = "Git";
365
365
  const optSparql = document.createElement("option");
366
366
  optSparql.value = "sparql";
367
367
  optSparql.textContent = "SPARQL";
368
- typeSelect.appendChild(optGit);
368
+ const optGit = document.createElement("option");
369
+ optGit.value = "git";
370
+ optGit.textContent = "Git";
369
371
  typeSelect.appendChild(optSparql);
372
+ typeSelect.appendChild(optGit);
370
373
 
371
374
  if (existing) {
372
375
  typeSelect.value = existing.type;
@@ -2,6 +2,7 @@ import type { GitWorkspaceConfig, FolderEntry, ReadResult, VersionInfo, WriteQue
2
2
  import type { WorkspaceBackend } from "./WorkspaceBackend";
3
3
  import { WorkspaceBackendError } from "./errors";
4
4
  import { normalizeQueryFilename } from "../normalizeQueryFilename";
5
+ import { parseGitRemote } from "./gitRemote";
5
6
 
6
7
  export interface GitProviderClient {
7
8
  validateAccess(config: GitWorkspaceConfig): Promise<void>;
@@ -112,4 +113,42 @@ export default class GitWorkspaceBackend implements WorkspaceBackend {
112
113
 
113
114
  await this.client.deleteQuery(this.config, queryId);
114
115
  }
116
+
117
+ getQueryUri(queryId: string): string | undefined {
118
+ try {
119
+ const { host, repoPath } = parseGitRemote(this.config.remoteUrl);
120
+ const parts = repoPath.split("/").filter(Boolean);
121
+ const [owner, repoName] = parts;
122
+ if (!owner || !repoName) return undefined;
123
+
124
+ const filePath = [this.config.rootPath, queryId].filter(Boolean).join("/");
125
+ const branch = this.config.branch?.trim() || "HEAD";
126
+ const provider = this.config.provider;
127
+
128
+ const isGithub =
129
+ provider === "github" ||
130
+ ((!provider || provider === "auto") &&
131
+ (host === "github.com" || (host.includes("github") && !host.includes("gitlab"))));
132
+ if (isGithub) {
133
+ return `https://${host}/${owner}/${repoName}/blob/${encodeURIComponent(branch)}/${filePath}`;
134
+ }
135
+
136
+ const isGitlab =
137
+ provider === "gitlab" ||
138
+ ((!provider || provider === "auto") && (host === "gitlab.com" || host.includes("gitlab")));
139
+ if (isGitlab) {
140
+ return `https://${host}/${repoPath}/-/blob/${encodeURIComponent(branch)}/${filePath}`;
141
+ }
142
+
143
+ const isBitbucket = provider === "bitbucket" || ((!provider || provider === "auto") && host === "bitbucket.org");
144
+ if (isBitbucket) {
145
+ return `https://bitbucket.org/${owner}/${repoName}/src/${encodeURIComponent(branch)}/${filePath}`;
146
+ }
147
+
148
+ // Gitea (and other self-hosted providers)
149
+ return `https://${host}/${owner}/${repoName}/src/branch/${encodeURIComponent(branch)}/${filePath}`;
150
+ } catch {
151
+ return undefined;
152
+ }
153
+ }
115
154
  }
@@ -795,4 +795,9 @@ LIMIT 1`;
795
795
  description: row.description?.value,
796
796
  };
797
797
  }
798
+
799
+ getQueryUri(queryId: string): string | undefined {
800
+ // In a SPARQL workspace the queryId is the managed-query IRI itself.
801
+ return queryId || undefined;
802
+ }
798
803
  }
@@ -44,4 +44,12 @@ export interface WorkspaceBackend {
44
44
  * Optional: Delete a folder and everything inside it (recursive).
45
45
  */
46
46
  deleteFolder?(folderId: string): Promise<void>;
47
+
48
+ /**
49
+ * Optional: Return a URI for the given query.
50
+ * - For SPARQL workspaces this is the managed-query IRI stored in the triplestore.
51
+ * - For git workspaces this is the browser-accessible URL to the file in the repository.
52
+ * Returns `undefined` when no meaningful URI can be determined.
53
+ */
54
+ getQueryUri?(queryId: string): string | undefined;
47
55
  }