@matdata/yasgui 5.10.0 → 5.11.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/README.md +6 -2
- package/build/ts/src/PersistentConfig.d.ts +10 -0
- package/build/ts/src/PersistentConfig.js +40 -0
- package/build/ts/src/PersistentConfig.js.map +1 -1
- package/build/ts/src/Tab.d.ts +17 -0
- package/build/ts/src/Tab.js +372 -15
- package/build/ts/src/Tab.js.map +1 -1
- package/build/ts/src/TabContextMenu.d.ts +1 -0
- package/build/ts/src/TabContextMenu.js +17 -0
- package/build/ts/src/TabContextMenu.js.map +1 -1
- package/build/ts/src/TabElements.d.ts +3 -0
- package/build/ts/src/TabElements.js +97 -28
- package/build/ts/src/TabElements.js.map +1 -1
- package/build/ts/src/TabSettingsModal.d.ts +2 -0
- package/build/ts/src/TabSettingsModal.js +44 -19
- package/build/ts/src/TabSettingsModal.js.map +1 -1
- package/build/ts/src/index.d.ts +3 -0
- package/build/ts/src/index.js +4 -0
- package/build/ts/src/index.js.map +1 -1
- package/build/ts/src/queryManagement/QueryBrowser.d.ts +64 -0
- package/build/ts/src/queryManagement/QueryBrowser.js +914 -0
- package/build/ts/src/queryManagement/QueryBrowser.js.map +1 -0
- package/build/ts/src/queryManagement/SaveManagedQueryModal.d.ts +55 -0
- package/build/ts/src/queryManagement/SaveManagedQueryModal.js +451 -0
- package/build/ts/src/queryManagement/SaveManagedQueryModal.js.map +1 -0
- package/build/ts/src/queryManagement/WorkspaceSettingsForm.d.ts +16 -0
- package/build/ts/src/queryManagement/WorkspaceSettingsForm.js +452 -0
- package/build/ts/src/queryManagement/WorkspaceSettingsForm.js.map +1 -0
- package/build/ts/src/queryManagement/backends/BaseGitProviderClient.d.ts +19 -0
- package/build/ts/src/queryManagement/backends/BaseGitProviderClient.js +77 -0
- package/build/ts/src/queryManagement/backends/BaseGitProviderClient.js.map +1 -0
- package/build/ts/src/queryManagement/backends/BitbucketProviderClient.d.ts +16 -0
- package/build/ts/src/queryManagement/backends/BitbucketProviderClient.js +269 -0
- package/build/ts/src/queryManagement/backends/BitbucketProviderClient.js.map +1 -0
- package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.d.ts +26 -0
- package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.js +93 -0
- package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.js.map +1 -0
- package/build/ts/src/queryManagement/backends/GiteaProviderClient.d.ts +14 -0
- package/build/ts/src/queryManagement/backends/GiteaProviderClient.js +244 -0
- package/build/ts/src/queryManagement/backends/GiteaProviderClient.js.map +1 -0
- package/build/ts/src/queryManagement/backends/GithubProviderClient.d.ts +14 -0
- package/build/ts/src/queryManagement/backends/GithubProviderClient.js +252 -0
- package/build/ts/src/queryManagement/backends/GithubProviderClient.js.map +1 -0
- package/build/ts/src/queryManagement/backends/GitlabProviderClient.d.ts +16 -0
- package/build/ts/src/queryManagement/backends/GitlabProviderClient.js +246 -0
- package/build/ts/src/queryManagement/backends/GitlabProviderClient.js.map +1 -0
- package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.d.ts +21 -0
- package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.js +175 -0
- package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.js.map +1 -0
- package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.d.ts +28 -0
- package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.js +687 -0
- package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.js.map +1 -0
- package/build/ts/src/queryManagement/backends/WorkspaceBackend.d.ts +15 -0
- package/build/ts/src/queryManagement/backends/WorkspaceBackend.js +2 -0
- package/build/ts/src/queryManagement/backends/WorkspaceBackend.js.map +1 -0
- package/build/ts/src/queryManagement/backends/errors.d.ts +7 -0
- package/build/ts/src/queryManagement/backends/errors.js +18 -0
- package/build/ts/src/queryManagement/backends/errors.js.map +1 -0
- package/build/ts/src/queryManagement/backends/getWorkspaceBackend.d.ts +8 -0
- package/build/ts/src/queryManagement/backends/getWorkspaceBackend.js +114 -0
- package/build/ts/src/queryManagement/backends/getWorkspaceBackend.js.map +1 -0
- package/build/ts/src/queryManagement/backends/gitRemote.d.ts +5 -0
- package/build/ts/src/queryManagement/backends/gitRemote.js +40 -0
- package/build/ts/src/queryManagement/backends/gitRemote.js.map +1 -0
- package/build/ts/src/queryManagement/browserFilter.d.ts +2 -0
- package/build/ts/src/queryManagement/browserFilter.js +7 -0
- package/build/ts/src/queryManagement/browserFilter.js.map +1 -0
- package/build/ts/src/queryManagement/index.d.ts +13 -0
- package/build/ts/src/queryManagement/index.js +14 -0
- package/build/ts/src/queryManagement/index.js.map +1 -0
- package/build/ts/src/queryManagement/normalizeQueryFilename.d.ts +1 -0
- package/build/ts/src/queryManagement/normalizeQueryFilename.js +10 -0
- package/build/ts/src/queryManagement/normalizeQueryFilename.js.map +1 -0
- package/build/ts/src/queryManagement/openManagedQuery.d.ts +15 -0
- package/build/ts/src/queryManagement/openManagedQuery.js +27 -0
- package/build/ts/src/queryManagement/openManagedQuery.js.map +1 -0
- package/build/ts/src/queryManagement/saveManagedQuery.d.ts +20 -0
- package/build/ts/src/queryManagement/saveManagedQuery.js +109 -0
- package/build/ts/src/queryManagement/saveManagedQuery.js.map +1 -0
- package/build/ts/src/queryManagement/textHash.d.ts +2 -0
- package/build/ts/src/queryManagement/textHash.js +13 -0
- package/build/ts/src/queryManagement/textHash.js.map +1 -0
- package/build/ts/src/queryManagement/types.d.ts +76 -0
- package/build/ts/src/queryManagement/types.js +2 -0
- package/build/ts/src/queryManagement/types.js.map +1 -0
- package/build/ts/src/queryManagement/validateWorkspaceConfig.d.ts +6 -0
- package/build/ts/src/queryManagement/validateWorkspaceConfig.js +33 -0
- package/build/ts/src/queryManagement/validateWorkspaceConfig.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 +10 -1
- package/build/yasgui.min.css.map +3 -3
- package/build/yasgui.min.js +398 -172
- package/build/yasgui.min.js.map +4 -4
- package/package.json +1 -1
- package/src/PersistentConfig.ts +61 -0
- package/src/Tab.ts +431 -20
- package/src/TabContextMenu.ts +10 -0
- package/src/TabElements.scss +46 -7
- package/src/TabElements.ts +95 -27
- package/src/TabSettingsModal.scss +102 -5
- package/src/TabSettingsModal.ts +48 -25
- package/src/endpointSelect.scss +2 -3
- package/src/index.scss +4 -0
- package/src/index.ts +7 -0
- package/src/queryManagement/QueryBrowser.scss +418 -0
- package/src/queryManagement/QueryBrowser.ts +1079 -0
- package/src/queryManagement/SaveManagedQueryModal.scss +245 -0
- package/src/queryManagement/SaveManagedQueryModal.ts +554 -0
- package/src/queryManagement/WorkspaceSettingsForm.ts +546 -0
- package/src/queryManagement/backends/BaseGitProviderClient.ts +124 -0
- package/src/queryManagement/backends/BitbucketProviderClient.ts +403 -0
- package/src/queryManagement/backends/GitWorkspaceBackend.ts +96 -0
- package/src/queryManagement/backends/GiteaProviderClient.ts +316 -0
- package/src/queryManagement/backends/GithubProviderClient.ts +319 -0
- package/src/queryManagement/backends/GitlabProviderClient.ts +327 -0
- package/src/queryManagement/backends/InMemoryWorkspaceBackend.ts +175 -0
- package/src/queryManagement/backends/SparqlWorkspaceBackend.ts +729 -0
- package/src/queryManagement/backends/WorkspaceBackend.ts +41 -0
- package/src/queryManagement/backends/errors.ts +28 -0
- package/src/queryManagement/backends/getWorkspaceBackend.ts +132 -0
- package/src/queryManagement/backends/gitRemote.ts +54 -0
- package/src/queryManagement/browserFilter.ts +8 -0
- package/src/queryManagement/index.ts +15 -0
- package/src/queryManagement/normalizeQueryFilename.ts +8 -0
- package/src/queryManagement/openManagedQuery.ts +31 -0
- package/src/queryManagement/saveManagedQuery.ts +135 -0
- package/src/queryManagement/textHash.ts +15 -0
- package/src/queryManagement/types.ts +85 -0
- package/src/queryManagement/validateWorkspaceConfig.ts +40 -0
- package/src/tab.scss +4 -14
- package/src/themes.scss +14 -23
- package/src/version.ts +1 -1
|
@@ -0,0 +1,1079 @@
|
|
|
1
|
+
import type Yasgui from "../index";
|
|
2
|
+
import { addClass, removeClass } from "@matdata/yasgui-utils";
|
|
3
|
+
import type { WorkspaceConfig, FolderEntry } from "./types";
|
|
4
|
+
import { filterFolderEntriesByName } from "./browserFilter";
|
|
5
|
+
import { getWorkspaceBackend } from "./backends/getWorkspaceBackend";
|
|
6
|
+
import { asWorkspaceBackendError } from "./backends/errors";
|
|
7
|
+
import { getEndpointToAutoSwitch } from "./openManagedQuery";
|
|
8
|
+
import { hashQueryText } from "./textHash";
|
|
9
|
+
import type { BackendType, VersionRef, ManagedTabMetadata } from "./types";
|
|
10
|
+
import { normalizeQueryFilename } from "./normalizeQueryFilename";
|
|
11
|
+
|
|
12
|
+
import "./QueryBrowser.scss";
|
|
13
|
+
|
|
14
|
+
export default class QueryBrowser {
|
|
15
|
+
private yasgui: Yasgui;
|
|
16
|
+
private rootEl: HTMLDivElement;
|
|
17
|
+
private drawerEl: HTMLDivElement;
|
|
18
|
+
private headerEl: HTMLDivElement;
|
|
19
|
+
private bodyEl: HTMLDivElement;
|
|
20
|
+
private footerEl: HTMLDivElement;
|
|
21
|
+
private tooltipEl: HTMLDivElement;
|
|
22
|
+
|
|
23
|
+
private workspaceSelectEl: HTMLSelectElement;
|
|
24
|
+
private searchEl: HTMLInputElement;
|
|
25
|
+
private backButtonEl: HTMLButtonElement;
|
|
26
|
+
private listEl: HTMLDivElement;
|
|
27
|
+
private statusEl: HTMLDivElement;
|
|
28
|
+
|
|
29
|
+
private isOpen = false;
|
|
30
|
+
private openerEl?: HTMLElement;
|
|
31
|
+
|
|
32
|
+
private selectedWorkspaceId?: string;
|
|
33
|
+
private currentFolderId: string | undefined;
|
|
34
|
+
|
|
35
|
+
private treeWorkspaceId?: string;
|
|
36
|
+
private expandedFolderIds = new Set<string>();
|
|
37
|
+
private folderEntriesById = new Map<string, FolderEntry[]>();
|
|
38
|
+
private folderLoadingById = new Set<string>();
|
|
39
|
+
private folderErrorById = new Map<string, string>();
|
|
40
|
+
|
|
41
|
+
private debouncedSearchHandle?: number;
|
|
42
|
+
|
|
43
|
+
private refreshRunId = 0;
|
|
44
|
+
|
|
45
|
+
private queryPreviewById = new Map<string, string>();
|
|
46
|
+
private queryPreviewLoadingById = new Set<string>();
|
|
47
|
+
private lastRenderedSignature: string | undefined;
|
|
48
|
+
|
|
49
|
+
private lastPointerPos: { x: number; y: number } | undefined;
|
|
50
|
+
|
|
51
|
+
private entrySignature(entry: FolderEntry): string {
|
|
52
|
+
const parent = entry.parentId || "";
|
|
53
|
+
// Include label + parent so renames/moves force a re-render.
|
|
54
|
+
return `${entry.kind}:${entry.id}:${entry.label}:${parent}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private invalidateRenderCache() {
|
|
58
|
+
this.lastRenderedSignature = undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public invalidateAndRefresh(workspaceId?: string) {
|
|
62
|
+
// Clear caches so new/renamed queries show up immediately.
|
|
63
|
+
if (!workspaceId || this.treeWorkspaceId === workspaceId) {
|
|
64
|
+
this.folderEntriesById.clear();
|
|
65
|
+
this.folderLoadingById.clear();
|
|
66
|
+
this.folderErrorById.clear();
|
|
67
|
+
this.queryPreviewById.clear();
|
|
68
|
+
this.queryPreviewLoadingById.clear();
|
|
69
|
+
this.invalidateRenderCache();
|
|
70
|
+
if (this.isOpen) void this.refresh();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
constructor(yasgui: Yasgui) {
|
|
75
|
+
this.yasgui = yasgui;
|
|
76
|
+
|
|
77
|
+
this.rootEl = document.createElement("div");
|
|
78
|
+
addClass(this.rootEl, "yasgui-query-browser");
|
|
79
|
+
|
|
80
|
+
this.tooltipEl = document.createElement("div");
|
|
81
|
+
addClass(this.tooltipEl, "yasgui-query-browser__tooltip");
|
|
82
|
+
this.tooltipEl.setAttribute("role", "tooltip");
|
|
83
|
+
this.tooltipEl.setAttribute("aria-hidden", "true");
|
|
84
|
+
|
|
85
|
+
this.drawerEl = document.createElement("div");
|
|
86
|
+
addClass(this.drawerEl, "yasgui-query-browser__drawer");
|
|
87
|
+
|
|
88
|
+
this.headerEl = document.createElement("div");
|
|
89
|
+
addClass(this.headerEl, "yasgui-query-browser__header");
|
|
90
|
+
|
|
91
|
+
this.bodyEl = document.createElement("div");
|
|
92
|
+
addClass(this.bodyEl, "yasgui-query-browser__body");
|
|
93
|
+
|
|
94
|
+
this.bodyEl.addEventListener("mousemove", (e) => {
|
|
95
|
+
this.lastPointerPos = { x: e.clientX, y: e.clientY };
|
|
96
|
+
if (this.tooltipEl.classList.contains("open")) this.positionTooltip();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
this.footerEl = document.createElement("div");
|
|
100
|
+
addClass(this.footerEl, "yasgui-query-browser__footer");
|
|
101
|
+
|
|
102
|
+
this.statusEl = document.createElement("div");
|
|
103
|
+
addClass(this.statusEl, "yasgui-query-browser__status");
|
|
104
|
+
|
|
105
|
+
this.workspaceSelectEl = document.createElement("select");
|
|
106
|
+
this.workspaceSelectEl.setAttribute("aria-label", "Select workspace");
|
|
107
|
+
this.workspaceSelectEl.addEventListener("change", () => {
|
|
108
|
+
this.selectedWorkspaceId = this.workspaceSelectEl.value || undefined;
|
|
109
|
+
this.currentFolderId = undefined;
|
|
110
|
+
this.resetTreeState();
|
|
111
|
+
this.searchEl.value = "";
|
|
112
|
+
void this.refresh();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
this.searchEl = document.createElement("input");
|
|
116
|
+
this.searchEl.type = "search";
|
|
117
|
+
this.searchEl.placeholder = "Search queries";
|
|
118
|
+
this.searchEl.setAttribute("aria-label", "Search managed queries by name");
|
|
119
|
+
this.searchEl.addEventListener("input", () => {
|
|
120
|
+
if (this.debouncedSearchHandle) window.clearTimeout(this.debouncedSearchHandle);
|
|
121
|
+
this.debouncedSearchHandle = window.setTimeout(() => {
|
|
122
|
+
void this.refresh();
|
|
123
|
+
}, 250);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
this.backButtonEl = document.createElement("button");
|
|
127
|
+
this.backButtonEl.type = "button";
|
|
128
|
+
this.backButtonEl.textContent = "Back";
|
|
129
|
+
addClass(this.backButtonEl, "yasgui-query-browser__back");
|
|
130
|
+
this.backButtonEl.setAttribute("aria-label", "Go to parent folder");
|
|
131
|
+
this.backButtonEl.addEventListener("click", () => {
|
|
132
|
+
if (!this.currentFolderId) return;
|
|
133
|
+
const parts = this.currentFolderId.split("/").filter(Boolean);
|
|
134
|
+
parts.pop();
|
|
135
|
+
this.currentFolderId = parts.join("/") || undefined;
|
|
136
|
+
void this.refresh();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const titleEl = document.createElement("div");
|
|
140
|
+
addClass(titleEl, "yasgui-query-browser__title");
|
|
141
|
+
titleEl.textContent = "Query Browser";
|
|
142
|
+
|
|
143
|
+
const headerButtons = document.createElement("div");
|
|
144
|
+
addClass(headerButtons, "yasgui-query-browser__header-buttons");
|
|
145
|
+
|
|
146
|
+
const helpButton = document.createElement("button");
|
|
147
|
+
helpButton.type = "button";
|
|
148
|
+
addClass(helpButton, "yasgui-query-browser__help");
|
|
149
|
+
helpButton.setAttribute("aria-label", "Open documentation");
|
|
150
|
+
helpButton.innerHTML = '<i class="fas fa-circle-question"></i>';
|
|
151
|
+
helpButton.addEventListener("click", () => {
|
|
152
|
+
window.open(
|
|
153
|
+
"https://yasgui-doc.matdata.eu/docs/user-guide#managed-queries-and-workspaces",
|
|
154
|
+
"_blank",
|
|
155
|
+
"noopener,noreferrer",
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const closeButton = document.createElement("button");
|
|
160
|
+
closeButton.type = "button";
|
|
161
|
+
addClass(closeButton, "yasgui-query-browser__close");
|
|
162
|
+
closeButton.setAttribute("aria-label", "Close query browser");
|
|
163
|
+
closeButton.innerHTML = '<i class="fas fa-xmark"></i>';
|
|
164
|
+
closeButton.addEventListener("click", () => this.close());
|
|
165
|
+
|
|
166
|
+
headerButtons.appendChild(helpButton);
|
|
167
|
+
headerButtons.appendChild(closeButton);
|
|
168
|
+
|
|
169
|
+
const headerControls = document.createElement("div");
|
|
170
|
+
addClass(headerControls, "yasgui-query-browser__header-controls");
|
|
171
|
+
headerControls.appendChild(this.workspaceSelectEl);
|
|
172
|
+
headerControls.appendChild(this.searchEl);
|
|
173
|
+
|
|
174
|
+
const headerTop = document.createElement("div");
|
|
175
|
+
addClass(headerTop, "yasgui-query-browser__header-top");
|
|
176
|
+
headerTop.appendChild(titleEl);
|
|
177
|
+
headerTop.appendChild(headerButtons);
|
|
178
|
+
|
|
179
|
+
this.headerEl.appendChild(headerTop);
|
|
180
|
+
this.headerEl.appendChild(headerControls);
|
|
181
|
+
|
|
182
|
+
this.listEl = document.createElement("div");
|
|
183
|
+
addClass(this.listEl, "yasgui-query-browser__list");
|
|
184
|
+
|
|
185
|
+
this.bodyEl.appendChild(this.backButtonEl);
|
|
186
|
+
this.bodyEl.appendChild(this.statusEl);
|
|
187
|
+
this.bodyEl.appendChild(this.listEl);
|
|
188
|
+
|
|
189
|
+
this.footerEl.textContent = "";
|
|
190
|
+
|
|
191
|
+
this.drawerEl.appendChild(this.headerEl);
|
|
192
|
+
this.drawerEl.appendChild(this.bodyEl);
|
|
193
|
+
this.drawerEl.appendChild(this.footerEl);
|
|
194
|
+
|
|
195
|
+
this.rootEl.appendChild(this.drawerEl);
|
|
196
|
+
this.rootEl.appendChild(this.tooltipEl);
|
|
197
|
+
|
|
198
|
+
this.rootEl.addEventListener("click", (e) => {
|
|
199
|
+
if (e.target === this.rootEl) this.close();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
document.addEventListener("keydown", (e) => {
|
|
203
|
+
if (!this.isOpen) return;
|
|
204
|
+
if (e.key === "Escape") {
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
this.close();
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
this.close();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
public getElement(): HTMLDivElement {
|
|
214
|
+
return this.rootEl;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public open(openerEl?: HTMLElement) {
|
|
218
|
+
this.openerEl = openerEl;
|
|
219
|
+
this.isOpen = true;
|
|
220
|
+
this.rootEl.style.display = "flex";
|
|
221
|
+
addClass(this.rootEl, "open");
|
|
222
|
+
this.rootEl.setAttribute("aria-hidden", "false");
|
|
223
|
+
this.drawerEl.setAttribute("role", "dialog");
|
|
224
|
+
this.drawerEl.setAttribute("aria-modal", "true");
|
|
225
|
+
|
|
226
|
+
void this.refresh().then(() => {
|
|
227
|
+
this.workspaceSelectEl.focus();
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public close() {
|
|
232
|
+
this.isOpen = false;
|
|
233
|
+
removeClass(this.rootEl, "open");
|
|
234
|
+
this.rootEl.setAttribute("aria-hidden", "true");
|
|
235
|
+
this.rootEl.style.display = "none";
|
|
236
|
+
|
|
237
|
+
if (this.openerEl) {
|
|
238
|
+
this.openerEl.focus();
|
|
239
|
+
this.openerEl = undefined;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
public toggle(openerEl?: HTMLElement) {
|
|
244
|
+
if (this.isOpen) this.close();
|
|
245
|
+
else this.open(openerEl);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private getWorkspaces(): WorkspaceConfig[] {
|
|
249
|
+
return this.yasgui.persistentConfig.getWorkspaces();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private ensureWorkspaceSelection() {
|
|
253
|
+
const workspaces = this.getWorkspaces();
|
|
254
|
+
if (workspaces.length === 0) {
|
|
255
|
+
this.selectedWorkspaceId = undefined;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!this.selectedWorkspaceId) {
|
|
260
|
+
const persistedActive = this.yasgui.persistentConfig.getActiveWorkspaceId();
|
|
261
|
+
this.selectedWorkspaceId = persistedActive || workspaces[0].id;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const exists = workspaces.some((w) => w.id === this.selectedWorkspaceId);
|
|
265
|
+
if (!exists) this.selectedWorkspaceId = workspaces[0].id;
|
|
266
|
+
|
|
267
|
+
this.yasgui.persistentConfig.setActiveWorkspaceId(this.selectedWorkspaceId);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private renderWorkspaceSelect() {
|
|
271
|
+
const workspaces = this.getWorkspaces();
|
|
272
|
+
this.workspaceSelectEl.innerHTML = "";
|
|
273
|
+
|
|
274
|
+
const placeholder = document.createElement("option");
|
|
275
|
+
placeholder.value = "";
|
|
276
|
+
placeholder.textContent = workspaces.length ? "Select workspace" : "No workspaces";
|
|
277
|
+
this.workspaceSelectEl.appendChild(placeholder);
|
|
278
|
+
|
|
279
|
+
for (const w of workspaces) {
|
|
280
|
+
const opt = document.createElement("option");
|
|
281
|
+
opt.value = w.id;
|
|
282
|
+
opt.textContent = w.label;
|
|
283
|
+
this.workspaceSelectEl.appendChild(opt);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
this.workspaceSelectEl.value = this.selectedWorkspaceId || "";
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private setStatus(text: string) {
|
|
290
|
+
this.statusEl.textContent = text;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private resetTreeState() {
|
|
294
|
+
this.treeWorkspaceId = undefined;
|
|
295
|
+
this.expandedFolderIds.clear();
|
|
296
|
+
this.folderEntriesById.clear();
|
|
297
|
+
this.folderLoadingById.clear();
|
|
298
|
+
this.folderErrorById.clear();
|
|
299
|
+
this.invalidateRenderCache();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private folderKey(folderId: string | undefined): string {
|
|
303
|
+
return folderId || "";
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private async ensureFolderLoaded(backend: ReturnType<typeof getWorkspaceBackend>, folderId: string | undefined) {
|
|
307
|
+
const key = this.folderKey(folderId);
|
|
308
|
+
if (this.folderEntriesById.has(key) || this.folderLoadingById.has(key)) return;
|
|
309
|
+
|
|
310
|
+
this.folderLoadingById.add(key);
|
|
311
|
+
this.folderErrorById.delete(key);
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const entries = await backend.listFolder(folderId);
|
|
315
|
+
this.folderEntriesById.set(key, entries);
|
|
316
|
+
} catch (e) {
|
|
317
|
+
const err = asWorkspaceBackendError(e);
|
|
318
|
+
this.folderErrorById.set(key, err.message || "Failed to load folder");
|
|
319
|
+
this.folderEntriesById.set(key, []);
|
|
320
|
+
} finally {
|
|
321
|
+
this.folderLoadingById.delete(key);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private formatStatusError(err: Error, workspaceType: WorkspaceConfig["type"]) {
|
|
326
|
+
const message = err.message || "Unknown error";
|
|
327
|
+
|
|
328
|
+
if (message.includes("No GitProviderClient configured")) {
|
|
329
|
+
return "Git workspaces require a supported provider. Supported: GitHub, GitLab, Bitbucket Cloud (bitbucket.org), and Gitea. For self-hosted/enterprise instances, configure a git workspace 'provider' and/or 'apiBaseUrl'.";
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (message.toLowerCase().includes("not implemented")) {
|
|
333
|
+
return "This workspace backend is not supported yet.";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return message;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private clearList() {
|
|
340
|
+
this.listEl.innerHTML = "";
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private renderEmptyWorkspaceState() {
|
|
344
|
+
const emptyState = document.createElement("div");
|
|
345
|
+
addClass(emptyState, "yasgui-query-browser__empty-state");
|
|
346
|
+
|
|
347
|
+
const icon = document.createElement("div");
|
|
348
|
+
addClass(icon, "yasgui-query-browser__empty-icon");
|
|
349
|
+
icon.innerHTML = '<i class="fas fa-file-lines"></i>';
|
|
350
|
+
|
|
351
|
+
const title = document.createElement("h3");
|
|
352
|
+
addClass(title, "yasgui-query-browser__empty-title");
|
|
353
|
+
title.textContent = "No Workspaces Configured";
|
|
354
|
+
|
|
355
|
+
const description = document.createElement("p");
|
|
356
|
+
addClass(description, "yasgui-query-browser__empty-description");
|
|
357
|
+
description.innerHTML = `Workspaces allow you to save and manage SPARQL queries in a shared, versioned store.
|
|
358
|
+
You can use SPARQL endpoints (recommended) or Git repositories (GitHub, GitLab, Bitbucket, Gitea) to store your queries.`;
|
|
359
|
+
|
|
360
|
+
const actionsContainer = document.createElement("div");
|
|
361
|
+
addClass(actionsContainer, "yasgui-query-browser__empty-actions");
|
|
362
|
+
|
|
363
|
+
const addWorkspaceBtn = document.createElement("button");
|
|
364
|
+
addClass(addWorkspaceBtn, "yasgui-query-browser__empty-button");
|
|
365
|
+
addClass(addWorkspaceBtn, "yasgui-query-browser__empty-button--primary");
|
|
366
|
+
addWorkspaceBtn.textContent = "Add Workspace";
|
|
367
|
+
addWorkspaceBtn.addEventListener("click", () => {
|
|
368
|
+
this.close();
|
|
369
|
+
const tab = this.yasgui.getTab();
|
|
370
|
+
if (tab && (tab as any).settingsModal) {
|
|
371
|
+
const modal = (tab as any).settingsModal;
|
|
372
|
+
modal.open();
|
|
373
|
+
// Switch to workspaces tab after a short delay to ensure modal is rendered
|
|
374
|
+
setTimeout(() => {
|
|
375
|
+
const workspacesButton = modal.modalContent?.querySelector(".modalNavButton:nth-child(3)") as HTMLElement;
|
|
376
|
+
if (workspacesButton) workspacesButton.click();
|
|
377
|
+
}, 50);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const learnMoreLink = document.createElement("a");
|
|
382
|
+
addClass(learnMoreLink, "yasgui-query-browser__empty-link");
|
|
383
|
+
learnMoreLink.href = "https://yasgui-doc.matdata.eu/docs/user-guide#managed-queries-and-workspaces";
|
|
384
|
+
learnMoreLink.target = "_blank";
|
|
385
|
+
learnMoreLink.rel = "noopener noreferrer";
|
|
386
|
+
learnMoreLink.textContent = "Learn more about workspaces";
|
|
387
|
+
|
|
388
|
+
actionsContainer.appendChild(addWorkspaceBtn);
|
|
389
|
+
actionsContainer.appendChild(learnMoreLink);
|
|
390
|
+
|
|
391
|
+
emptyState.appendChild(icon);
|
|
392
|
+
emptyState.appendChild(title);
|
|
393
|
+
emptyState.appendChild(description);
|
|
394
|
+
emptyState.appendChild(actionsContainer);
|
|
395
|
+
|
|
396
|
+
this.listEl.appendChild(emptyState);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private formatQueryPreview(queryText: string, description?: string): string {
|
|
400
|
+
const normalized = queryText.replace(/\r\n?/g, "\n").trim();
|
|
401
|
+
const lines = normalized.split("\n").map((l) => l.trimEnd());
|
|
402
|
+
const maxLines = 12;
|
|
403
|
+
const selected = lines.slice(0, maxLines);
|
|
404
|
+
let out = selected.join("\n");
|
|
405
|
+
|
|
406
|
+
const maxChars = 800;
|
|
407
|
+
if (out.length > maxChars) out = out.slice(0, maxChars - 1).trimEnd() + "…";
|
|
408
|
+
if (lines.length > maxLines && !out.endsWith("…")) out = out + "\n…";
|
|
409
|
+
|
|
410
|
+
const trimmedDesc = description?.replace(/\r\n?/g, "\n").trim();
|
|
411
|
+
if (!trimmedDesc) return out;
|
|
412
|
+
return `${trimmedDesc}\n\n${out}`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private async ensureQueryPreview(
|
|
416
|
+
backend: ReturnType<typeof getWorkspaceBackend>,
|
|
417
|
+
queryId: string,
|
|
418
|
+
): Promise<string | undefined> {
|
|
419
|
+
const cached = this.queryPreviewById.get(queryId);
|
|
420
|
+
if (cached) return cached;
|
|
421
|
+
if (this.queryPreviewLoadingById.has(queryId)) return undefined;
|
|
422
|
+
|
|
423
|
+
this.queryPreviewLoadingById.add(queryId);
|
|
424
|
+
try {
|
|
425
|
+
const res = await backend.readQuery(queryId);
|
|
426
|
+
const preview = this.formatQueryPreview(res.queryText, res.description);
|
|
427
|
+
this.queryPreviewById.set(queryId, preview);
|
|
428
|
+
return preview;
|
|
429
|
+
} catch {
|
|
430
|
+
return undefined;
|
|
431
|
+
} finally {
|
|
432
|
+
this.queryPreviewLoadingById.delete(queryId);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private getManagedQueryIdFromMetadata(meta: ManagedTabMetadata): string | undefined {
|
|
437
|
+
if (meta.backendType === "git") return (meta.queryRef as any)?.path as string | undefined;
|
|
438
|
+
return (meta.queryRef as any)?.managedQueryIri as string | undefined;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private findOpenManagedTabId(workspaceId: string, backendType: BackendType, queryId: string): string | undefined {
|
|
442
|
+
for (const tab of Object.values(this.yasgui._tabs)) {
|
|
443
|
+
const meta = tab.getManagedQueryMetadata();
|
|
444
|
+
if (!meta) continue;
|
|
445
|
+
if (meta.workspaceId !== workspaceId) continue;
|
|
446
|
+
if (meta.backendType !== backendType) continue;
|
|
447
|
+
const openQueryId = this.getManagedQueryIdFromMetadata(meta);
|
|
448
|
+
if (openQueryId === queryId) return tab.getId();
|
|
449
|
+
}
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private showTooltip(text: string) {
|
|
454
|
+
this.tooltipEl.textContent = text;
|
|
455
|
+
this.tooltipEl.setAttribute("aria-hidden", "false");
|
|
456
|
+
addClass(this.tooltipEl, "open");
|
|
457
|
+
this.positionTooltip();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private hideTooltip() {
|
|
461
|
+
this.tooltipEl.textContent = "";
|
|
462
|
+
this.tooltipEl.setAttribute("aria-hidden", "true");
|
|
463
|
+
removeClass(this.tooltipEl, "open");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private positionTooltip() {
|
|
467
|
+
const pos = this.lastPointerPos;
|
|
468
|
+
if (!pos) return;
|
|
469
|
+
|
|
470
|
+
// Start with a simple cursor offset.
|
|
471
|
+
let left = pos.x + 12;
|
|
472
|
+
let top = pos.y + 12;
|
|
473
|
+
|
|
474
|
+
// Clamp to viewport.
|
|
475
|
+
const rect = this.tooltipEl.getBoundingClientRect();
|
|
476
|
+
const vw = window.innerWidth;
|
|
477
|
+
const vh = window.innerHeight;
|
|
478
|
+
if (left + rect.width > vw - 8) left = Math.max(8, vw - rect.width - 8);
|
|
479
|
+
if (top + rect.height > vh - 8) top = Math.max(8, vh - rect.height - 8);
|
|
480
|
+
|
|
481
|
+
this.tooltipEl.style.left = `${left}px`;
|
|
482
|
+
this.tooltipEl.style.top = `${top}px`;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
private attachQueryHoverPreview(row: HTMLElement, backend: ReturnType<typeof getWorkspaceBackend>, queryId: string) {
|
|
486
|
+
const onEnter = () => {
|
|
487
|
+
const cached = this.queryPreviewById.get(queryId);
|
|
488
|
+
if (cached) {
|
|
489
|
+
this.showTooltip(cached);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
this.showTooltip("Loading preview…");
|
|
494
|
+
void this.ensureQueryPreview(backend, queryId).then((preview) => {
|
|
495
|
+
if (!this.tooltipEl.classList.contains("open")) return;
|
|
496
|
+
if (preview) this.showTooltip(preview);
|
|
497
|
+
});
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const onLeave = () => {
|
|
501
|
+
this.hideTooltip();
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
row.addEventListener("mouseenter", onEnter);
|
|
505
|
+
row.addEventListener("mouseleave", onLeave);
|
|
506
|
+
row.addEventListener("focusin", onEnter);
|
|
507
|
+
row.addEventListener("focusout", onLeave);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private getBackendForSelectedWorkspace(): ReturnType<typeof getWorkspaceBackend> | undefined {
|
|
511
|
+
const workspace = this.getWorkspaces().find((w) => w.id === this.selectedWorkspaceId);
|
|
512
|
+
if (!workspace) return undefined;
|
|
513
|
+
return getWorkspaceBackend(workspace, { persistentConfig: this.yasgui.persistentConfig });
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private versionRefFromVersionTag(backendType: BackendType, versionTag: string | undefined): VersionRef | undefined {
|
|
517
|
+
if (!versionTag) return undefined;
|
|
518
|
+
if (backendType === "git") return { commitSha: versionTag };
|
|
519
|
+
return { managedQueryVersionIri: versionTag };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private async openManagedQueryInNewTab(
|
|
523
|
+
backend: ReturnType<typeof getWorkspaceBackend>,
|
|
524
|
+
backendType: BackendType,
|
|
525
|
+
queryId: string,
|
|
526
|
+
queryLabel?: string,
|
|
527
|
+
) {
|
|
528
|
+
const workspaceId = this.selectedWorkspaceId!;
|
|
529
|
+
const alreadyOpenTabId = this.findOpenManagedTabId(workspaceId, backendType, queryId);
|
|
530
|
+
if (alreadyOpenTabId) {
|
|
531
|
+
this.yasgui.selectTabId(alreadyOpenTabId);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const read = await backend.readQuery(queryId);
|
|
536
|
+
|
|
537
|
+
const tab = this.yasgui.addTab(true);
|
|
538
|
+
if (queryLabel) tab.setName(queryLabel);
|
|
539
|
+
|
|
540
|
+
const endpoint = getEndpointToAutoSwitch(backendType, read);
|
|
541
|
+
if (endpoint) tab.setEndpoint(endpoint);
|
|
542
|
+
|
|
543
|
+
tab.setQuery(read.queryText);
|
|
544
|
+
|
|
545
|
+
const managedMetadata: ManagedTabMetadata = {
|
|
546
|
+
workspaceId,
|
|
547
|
+
backendType,
|
|
548
|
+
queryRef: backendType === "git" ? { path: queryId } : { managedQueryIri: queryId },
|
|
549
|
+
lastSavedVersionRef: this.versionRefFromVersionTag(backendType, read.versionTag),
|
|
550
|
+
lastSavedTextHash: hashQueryText(read.queryText),
|
|
551
|
+
};
|
|
552
|
+
tab.setManagedQueryMetadata(managedMetadata);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private addQueryRowActions(row: HTMLElement, backend: ReturnType<typeof getWorkspaceBackend>, entry: FolderEntry) {
|
|
556
|
+
const actions = document.createElement("span");
|
|
557
|
+
addClass(actions, "yasgui-query-browser__actions");
|
|
558
|
+
|
|
559
|
+
if (entry.kind === "folder") {
|
|
560
|
+
if (backend.renameFolder) {
|
|
561
|
+
const renameBtn = document.createElement("button");
|
|
562
|
+
renameBtn.type = "button";
|
|
563
|
+
addClass(renameBtn, "yasgui-query-browser__action");
|
|
564
|
+
renameBtn.textContent = "Rename";
|
|
565
|
+
renameBtn.setAttribute("aria-label", `Rename folder ${entry.label}`);
|
|
566
|
+
renameBtn.addEventListener("click", async (e) => {
|
|
567
|
+
e.preventDefault();
|
|
568
|
+
e.stopPropagation();
|
|
569
|
+
|
|
570
|
+
const next = window.prompt("Rename folder", entry.label);
|
|
571
|
+
if (!next) return;
|
|
572
|
+
const trimmed = next.trim();
|
|
573
|
+
if (!trimmed || trimmed === entry.label) return;
|
|
574
|
+
|
|
575
|
+
try {
|
|
576
|
+
await backend.renameFolder!(entry.id, trimmed);
|
|
577
|
+
this.folderEntriesById.clear();
|
|
578
|
+
this.invalidateRenderCache();
|
|
579
|
+
await this.refresh();
|
|
580
|
+
} catch (err) {
|
|
581
|
+
window.alert(asWorkspaceBackendError(err).message);
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
actions.appendChild(renameBtn);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (backend.deleteFolder) {
|
|
588
|
+
const deleteBtn = document.createElement("button");
|
|
589
|
+
deleteBtn.type = "button";
|
|
590
|
+
addClass(deleteBtn, "yasgui-query-browser__action");
|
|
591
|
+
addClass(deleteBtn, "yasgui-query-browser__action--danger");
|
|
592
|
+
deleteBtn.textContent = "Delete";
|
|
593
|
+
deleteBtn.setAttribute("aria-label", `Delete folder ${entry.label}`);
|
|
594
|
+
deleteBtn.addEventListener("click", async (e) => {
|
|
595
|
+
e.preventDefault();
|
|
596
|
+
e.stopPropagation();
|
|
597
|
+
|
|
598
|
+
const ok = window.confirm(`Delete folder '${entry.label}' and everything inside it? This cannot be undone.`);
|
|
599
|
+
if (!ok) return;
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
await backend.deleteFolder!(entry.id);
|
|
603
|
+
this.folderEntriesById.clear();
|
|
604
|
+
this.invalidateRenderCache();
|
|
605
|
+
await this.refresh();
|
|
606
|
+
} catch (err) {
|
|
607
|
+
window.alert(asWorkspaceBackendError(err).message);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
actions.appendChild(deleteBtn);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (actions.childElementCount > 0) row.appendChild(actions);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (entry.kind !== "query") return;
|
|
618
|
+
|
|
619
|
+
if (backend.renameQuery) {
|
|
620
|
+
const renameBtn = document.createElement("button");
|
|
621
|
+
renameBtn.type = "button";
|
|
622
|
+
addClass(renameBtn, "yasgui-query-browser__action");
|
|
623
|
+
renameBtn.textContent = "Rename";
|
|
624
|
+
renameBtn.setAttribute("aria-label", `Rename ${entry.label}`);
|
|
625
|
+
renameBtn.addEventListener("click", async (e) => {
|
|
626
|
+
e.preventDefault();
|
|
627
|
+
e.stopPropagation();
|
|
628
|
+
|
|
629
|
+
const next = window.prompt("Rename query", entry.label);
|
|
630
|
+
if (!next) return;
|
|
631
|
+
const trimmed = next.trim();
|
|
632
|
+
if (!trimmed || trimmed === entry.label) return;
|
|
633
|
+
|
|
634
|
+
// For git workspaces we can deterministically compute the new path, so we can
|
|
635
|
+
// also update any already-open managed tabs that reference this query.
|
|
636
|
+
const gitRenameInfo = (() => {
|
|
637
|
+
if (backend.type !== "git") return undefined;
|
|
638
|
+
const parts = entry.id.split("/").filter(Boolean);
|
|
639
|
+
parts.pop();
|
|
640
|
+
const folderPrefix = parts.join("/");
|
|
641
|
+
const safe = trimmed.replace(/[\\/]/g, "-");
|
|
642
|
+
const newFilename = normalizeQueryFilename(safe);
|
|
643
|
+
const newPath = folderPrefix ? `${folderPrefix}/${newFilename}` : newFilename;
|
|
644
|
+
return { oldPath: entry.id, newPath };
|
|
645
|
+
})();
|
|
646
|
+
|
|
647
|
+
// Show loading state
|
|
648
|
+
const originalText = renameBtn.textContent;
|
|
649
|
+
renameBtn.disabled = true;
|
|
650
|
+
renameBtn.textContent = "Renaming…";
|
|
651
|
+
addClass(renameBtn, "loading");
|
|
652
|
+
|
|
653
|
+
try {
|
|
654
|
+
await backend.renameQuery!(entry.id, trimmed);
|
|
655
|
+
|
|
656
|
+
if (gitRenameInfo && gitRenameInfo.newPath && gitRenameInfo.oldPath) {
|
|
657
|
+
for (const tab of Object.values(this.yasgui._tabs)) {
|
|
658
|
+
const meta = (tab as any).getManagedQueryMetadata?.() as ManagedTabMetadata | undefined;
|
|
659
|
+
if (!meta) continue;
|
|
660
|
+
if (meta.backendType !== "git") continue;
|
|
661
|
+
if (meta.workspaceId !== this.selectedWorkspaceId) continue;
|
|
662
|
+
const currentPath = (meta.queryRef as any)?.path as string | undefined;
|
|
663
|
+
if (currentPath !== gitRenameInfo.oldPath) continue;
|
|
664
|
+
|
|
665
|
+
try {
|
|
666
|
+
const read = await backend.readQuery(gitRenameInfo.newPath);
|
|
667
|
+
const lastSavedTextHash = hashQueryText(read.queryText);
|
|
668
|
+
const lastSavedVersionRef = this.versionRefFromVersionTag("git", read.versionTag);
|
|
669
|
+
|
|
670
|
+
(tab as any).setManagedQueryMetadata?.({
|
|
671
|
+
...meta,
|
|
672
|
+
queryRef: { ...(meta.queryRef as any), path: gitRenameInfo.newPath },
|
|
673
|
+
lastSavedTextHash,
|
|
674
|
+
lastSavedVersionRef,
|
|
675
|
+
});
|
|
676
|
+
(tab as any).setName?.(trimmed);
|
|
677
|
+
} catch {
|
|
678
|
+
// Best-effort: if refreshing metadata fails, the Query Browser still reflects the rename.
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
this.queryPreviewById.delete(entry.id);
|
|
684
|
+
this.folderEntriesById.clear();
|
|
685
|
+
this.invalidateRenderCache();
|
|
686
|
+
await this.refresh();
|
|
687
|
+
} catch (err) {
|
|
688
|
+
// Restore button state on error
|
|
689
|
+
renameBtn.disabled = false;
|
|
690
|
+
renameBtn.textContent = originalText || "Rename";
|
|
691
|
+
removeClass(renameBtn, "loading");
|
|
692
|
+
window.alert(asWorkspaceBackendError(err).message);
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
actions.appendChild(renameBtn);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (backend.deleteQuery) {
|
|
699
|
+
const deleteBtn = document.createElement("button");
|
|
700
|
+
deleteBtn.type = "button";
|
|
701
|
+
addClass(deleteBtn, "yasgui-query-browser__action");
|
|
702
|
+
addClass(deleteBtn, "yasgui-query-browser__action--danger");
|
|
703
|
+
deleteBtn.textContent = "Delete";
|
|
704
|
+
deleteBtn.setAttribute("aria-label", `Delete ${entry.label}`);
|
|
705
|
+
deleteBtn.addEventListener("click", async (e) => {
|
|
706
|
+
e.preventDefault();
|
|
707
|
+
e.stopPropagation();
|
|
708
|
+
const ok = window.confirm(`Delete '${entry.label}'? This cannot be undone.`);
|
|
709
|
+
if (!ok) return;
|
|
710
|
+
|
|
711
|
+
// Show loading state
|
|
712
|
+
const originalText = deleteBtn.textContent;
|
|
713
|
+
deleteBtn.disabled = true;
|
|
714
|
+
deleteBtn.textContent = "Deleting…";
|
|
715
|
+
addClass(deleteBtn, "loading");
|
|
716
|
+
|
|
717
|
+
try {
|
|
718
|
+
await backend.deleteQuery!(entry.id);
|
|
719
|
+
this.queryPreviewById.delete(entry.id);
|
|
720
|
+
this.folderEntriesById.clear();
|
|
721
|
+
this.invalidateRenderCache();
|
|
722
|
+
await this.refresh();
|
|
723
|
+
} catch (err) {
|
|
724
|
+
// Restore button state on error
|
|
725
|
+
deleteBtn.disabled = false;
|
|
726
|
+
deleteBtn.textContent = originalText || "Delete";
|
|
727
|
+
removeClass(deleteBtn, "loading");
|
|
728
|
+
window.alert(asWorkspaceBackendError(err).message);
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
actions.appendChild(deleteBtn);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (actions.childElementCount > 0) {
|
|
735
|
+
row.appendChild(actions);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
private renderFlatEntries(backend: ReturnType<typeof getWorkspaceBackend>, entries: FolderEntry[]) {
|
|
740
|
+
this.clearList();
|
|
741
|
+
|
|
742
|
+
const makeIndentStyle = (depth: number) => `padding-left: ${depth * 16}px;`;
|
|
743
|
+
|
|
744
|
+
for (const entry of entries) {
|
|
745
|
+
const row = document.createElement("div");
|
|
746
|
+
addClass(row, "yasgui-query-browser__tree-row");
|
|
747
|
+
addClass(
|
|
748
|
+
row,
|
|
749
|
+
entry.kind === "folder" ? "yasgui-query-browser__tree-row--folder" : "yasgui-query-browser__tree-row--query",
|
|
750
|
+
);
|
|
751
|
+
row.setAttribute("role", "button");
|
|
752
|
+
row.setAttribute("tabindex", "0");
|
|
753
|
+
row.setAttribute(
|
|
754
|
+
"aria-label",
|
|
755
|
+
entry.kind === "folder" ? `Open folder ${entry.label}` : `Open query ${entry.label}`,
|
|
756
|
+
);
|
|
757
|
+
row.setAttribute("style", makeIndentStyle(0));
|
|
758
|
+
|
|
759
|
+
if (entry.kind === "folder") {
|
|
760
|
+
const isExpanded = this.expandedFolderIds.has(entry.id);
|
|
761
|
+
|
|
762
|
+
const caret = document.createElement("span");
|
|
763
|
+
addClass(caret, "yasgui-query-browser__tree-caret");
|
|
764
|
+
caret.textContent = isExpanded ? "▾" : "▸";
|
|
765
|
+
caret.setAttribute("aria-hidden", "true");
|
|
766
|
+
|
|
767
|
+
const folderIcon = document.createElement("span");
|
|
768
|
+
addClass(folderIcon, "yasgui-query-browser__tree-icon");
|
|
769
|
+
folderIcon.textContent = "📁";
|
|
770
|
+
|
|
771
|
+
const label = document.createElement("span");
|
|
772
|
+
addClass(label, "yasgui-query-browser__tree-label");
|
|
773
|
+
label.textContent = entry.label;
|
|
774
|
+
|
|
775
|
+
row.appendChild(caret);
|
|
776
|
+
row.appendChild(folderIcon);
|
|
777
|
+
row.appendChild(label);
|
|
778
|
+
|
|
779
|
+
this.addQueryRowActions(row, backend, entry);
|
|
780
|
+
} else {
|
|
781
|
+
const caretPlaceholder = document.createElement("span");
|
|
782
|
+
addClass(caretPlaceholder, "yasgui-query-browser__tree-caret");
|
|
783
|
+
caretPlaceholder.textContent = "▸";
|
|
784
|
+
caretPlaceholder.setAttribute("aria-hidden", "true");
|
|
785
|
+
addClass(caretPlaceholder, "yasgui-query-browser__tree-caret--placeholder");
|
|
786
|
+
|
|
787
|
+
const icon = document.createElement("span");
|
|
788
|
+
addClass(icon, "yasgui-query-browser__tree-icon");
|
|
789
|
+
icon.textContent = "🕸️";
|
|
790
|
+
|
|
791
|
+
const label = document.createElement("span");
|
|
792
|
+
addClass(label, "yasgui-query-browser__tree-label");
|
|
793
|
+
label.textContent = entry.label;
|
|
794
|
+
|
|
795
|
+
row.appendChild(caretPlaceholder);
|
|
796
|
+
row.appendChild(icon);
|
|
797
|
+
row.appendChild(label);
|
|
798
|
+
|
|
799
|
+
this.attachQueryHoverPreview(row, backend, entry.id);
|
|
800
|
+
this.addQueryRowActions(row, backend, entry);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const activate = async () => {
|
|
804
|
+
if (entry.kind === "folder") {
|
|
805
|
+
// Switch from search results back to the normal tree view.
|
|
806
|
+
this.searchEl.value = "";
|
|
807
|
+
|
|
808
|
+
if (this.expandedFolderIds.has(entry.id)) {
|
|
809
|
+
this.expandedFolderIds.delete(entry.id);
|
|
810
|
+
} else {
|
|
811
|
+
this.expandedFolderIds.add(entry.id);
|
|
812
|
+
await this.ensureFolderLoaded(backend, entry.id);
|
|
813
|
+
}
|
|
814
|
+
await this.refresh();
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
await this.openManagedQueryInNewTab(backend, backend.type, entry.id, entry.label);
|
|
819
|
+
|
|
820
|
+
this.close();
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
row.addEventListener("keydown", (e) => {
|
|
824
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
825
|
+
e.preventDefault();
|
|
826
|
+
void activate();
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
row.addEventListener("click", async () => {
|
|
831
|
+
await activate();
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
this.listEl.appendChild(row);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
private renderTree(backend: ReturnType<typeof getWorkspaceBackend>) {
|
|
839
|
+
this.clearList();
|
|
840
|
+
|
|
841
|
+
const makeIndentStyle = (depth: number) => `padding-left: ${depth * 16}px;`;
|
|
842
|
+
|
|
843
|
+
const renderFolderChildren = (folderId: string | undefined, depth: number) => {
|
|
844
|
+
const key = this.folderKey(folderId);
|
|
845
|
+
const entries = this.folderEntriesById.get(key) || [];
|
|
846
|
+
|
|
847
|
+
const folders = entries
|
|
848
|
+
.filter((e) => e.kind === "folder")
|
|
849
|
+
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
|
|
850
|
+
const queries = entries
|
|
851
|
+
.filter((e) => e.kind === "query")
|
|
852
|
+
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
|
|
853
|
+
|
|
854
|
+
for (const entry of [...folders, ...queries]) {
|
|
855
|
+
if (entry.kind === "folder") {
|
|
856
|
+
const isExpanded = this.expandedFolderIds.has(entry.id);
|
|
857
|
+
|
|
858
|
+
const row = document.createElement("div");
|
|
859
|
+
addClass(row, "yasgui-query-browser__tree-row");
|
|
860
|
+
addClass(row, "yasgui-query-browser__tree-row--folder");
|
|
861
|
+
row.setAttribute("aria-label", `${isExpanded ? "Collapse" : "Expand"} folder ${entry.label}`);
|
|
862
|
+
row.setAttribute("style", makeIndentStyle(depth));
|
|
863
|
+
row.setAttribute("role", "button");
|
|
864
|
+
row.setAttribute("tabindex", "0");
|
|
865
|
+
|
|
866
|
+
const caret = document.createElement("span");
|
|
867
|
+
addClass(caret, "yasgui-query-browser__tree-caret");
|
|
868
|
+
caret.textContent = isExpanded ? "▾" : "▸";
|
|
869
|
+
caret.setAttribute("aria-hidden", "true");
|
|
870
|
+
|
|
871
|
+
const folderIcon = document.createElement("span");
|
|
872
|
+
addClass(folderIcon, "yasgui-query-browser__tree-icon");
|
|
873
|
+
folderIcon.textContent = "📁";
|
|
874
|
+
|
|
875
|
+
const label = document.createElement("span");
|
|
876
|
+
addClass(label, "yasgui-query-browser__tree-label");
|
|
877
|
+
label.textContent = entry.label;
|
|
878
|
+
|
|
879
|
+
row.appendChild(caret);
|
|
880
|
+
row.appendChild(folderIcon);
|
|
881
|
+
row.appendChild(label);
|
|
882
|
+
|
|
883
|
+
this.addQueryRowActions(row, backend, entry);
|
|
884
|
+
|
|
885
|
+
const activate = async () => {
|
|
886
|
+
if (this.expandedFolderIds.has(entry.id)) {
|
|
887
|
+
this.expandedFolderIds.delete(entry.id);
|
|
888
|
+
await this.refresh();
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
this.expandedFolderIds.add(entry.id);
|
|
893
|
+
await this.ensureFolderLoaded(backend, entry.id);
|
|
894
|
+
await this.refresh();
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
row.addEventListener("keydown", (e) => {
|
|
898
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
899
|
+
e.preventDefault();
|
|
900
|
+
void activate();
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
row.addEventListener("click", async () => {
|
|
905
|
+
await activate();
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
this.listEl.appendChild(row);
|
|
909
|
+
|
|
910
|
+
if (isExpanded) {
|
|
911
|
+
const childKey = this.folderKey(entry.id);
|
|
912
|
+
if (this.folderLoadingById.has(childKey)) {
|
|
913
|
+
const loading = document.createElement("div");
|
|
914
|
+
addClass(loading, "yasgui-query-browser__tree-meta");
|
|
915
|
+
loading.setAttribute("style", makeIndentStyle(depth + 1));
|
|
916
|
+
loading.textContent = "Loading…";
|
|
917
|
+
this.listEl.appendChild(loading);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const err = this.folderErrorById.get(childKey);
|
|
921
|
+
if (err) {
|
|
922
|
+
const error = document.createElement("div");
|
|
923
|
+
addClass(error, "yasgui-query-browser__tree-meta");
|
|
924
|
+
addClass(error, "yasgui-query-browser__tree-meta--error");
|
|
925
|
+
error.setAttribute("style", makeIndentStyle(depth + 1));
|
|
926
|
+
error.textContent = err;
|
|
927
|
+
this.listEl.appendChild(error);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
renderFolderChildren(entry.id, depth + 1);
|
|
931
|
+
}
|
|
932
|
+
} else {
|
|
933
|
+
const row = document.createElement("div");
|
|
934
|
+
addClass(row, "yasgui-query-browser__tree-row");
|
|
935
|
+
addClass(row, "yasgui-query-browser__tree-row--query");
|
|
936
|
+
row.setAttribute("aria-label", `Open query ${entry.label}`);
|
|
937
|
+
row.setAttribute("style", makeIndentStyle(depth));
|
|
938
|
+
row.setAttribute("role", "button");
|
|
939
|
+
row.setAttribute("tabindex", "0");
|
|
940
|
+
|
|
941
|
+
const caretPlaceholder = document.createElement("span");
|
|
942
|
+
addClass(caretPlaceholder, "yasgui-query-browser__tree-caret");
|
|
943
|
+
caretPlaceholder.textContent = "▸";
|
|
944
|
+
caretPlaceholder.setAttribute("aria-hidden", "true");
|
|
945
|
+
addClass(caretPlaceholder, "yasgui-query-browser__tree-caret--placeholder");
|
|
946
|
+
|
|
947
|
+
const icon = document.createElement("span");
|
|
948
|
+
addClass(icon, "yasgui-query-browser__tree-icon");
|
|
949
|
+
icon.textContent = "🕸️";
|
|
950
|
+
|
|
951
|
+
const label = document.createElement("span");
|
|
952
|
+
addClass(label, "yasgui-query-browser__tree-label");
|
|
953
|
+
label.textContent = entry.label;
|
|
954
|
+
|
|
955
|
+
row.appendChild(caretPlaceholder);
|
|
956
|
+
row.appendChild(icon);
|
|
957
|
+
row.appendChild(label);
|
|
958
|
+
|
|
959
|
+
this.attachQueryHoverPreview(row, backend, entry.id);
|
|
960
|
+
this.addQueryRowActions(row, backend, entry);
|
|
961
|
+
|
|
962
|
+
const activate = async () => {
|
|
963
|
+
await this.openManagedQueryInNewTab(backend, backend.type, entry.id, entry.label);
|
|
964
|
+
|
|
965
|
+
this.close();
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
row.addEventListener("keydown", (e) => {
|
|
969
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
970
|
+
e.preventDefault();
|
|
971
|
+
void activate();
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
row.addEventListener("click", async () => {
|
|
976
|
+
await activate();
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
this.listEl.appendChild(row);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
renderFolderChildren(undefined, 0);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
public async refresh() {
|
|
988
|
+
const runId = ++this.refreshRunId;
|
|
989
|
+
this.ensureWorkspaceSelection();
|
|
990
|
+
this.renderWorkspaceSelect();
|
|
991
|
+
|
|
992
|
+
const workspaces = this.getWorkspaces();
|
|
993
|
+
if (!this.selectedWorkspaceId || workspaces.length === 0) {
|
|
994
|
+
this.backButtonEl.disabled = true;
|
|
995
|
+
this.setStatus("");
|
|
996
|
+
this.clearList();
|
|
997
|
+
this.renderEmptyWorkspaceState();
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
const workspace = workspaces.find((w) => w.id === this.selectedWorkspaceId);
|
|
1002
|
+
if (!workspace) return;
|
|
1003
|
+
|
|
1004
|
+
const backend = getWorkspaceBackend(workspace, { persistentConfig: this.yasgui.persistentConfig });
|
|
1005
|
+
const searchQuery = this.searchEl.value;
|
|
1006
|
+
|
|
1007
|
+
// Tree view replaces folder navigation; keep the Back button disabled.
|
|
1008
|
+
this.backButtonEl.disabled = true;
|
|
1009
|
+
|
|
1010
|
+
if (this.treeWorkspaceId !== workspace.id) {
|
|
1011
|
+
this.resetTreeState();
|
|
1012
|
+
this.treeWorkspaceId = workspace.id;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
try {
|
|
1016
|
+
this.setStatus("Loading…");
|
|
1017
|
+
const trimmed = searchQuery.trim();
|
|
1018
|
+
|
|
1019
|
+
if (trimmed) {
|
|
1020
|
+
let entries: FolderEntry[];
|
|
1021
|
+
if (backend.searchByName) {
|
|
1022
|
+
entries = await backend.searchByName(trimmed);
|
|
1023
|
+
} else {
|
|
1024
|
+
const root = await backend.listFolder(undefined);
|
|
1025
|
+
entries = filterFolderEntriesByName(root, trimmed);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (runId !== this.refreshRunId) return;
|
|
1029
|
+
|
|
1030
|
+
const signature = entries.map((e) => this.entrySignature(e)).join("|");
|
|
1031
|
+
this.setStatus(entries.length ? "" : "No results");
|
|
1032
|
+
if (signature !== this.lastRenderedSignature) {
|
|
1033
|
+
this.lastRenderedSignature = signature;
|
|
1034
|
+
this.renderFlatEntries(backend, entries);
|
|
1035
|
+
}
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
await this.ensureFolderLoaded(backend, undefined);
|
|
1040
|
+
|
|
1041
|
+
if (runId !== this.refreshRunId) return;
|
|
1042
|
+
|
|
1043
|
+
const rootKey = this.folderKey(undefined);
|
|
1044
|
+
const rootEntries = this.folderEntriesById.get(rootKey) || [];
|
|
1045
|
+
|
|
1046
|
+
const expandedIds = Array.from(this.expandedFolderIds).sort();
|
|
1047
|
+
const relevantFolderKeys = [rootKey, ...expandedIds.map((id) => this.folderKey(id))].sort();
|
|
1048
|
+
const foldersPart = relevantFolderKeys
|
|
1049
|
+
.map((key) => {
|
|
1050
|
+
const entries = this.folderEntriesById.get(key) || [];
|
|
1051
|
+
const entryPart = entries
|
|
1052
|
+
.map((e) => this.entrySignature(e))
|
|
1053
|
+
.sort()
|
|
1054
|
+
.join("|");
|
|
1055
|
+
const loading = this.folderLoadingById.has(key) ? "1" : "0";
|
|
1056
|
+
const err = this.folderErrorById.get(key) || "";
|
|
1057
|
+
return `${key}:${loading}:${err}:${entryPart}`;
|
|
1058
|
+
})
|
|
1059
|
+
.join(";");
|
|
1060
|
+
|
|
1061
|
+
const signature = [
|
|
1062
|
+
`rootCount:${rootEntries.length}`,
|
|
1063
|
+
`expanded:${expandedIds.join(",")}`,
|
|
1064
|
+
`folders:${foldersPart}`,
|
|
1065
|
+
].join(";");
|
|
1066
|
+
|
|
1067
|
+
this.setStatus(rootEntries.length ? "" : "No queries, add one by saving a tab to this workspace.");
|
|
1068
|
+
if (signature !== this.lastRenderedSignature) {
|
|
1069
|
+
this.lastRenderedSignature = signature;
|
|
1070
|
+
this.renderTree(backend);
|
|
1071
|
+
}
|
|
1072
|
+
} catch (e) {
|
|
1073
|
+
if (runId !== this.refreshRunId) return;
|
|
1074
|
+
const err = asWorkspaceBackendError(e);
|
|
1075
|
+
this.setStatus(this.formatStatusError(err, workspace.type));
|
|
1076
|
+
this.clearList();
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|