@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.
Files changed (133) hide show
  1. package/README.md +6 -2
  2. package/build/ts/src/PersistentConfig.d.ts +10 -0
  3. package/build/ts/src/PersistentConfig.js +40 -0
  4. package/build/ts/src/PersistentConfig.js.map +1 -1
  5. package/build/ts/src/Tab.d.ts +17 -0
  6. package/build/ts/src/Tab.js +372 -15
  7. package/build/ts/src/Tab.js.map +1 -1
  8. package/build/ts/src/TabContextMenu.d.ts +1 -0
  9. package/build/ts/src/TabContextMenu.js +17 -0
  10. package/build/ts/src/TabContextMenu.js.map +1 -1
  11. package/build/ts/src/TabElements.d.ts +3 -0
  12. package/build/ts/src/TabElements.js +97 -28
  13. package/build/ts/src/TabElements.js.map +1 -1
  14. package/build/ts/src/TabSettingsModal.d.ts +2 -0
  15. package/build/ts/src/TabSettingsModal.js +44 -19
  16. package/build/ts/src/TabSettingsModal.js.map +1 -1
  17. package/build/ts/src/index.d.ts +3 -0
  18. package/build/ts/src/index.js +4 -0
  19. package/build/ts/src/index.js.map +1 -1
  20. package/build/ts/src/queryManagement/QueryBrowser.d.ts +64 -0
  21. package/build/ts/src/queryManagement/QueryBrowser.js +914 -0
  22. package/build/ts/src/queryManagement/QueryBrowser.js.map +1 -0
  23. package/build/ts/src/queryManagement/SaveManagedQueryModal.d.ts +55 -0
  24. package/build/ts/src/queryManagement/SaveManagedQueryModal.js +451 -0
  25. package/build/ts/src/queryManagement/SaveManagedQueryModal.js.map +1 -0
  26. package/build/ts/src/queryManagement/WorkspaceSettingsForm.d.ts +16 -0
  27. package/build/ts/src/queryManagement/WorkspaceSettingsForm.js +452 -0
  28. package/build/ts/src/queryManagement/WorkspaceSettingsForm.js.map +1 -0
  29. package/build/ts/src/queryManagement/backends/BaseGitProviderClient.d.ts +19 -0
  30. package/build/ts/src/queryManagement/backends/BaseGitProviderClient.js +77 -0
  31. package/build/ts/src/queryManagement/backends/BaseGitProviderClient.js.map +1 -0
  32. package/build/ts/src/queryManagement/backends/BitbucketProviderClient.d.ts +16 -0
  33. package/build/ts/src/queryManagement/backends/BitbucketProviderClient.js +269 -0
  34. package/build/ts/src/queryManagement/backends/BitbucketProviderClient.js.map +1 -0
  35. package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.d.ts +26 -0
  36. package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.js +93 -0
  37. package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.js.map +1 -0
  38. package/build/ts/src/queryManagement/backends/GiteaProviderClient.d.ts +14 -0
  39. package/build/ts/src/queryManagement/backends/GiteaProviderClient.js +244 -0
  40. package/build/ts/src/queryManagement/backends/GiteaProviderClient.js.map +1 -0
  41. package/build/ts/src/queryManagement/backends/GithubProviderClient.d.ts +14 -0
  42. package/build/ts/src/queryManagement/backends/GithubProviderClient.js +252 -0
  43. package/build/ts/src/queryManagement/backends/GithubProviderClient.js.map +1 -0
  44. package/build/ts/src/queryManagement/backends/GitlabProviderClient.d.ts +16 -0
  45. package/build/ts/src/queryManagement/backends/GitlabProviderClient.js +246 -0
  46. package/build/ts/src/queryManagement/backends/GitlabProviderClient.js.map +1 -0
  47. package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.d.ts +21 -0
  48. package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.js +175 -0
  49. package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.js.map +1 -0
  50. package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.d.ts +28 -0
  51. package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.js +687 -0
  52. package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.js.map +1 -0
  53. package/build/ts/src/queryManagement/backends/WorkspaceBackend.d.ts +15 -0
  54. package/build/ts/src/queryManagement/backends/WorkspaceBackend.js +2 -0
  55. package/build/ts/src/queryManagement/backends/WorkspaceBackend.js.map +1 -0
  56. package/build/ts/src/queryManagement/backends/errors.d.ts +7 -0
  57. package/build/ts/src/queryManagement/backends/errors.js +18 -0
  58. package/build/ts/src/queryManagement/backends/errors.js.map +1 -0
  59. package/build/ts/src/queryManagement/backends/getWorkspaceBackend.d.ts +8 -0
  60. package/build/ts/src/queryManagement/backends/getWorkspaceBackend.js +114 -0
  61. package/build/ts/src/queryManagement/backends/getWorkspaceBackend.js.map +1 -0
  62. package/build/ts/src/queryManagement/backends/gitRemote.d.ts +5 -0
  63. package/build/ts/src/queryManagement/backends/gitRemote.js +40 -0
  64. package/build/ts/src/queryManagement/backends/gitRemote.js.map +1 -0
  65. package/build/ts/src/queryManagement/browserFilter.d.ts +2 -0
  66. package/build/ts/src/queryManagement/browserFilter.js +7 -0
  67. package/build/ts/src/queryManagement/browserFilter.js.map +1 -0
  68. package/build/ts/src/queryManagement/index.d.ts +13 -0
  69. package/build/ts/src/queryManagement/index.js +14 -0
  70. package/build/ts/src/queryManagement/index.js.map +1 -0
  71. package/build/ts/src/queryManagement/normalizeQueryFilename.d.ts +1 -0
  72. package/build/ts/src/queryManagement/normalizeQueryFilename.js +10 -0
  73. package/build/ts/src/queryManagement/normalizeQueryFilename.js.map +1 -0
  74. package/build/ts/src/queryManagement/openManagedQuery.d.ts +15 -0
  75. package/build/ts/src/queryManagement/openManagedQuery.js +27 -0
  76. package/build/ts/src/queryManagement/openManagedQuery.js.map +1 -0
  77. package/build/ts/src/queryManagement/saveManagedQuery.d.ts +20 -0
  78. package/build/ts/src/queryManagement/saveManagedQuery.js +109 -0
  79. package/build/ts/src/queryManagement/saveManagedQuery.js.map +1 -0
  80. package/build/ts/src/queryManagement/textHash.d.ts +2 -0
  81. package/build/ts/src/queryManagement/textHash.js +13 -0
  82. package/build/ts/src/queryManagement/textHash.js.map +1 -0
  83. package/build/ts/src/queryManagement/types.d.ts +76 -0
  84. package/build/ts/src/queryManagement/types.js +2 -0
  85. package/build/ts/src/queryManagement/types.js.map +1 -0
  86. package/build/ts/src/queryManagement/validateWorkspaceConfig.d.ts +6 -0
  87. package/build/ts/src/queryManagement/validateWorkspaceConfig.js +33 -0
  88. package/build/ts/src/queryManagement/validateWorkspaceConfig.js.map +1 -0
  89. package/build/ts/src/version.d.ts +1 -1
  90. package/build/ts/src/version.js +1 -1
  91. package/build/yasgui.min.css +10 -1
  92. package/build/yasgui.min.css.map +3 -3
  93. package/build/yasgui.min.js +398 -172
  94. package/build/yasgui.min.js.map +4 -4
  95. package/package.json +1 -1
  96. package/src/PersistentConfig.ts +61 -0
  97. package/src/Tab.ts +431 -20
  98. package/src/TabContextMenu.ts +10 -0
  99. package/src/TabElements.scss +46 -7
  100. package/src/TabElements.ts +95 -27
  101. package/src/TabSettingsModal.scss +102 -5
  102. package/src/TabSettingsModal.ts +48 -25
  103. package/src/endpointSelect.scss +2 -3
  104. package/src/index.scss +4 -0
  105. package/src/index.ts +7 -0
  106. package/src/queryManagement/QueryBrowser.scss +418 -0
  107. package/src/queryManagement/QueryBrowser.ts +1079 -0
  108. package/src/queryManagement/SaveManagedQueryModal.scss +245 -0
  109. package/src/queryManagement/SaveManagedQueryModal.ts +554 -0
  110. package/src/queryManagement/WorkspaceSettingsForm.ts +546 -0
  111. package/src/queryManagement/backends/BaseGitProviderClient.ts +124 -0
  112. package/src/queryManagement/backends/BitbucketProviderClient.ts +403 -0
  113. package/src/queryManagement/backends/GitWorkspaceBackend.ts +96 -0
  114. package/src/queryManagement/backends/GiteaProviderClient.ts +316 -0
  115. package/src/queryManagement/backends/GithubProviderClient.ts +319 -0
  116. package/src/queryManagement/backends/GitlabProviderClient.ts +327 -0
  117. package/src/queryManagement/backends/InMemoryWorkspaceBackend.ts +175 -0
  118. package/src/queryManagement/backends/SparqlWorkspaceBackend.ts +729 -0
  119. package/src/queryManagement/backends/WorkspaceBackend.ts +41 -0
  120. package/src/queryManagement/backends/errors.ts +28 -0
  121. package/src/queryManagement/backends/getWorkspaceBackend.ts +132 -0
  122. package/src/queryManagement/backends/gitRemote.ts +54 -0
  123. package/src/queryManagement/browserFilter.ts +8 -0
  124. package/src/queryManagement/index.ts +15 -0
  125. package/src/queryManagement/normalizeQueryFilename.ts +8 -0
  126. package/src/queryManagement/openManagedQuery.ts +31 -0
  127. package/src/queryManagement/saveManagedQuery.ts +135 -0
  128. package/src/queryManagement/textHash.ts +15 -0
  129. package/src/queryManagement/types.ts +85 -0
  130. package/src/queryManagement/validateWorkspaceConfig.ts +40 -0
  131. package/src/tab.scss +4 -14
  132. package/src/themes.scss +14 -23
  133. package/src/version.ts +1 -1
@@ -0,0 +1,546 @@
1
+ import type PersistentConfig from "../PersistentConfig";
2
+ import type { WorkspaceConfig } from "./types";
3
+ import { addClass } from "@matdata/yasgui-utils";
4
+ import { validateWorkspaceConfig } from "./validateWorkspaceConfig";
5
+ import { getWorkspaceBackend } from "./backends/getWorkspaceBackend";
6
+ import { asWorkspaceBackendError } from "./backends/errors";
7
+
8
+ export interface WorkspaceSettingsFormOptions {
9
+ persistentConfig: PersistentConfig;
10
+ onDeleteRequested: (workspaceId: string) => void;
11
+ }
12
+
13
+ function isNonEmpty(value: string | undefined): boolean {
14
+ return !!value && value.trim().length > 0;
15
+ }
16
+
17
+ function newWorkspaceId(): string {
18
+ return `ws_${Date.now().toString(36)}_${Math.random().toString(16).slice(2)}`;
19
+ }
20
+
21
+ export class WorkspaceSettingsForm {
22
+ private options: WorkspaceSettingsFormOptions;
23
+ private container: HTMLElement;
24
+
25
+ constructor(container: HTMLElement, options: WorkspaceSettingsFormOptions) {
26
+ this.container = container;
27
+ this.options = options;
28
+ }
29
+
30
+ public render() {
31
+ this.container.innerHTML = "";
32
+
33
+ const header = document.createElement("div");
34
+ addClass(header, "settingsSection");
35
+
36
+ const label = document.createElement("label");
37
+ label.textContent = "Workspaces";
38
+ addClass(label, "settingsLabel");
39
+
40
+ const help = document.createElement("div");
41
+ help.textContent =
42
+ "Configure managed-query workspaces (Git or SPARQL). Credentials are stored locally and are never shown again after entry.";
43
+ addClass(help, "settingsHelp");
44
+
45
+ header.appendChild(label);
46
+ header.appendChild(help);
47
+
48
+ const activeRow = document.createElement("div");
49
+ addClass(activeRow, "checkboxContainer");
50
+
51
+ const activeLabel = document.createElement("label");
52
+ activeLabel.textContent = "Default workspace";
53
+ activeLabel.style.marginRight = "10px";
54
+
55
+ const activeSelect = document.createElement("select");
56
+ activeSelect.setAttribute("aria-label", "Select default workspace");
57
+ addClass(activeSelect, "settingsSelect");
58
+
59
+ const workspaces = this.options.persistentConfig.getWorkspaces();
60
+ const persistedActive = this.options.persistentConfig.getActiveWorkspaceId();
61
+
62
+ const placeholder = document.createElement("option");
63
+ placeholder.value = "";
64
+ placeholder.textContent = workspaces.length ? "Select workspace" : "No workspaces";
65
+ activeSelect.appendChild(placeholder);
66
+
67
+ for (const w of workspaces) {
68
+ const opt = document.createElement("option");
69
+ opt.value = w.id;
70
+ opt.textContent = w.label;
71
+ activeSelect.appendChild(opt);
72
+ }
73
+
74
+ activeSelect.value = persistedActive || "";
75
+ activeSelect.onchange = () => {
76
+ const selected = activeSelect.value || undefined;
77
+ this.options.persistentConfig.setActiveWorkspaceId(selected);
78
+ };
79
+
80
+ activeRow.appendChild(activeLabel);
81
+ activeRow.appendChild(activeSelect);
82
+
83
+ header.appendChild(activeRow);
84
+ this.container.appendChild(header);
85
+
86
+ if (workspaces.length === 0) {
87
+ const empty = document.createElement("div");
88
+ addClass(empty, "settingsHelp");
89
+ empty.textContent = "No workspaces configured yet. Add one below.";
90
+ this.container.appendChild(empty);
91
+ }
92
+
93
+ this.renderWorkspaceList(workspaces);
94
+ this.renderAddWorkspaceButton();
95
+ }
96
+
97
+ private renderWorkspaceList(workspaces: WorkspaceConfig[]) {
98
+ if (workspaces.length === 0) return;
99
+
100
+ const section = document.createElement("div");
101
+ addClass(section, "settingsSection");
102
+
103
+ const label = document.createElement("label");
104
+ label.textContent = "Configured workspaces";
105
+ addClass(label, "settingsLabel");
106
+ section.appendChild(label);
107
+
108
+ const list = document.createElement("div");
109
+ addClass(list, "workspaceList");
110
+
111
+ for (const workspace of workspaces) {
112
+ const row = document.createElement("div");
113
+ addClass(row, "workspaceListRow");
114
+
115
+ const nameEl = document.createElement("div");
116
+ addClass(nameEl, "workspaceListLabel");
117
+ nameEl.textContent = workspace.label || workspace.id;
118
+ row.appendChild(nameEl);
119
+
120
+ const actions = document.createElement("div");
121
+ addClass(actions, "workspaceListActions");
122
+
123
+ const status = document.createElement("div");
124
+ addClass(status, "workspaceListStatus");
125
+
126
+ const editBtn = document.createElement("button");
127
+ editBtn.type = "button";
128
+ editBtn.textContent = "Edit";
129
+ addClass(editBtn, "secondaryButton");
130
+ editBtn.onclick = () => {
131
+ status.textContent = "";
132
+ this.openWorkspaceConfigModal({ mode: "edit", workspace });
133
+ };
134
+
135
+ const validateBtn = document.createElement("button");
136
+ validateBtn.type = "button";
137
+ validateBtn.textContent = "Validate";
138
+ addClass(validateBtn, "secondaryButton");
139
+ validateBtn.onclick = async () => {
140
+ try {
141
+ status.textContent = "Validating…";
142
+ const backend = getWorkspaceBackend(workspace, { persistentConfig: this.options.persistentConfig });
143
+ await backend.validateAccess();
144
+ status.textContent = "✓ Access OK";
145
+ } catch (e) {
146
+ const err = asWorkspaceBackendError(e);
147
+ status.textContent = err.message;
148
+ }
149
+ };
150
+
151
+ const removeBtn = document.createElement("button");
152
+ removeBtn.type = "button";
153
+ removeBtn.textContent = "Remove";
154
+ addClass(removeBtn, "dangerButton");
155
+ removeBtn.onclick = () => {
156
+ status.textContent = "";
157
+ this.options.onDeleteRequested(workspace.id);
158
+ };
159
+
160
+ actions.appendChild(editBtn);
161
+ actions.appendChild(validateBtn);
162
+ actions.appendChild(removeBtn);
163
+ row.appendChild(actions);
164
+ row.appendChild(status);
165
+ list.appendChild(row);
166
+ }
167
+
168
+ section.appendChild(list);
169
+ this.container.appendChild(section);
170
+ }
171
+
172
+ private createEndpointSelect(currentValue?: string): HTMLSelectElement {
173
+ const select = document.createElement("select");
174
+ select.setAttribute("aria-label", "SPARQL endpoint");
175
+ addClass(select, "settingsSelect");
176
+
177
+ const configs = this.options.persistentConfig.getEndpointConfigs();
178
+ const endpoints = configs
179
+ .map((c) => ({ endpoint: c.endpoint, label: c.label?.trim() }))
180
+ .filter((c) => !!c.endpoint && c.endpoint.trim().length > 0);
181
+
182
+ endpoints.sort((a, b) => {
183
+ const aKey = (a.label || a.endpoint).toLowerCase();
184
+ const bKey = (b.label || b.endpoint).toLowerCase();
185
+ return aKey.localeCompare(bKey);
186
+ });
187
+
188
+ const placeholder = document.createElement("option");
189
+ placeholder.value = "";
190
+ placeholder.textContent = endpoints.length ? "Select SPARQL endpoint" : "No SPARQL endpoints configured";
191
+ select.appendChild(placeholder);
192
+
193
+ const trimmedCurrent = currentValue?.trim();
194
+ if (trimmedCurrent && !endpoints.some((e) => e.endpoint === trimmedCurrent)) {
195
+ const opt = document.createElement("option");
196
+ opt.value = trimmedCurrent;
197
+ opt.textContent = `${trimmedCurrent} (not in list)`;
198
+ select.appendChild(opt);
199
+ }
200
+
201
+ for (const e of endpoints) {
202
+ const opt = document.createElement("option");
203
+ opt.value = e.endpoint;
204
+ opt.textContent = e.label ? `${e.label} (${e.endpoint})` : e.endpoint;
205
+ select.appendChild(opt);
206
+ }
207
+
208
+ select.value = trimmedCurrent || "";
209
+ return select;
210
+ }
211
+
212
+ private renderAddWorkspaceButton() {
213
+ const section = document.createElement("div");
214
+ addClass(section, "settingsSection");
215
+
216
+ const label = document.createElement("label");
217
+ label.textContent = "Add workspace";
218
+ addClass(label, "settingsLabel");
219
+
220
+ const help = document.createElement("div");
221
+ help.textContent = "Add a new workspace configuration.";
222
+ addClass(help, "settingsHelp");
223
+
224
+ const addBtn = document.createElement("button");
225
+ addBtn.type = "button";
226
+ addBtn.textContent = "+ Add new workspace";
227
+ addClass(addBtn, "primaryButton");
228
+ addBtn.onclick = () => this.openWorkspaceConfigModal({ mode: "add" });
229
+
230
+ section.appendChild(label);
231
+ section.appendChild(help);
232
+ section.appendChild(addBtn);
233
+ this.container.appendChild(section);
234
+ }
235
+
236
+ private openWorkspaceConfigModal(input: { mode: "add" } | { mode: "edit"; workspace: WorkspaceConfig }) {
237
+ const overlay = document.createElement("div");
238
+ addClass(overlay, "tabSettingsModalOverlay", "workspaceConfigModalOverlay", "open");
239
+
240
+ const modal = document.createElement("div");
241
+ addClass(modal, "workspaceConfigModal");
242
+ modal.onclick = (e) => e.stopPropagation();
243
+
244
+ const close = () => {
245
+ overlay.remove();
246
+ document.removeEventListener("keydown", onKeyDown);
247
+ };
248
+
249
+ const onKeyDown = (e: KeyboardEvent) => {
250
+ if (e.key !== "Escape") return;
251
+ e.preventDefault();
252
+ close();
253
+ };
254
+ document.addEventListener("keydown", onKeyDown);
255
+
256
+ overlay.onclick = () => close();
257
+
258
+ const header = document.createElement("div");
259
+ addClass(header, "modalHeader");
260
+
261
+ const title = document.createElement("h2");
262
+ title.textContent = input.mode === "add" ? "Add workspace" : "Edit workspace";
263
+ header.appendChild(title);
264
+
265
+ const closeBtn = document.createElement("button");
266
+ closeBtn.textContent = "×";
267
+ addClass(closeBtn, "closeButton");
268
+ closeBtn.onclick = () => close();
269
+ header.appendChild(closeBtn);
270
+
271
+ const body = document.createElement("div");
272
+ addClass(body, "workspaceConfigModalBody");
273
+
274
+ const status = document.createElement("div");
275
+ addClass(status, "settingsHelp");
276
+ status.style.fontStyle = "normal";
277
+
278
+ const mode = input.mode;
279
+ const existing = mode === "edit" ? input.workspace : undefined;
280
+
281
+ const typeSelect = document.createElement("select");
282
+ typeSelect.setAttribute("aria-label", "Workspace type");
283
+ addClass(typeSelect, "settingsSelect");
284
+ const optGit = document.createElement("option");
285
+ optGit.value = "git";
286
+ optGit.textContent = "Git";
287
+ const optSparql = document.createElement("option");
288
+ optSparql.value = "sparql";
289
+ optSparql.textContent = "SPARQL";
290
+ typeSelect.appendChild(optGit);
291
+ typeSelect.appendChild(optSparql);
292
+
293
+ if (existing) {
294
+ typeSelect.value = existing.type;
295
+ typeSelect.disabled = true;
296
+ }
297
+
298
+ const labelInput = document.createElement("input");
299
+ labelInput.type = "text";
300
+ labelInput.placeholder = "Workspace label";
301
+ labelInput.value = existing?.label || "";
302
+ addClass(labelInput, "settingsInput");
303
+
304
+ const descriptionInput = document.createElement("input");
305
+ descriptionInput.type = "text";
306
+ descriptionInput.placeholder = "Optional description";
307
+ descriptionInput.value = existing?.description || "";
308
+ addClass(descriptionInput, "settingsInput");
309
+
310
+ const dynamic = document.createElement("div");
311
+
312
+ const renderDynamic = () => {
313
+ dynamic.innerHTML = "";
314
+ status.textContent = "";
315
+
316
+ const typeValue = existing?.type ?? (typeSelect.value as WorkspaceConfig["type"]);
317
+
318
+ if (typeValue === "git") {
319
+ const gitHelp = document.createElement("div");
320
+ addClass(gitHelp, "settingsHelp");
321
+ gitHelp.textContent =
322
+ "Supported Git providers: GitHub, GitLab, Bitbucket Cloud, Gitea. Note: ssh:// and SCP-style remotes are only parsed to identify the repository; YASGUI uses provider HTTPS APIs (not SSH) to read/write files.";
323
+
324
+ const remoteUrlInput = document.createElement("input");
325
+ remoteUrlInput.type = "url";
326
+ remoteUrlInput.placeholder = "Git remote URL (https://..., ssh://..., or git@host:org/repo.git)";
327
+ remoteUrlInput.value = existing && existing.type === "git" ? existing.remoteUrl : "";
328
+ addClass(remoteUrlInput, "settingsInput");
329
+
330
+ const branchInput = document.createElement("input");
331
+ branchInput.type = "text";
332
+ branchInput.placeholder = "Branch (optional; e.g., main)";
333
+ branchInput.value = existing && existing.type === "git" ? existing.branch : "";
334
+ addClass(branchInput, "settingsInput");
335
+
336
+ const rootPathInput = document.createElement("input");
337
+ rootPathInput.type = "text";
338
+ rootPathInput.placeholder = "Root path in repo (optional)";
339
+ rootPathInput.value = existing && existing.type === "git" ? existing.rootPath : "";
340
+ addClass(rootPathInput, "settingsInput");
341
+
342
+ const usernameInput = document.createElement("input");
343
+ usernameInput.type = "text";
344
+ usernameInput.placeholder = "Username (optional)";
345
+ usernameInput.value = existing && existing.type === "git" ? existing.auth.username || "" : "";
346
+ addClass(usernameInput, "settingsInput");
347
+
348
+ const providerSelect = document.createElement("select");
349
+ providerSelect.setAttribute("aria-label", "Git provider");
350
+ addClass(providerSelect, "settingsSelect");
351
+ const providerOptions: Array<{ value: "auto" | "github" | "gitlab" | "bitbucket" | "gitea"; label: string }> = [
352
+ { value: "auto", label: "Auto-detect" },
353
+ { value: "github", label: "GitHub" },
354
+ { value: "gitlab", label: "GitLab" },
355
+ { value: "bitbucket", label: "Bitbucket Cloud" },
356
+ { value: "gitea", label: "Gitea" },
357
+ ];
358
+ for (const opt of providerOptions) {
359
+ const option = document.createElement("option");
360
+ option.value = opt.value;
361
+ option.textContent = opt.label;
362
+ providerSelect.appendChild(option);
363
+ }
364
+ providerSelect.value = (existing && existing.type === "git" && existing.provider) || "auto";
365
+
366
+ const apiBaseUrlInput = document.createElement("input");
367
+ apiBaseUrlInput.type = "url";
368
+ apiBaseUrlInput.placeholder = "API base URL (optional; for self-hosted/enterprise)";
369
+ apiBaseUrlInput.value = existing && existing.type === "git" ? existing.apiBaseUrl || "" : "";
370
+ addClass(apiBaseUrlInput, "settingsInput");
371
+
372
+ const tokenInput = document.createElement("input");
373
+ tokenInput.type = "password";
374
+ tokenInput.value = "";
375
+ tokenInput.placeholder =
376
+ existing && existing.type === "git" && existing.auth.token
377
+ ? "Token configured (enter to replace)"
378
+ : "Personal access token";
379
+ tokenInput.autocomplete = "new-password";
380
+ addClass(tokenInput, "settingsInput");
381
+
382
+ const advancedToggle = document.createElement("button");
383
+ advancedToggle.type = "button";
384
+ addClass(advancedToggle, "secondaryButton");
385
+
386
+ const advancedContainer = document.createElement("div");
387
+ let isAdvancedOpen = false;
388
+ const syncAdvanced = () => {
389
+ advancedToggle.textContent = isAdvancedOpen ? "Hide advanced" : "Show advanced";
390
+ advancedContainer.style.display = isAdvancedOpen ? "" : "none";
391
+ };
392
+ advancedToggle.onclick = () => {
393
+ isAdvancedOpen = !isAdvancedOpen;
394
+ syncAdvanced();
395
+ };
396
+ syncAdvanced();
397
+
398
+ advancedContainer.appendChild(this.wrapField("Provider", providerSelect));
399
+ advancedContainer.appendChild(this.wrapField("API base URL", apiBaseUrlInput));
400
+ advancedContainer.appendChild(this.wrapField("Branch", branchInput));
401
+ advancedContainer.appendChild(this.wrapField("Root path", rootPathInput));
402
+ advancedContainer.appendChild(this.wrapField("Username", usernameInput));
403
+
404
+ dynamic.appendChild(gitHelp);
405
+ dynamic.appendChild(this.wrapField("Remote URL", remoteUrlInput));
406
+ dynamic.appendChild(this.wrapField("Token", tokenInput));
407
+ dynamic.appendChild(this.wrapField("Advanced", advancedToggle));
408
+ dynamic.appendChild(advancedContainer);
409
+
410
+ (dynamic as any).__getConfig = (): WorkspaceConfig => {
411
+ const token = tokenInput.value.trim();
412
+ const previousToken = existing && existing.type === "git" ? existing.auth.token : undefined;
413
+ const provider = providerSelect.value as "auto" | "github" | "gitlab" | "bitbucket" | "gitea";
414
+ return {
415
+ id: existing?.id || newWorkspaceId(),
416
+ type: "git",
417
+ label: labelInput.value.trim(),
418
+ description: descriptionInput.value.trim() || undefined,
419
+ remoteUrl: remoteUrlInput.value.trim(),
420
+ branch: branchInput.value.trim(),
421
+ rootPath: rootPathInput.value.trim(),
422
+ auth: {
423
+ type: "pat",
424
+ username: usernameInput.value.trim() || undefined,
425
+ token: isNonEmpty(token) ? token : previousToken || "",
426
+ },
427
+ provider: provider === "auto" ? undefined : provider,
428
+ apiBaseUrl: apiBaseUrlInput.value.trim() || undefined,
429
+ };
430
+ };
431
+ return;
432
+ }
433
+
434
+ const endpointSelect = this.createEndpointSelect(
435
+ existing && existing.type === "sparql" ? existing.endpoint : undefined,
436
+ );
437
+
438
+ const sparqlHelp = document.createElement("div");
439
+ addClass(sparqlHelp, "settingsHelp");
440
+ sparqlHelp.textContent =
441
+ "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
+
443
+ const workspaceIriInput = document.createElement("input");
444
+ workspaceIriInput.type = "url";
445
+ workspaceIriInput.placeholder = "Workspace IRI";
446
+ workspaceIriInput.value = existing && existing.type === "sparql" ? existing.workspaceIri : "";
447
+ addClass(workspaceIriInput, "settingsInput");
448
+
449
+ const defaultGraphInput = document.createElement("input");
450
+ defaultGraphInput.type = "url";
451
+ defaultGraphInput.placeholder = "Default graph (optional)";
452
+ defaultGraphInput.value = existing && existing.type === "sparql" ? existing.defaultGraph || "" : "";
453
+ addClass(defaultGraphInput, "settingsInput");
454
+
455
+ dynamic.appendChild(this.wrapField("SPARQL endpoint", endpointSelect));
456
+ dynamic.appendChild(sparqlHelp);
457
+ dynamic.appendChild(this.wrapField("Workspace IRI", workspaceIriInput));
458
+ dynamic.appendChild(this.wrapField("Default graph", defaultGraphInput));
459
+
460
+ (dynamic as any).__getConfig = (): WorkspaceConfig => {
461
+ return {
462
+ id: existing?.id || newWorkspaceId(),
463
+ type: "sparql",
464
+ label: labelInput.value.trim(),
465
+ description: descriptionInput.value.trim() || undefined,
466
+ endpoint: endpointSelect.value.trim(),
467
+ workspaceIri: workspaceIriInput.value.trim(),
468
+ defaultGraph: defaultGraphInput.value.trim() || undefined,
469
+ };
470
+ };
471
+ };
472
+
473
+ typeSelect.onchange = () => renderDynamic();
474
+ renderDynamic();
475
+
476
+ const footer = document.createElement("div");
477
+ addClass(footer, "workspaceConfigModalFooter");
478
+
479
+ const cancelBtn = document.createElement("button");
480
+ cancelBtn.type = "button";
481
+ cancelBtn.textContent = "Cancel";
482
+ addClass(cancelBtn, "secondaryButton");
483
+ cancelBtn.onclick = () => close();
484
+
485
+ const saveBtn = document.createElement("button");
486
+ saveBtn.type = "button";
487
+ saveBtn.textContent = "Save";
488
+ addClass(saveBtn, "primaryButton");
489
+ saveBtn.onclick = () => {
490
+ status.textContent = "";
491
+ const getConfig = (dynamic as any).__getConfig as undefined | (() => WorkspaceConfig);
492
+ if (!getConfig) return;
493
+ const config = getConfig();
494
+
495
+ if (config.type === "git" && !isNonEmpty(config.auth.token)) {
496
+ status.textContent = "Git token is required.";
497
+ return;
498
+ }
499
+
500
+ const result = validateWorkspaceConfig(config);
501
+ if (!result.valid) {
502
+ status.textContent = result.errors.join("\n");
503
+ return;
504
+ }
505
+
506
+ this.options.persistentConfig.addOrUpdateWorkspace(config);
507
+ if (!this.options.persistentConfig.getActiveWorkspaceId()) {
508
+ this.options.persistentConfig.setActiveWorkspaceId(config.id);
509
+ }
510
+ close();
511
+ this.render();
512
+ };
513
+
514
+ footer.appendChild(cancelBtn);
515
+ footer.appendChild(saveBtn);
516
+
517
+ body.appendChild(this.wrapField("Type", typeSelect));
518
+ body.appendChild(this.wrapField("Label", labelInput));
519
+ body.appendChild(this.wrapField("Description", descriptionInput));
520
+ body.appendChild(dynamic);
521
+ body.appendChild(status);
522
+
523
+ modal.appendChild(header);
524
+ modal.appendChild(body);
525
+ modal.appendChild(footer);
526
+ overlay.appendChild(modal);
527
+ document.body.appendChild(overlay);
528
+
529
+ // Focus first input for faster entry.
530
+ labelInput.focus();
531
+ }
532
+
533
+ private wrapField(labelText: string, inputEl: HTMLElement): HTMLElement {
534
+ const wrapper = document.createElement("div");
535
+ addClass(wrapper, "settingsField");
536
+
537
+ const label = document.createElement("div");
538
+ addClass(label, "settingsFieldLabel");
539
+ label.textContent = labelText;
540
+
541
+ wrapper.appendChild(label);
542
+ wrapper.appendChild(inputEl);
543
+
544
+ return wrapper;
545
+ }
546
+ }
@@ -0,0 +1,124 @@
1
+ import type { GitWorkspaceConfig, WriteQueryOptions } from "../types";
2
+ import type { GitProviderClient } from "./GitWorkspaceBackend";
3
+ import { WorkspaceBackendError } from "./errors";
4
+
5
+ /**
6
+ * Base class for Git provider clients that provides common functionality
7
+ * such as helper methods, error handling, and consistent commit message generation.
8
+ */
9
+ export abstract class BaseGitProviderClient implements GitProviderClient {
10
+ /**
11
+ * Join path parts into a single path string, filtering out empty parts
12
+ * and normalizing slashes.
13
+ */
14
+ protected joinPath(...parts: Array<string | undefined>): string {
15
+ const cleaned = parts
16
+ .filter((p): p is string => !!p)
17
+ .map((p) => p.replace(/^\/+|\/+$/g, ""))
18
+ .filter((p) => p.length > 0);
19
+ return cleaned.join("/");
20
+ }
21
+
22
+ /**
23
+ * Encode a path for use in URLs, encoding each segment separately.
24
+ */
25
+ protected encodePath(path: string): string {
26
+ return path
27
+ .split("/")
28
+ .filter((p) => p.length > 0)
29
+ .map((p) => encodeURIComponent(p))
30
+ .join("/");
31
+ }
32
+
33
+ /**
34
+ * Base64 encode a UTF-8 string.
35
+ */
36
+ protected base64EncodeUtf8(value: string): string {
37
+ if (typeof (globalThis as any).btoa === "function") {
38
+ // btoa is latin1-only; encode to bytes first.
39
+ const bytes = new TextEncoder().encode(value);
40
+ let binary = "";
41
+ for (const b of bytes) binary += String.fromCharCode(b);
42
+ return (globalThis as any).btoa(binary);
43
+ }
44
+
45
+ const buf = (globalThis as any).Buffer;
46
+ if (buf && typeof buf.from === "function") return buf.from(value, "utf8").toString("base64");
47
+ throw new Error("Base64 encoding not available in this environment");
48
+ }
49
+
50
+ /**
51
+ * Base64 decode a UTF-8 string.
52
+ */
53
+ protected base64DecodeUtf8(value: string): string {
54
+ const cleaned = value.replace(/\s+/g, "");
55
+
56
+ if (typeof (globalThis as any).atob === "function") {
57
+ const binary = (globalThis as any).atob(cleaned);
58
+ const bytes = new Uint8Array(binary.length);
59
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
60
+ return new TextDecoder().decode(bytes);
61
+ }
62
+
63
+ const buf = (globalThis as any).Buffer;
64
+ if (buf && typeof buf.from === "function") return buf.from(cleaned, "base64").toString("utf8");
65
+ throw new Error("Base64 decoding not available in this environment");
66
+ }
67
+
68
+ /**
69
+ * Basic auth encoding for username:password.
70
+ */
71
+ protected base64Encode(value: string): string {
72
+ if (typeof (globalThis as any).btoa === "function") return (globalThis as any).btoa(value);
73
+ const buf = (globalThis as any).Buffer;
74
+ if (buf && typeof buf.from === "function") return buf.from(value, "utf8").toString("base64");
75
+ throw new Error("Base64 encoding not available in this environment");
76
+ }
77
+
78
+ /**
79
+ * Ensure HTTP status is OK (2xx), throw appropriate error otherwise.
80
+ */
81
+ protected ensureOk(status: number, message: string): void {
82
+ if (status >= 200 && status < 300) return;
83
+ if (status === 401) throw new WorkspaceBackendError("AUTH_FAILED", message);
84
+ if (status === 403) throw new WorkspaceBackendError("FORBIDDEN", message);
85
+ if (status === 404) throw new WorkspaceBackendError("NOT_FOUND", message);
86
+ if (status === 409) throw new WorkspaceBackendError("CONFLICT", message);
87
+ if (status === 429) throw new WorkspaceBackendError("RATE_LIMITED", message);
88
+ throw new WorkspaceBackendError("UNKNOWN", message);
89
+ }
90
+
91
+ /**
92
+ * Generate consistent commit message for write operations.
93
+ * Uses "Add" for new files and "Update" for existing files.
94
+ */
95
+ protected getCommitMessage(queryId: string, options: WriteQueryOptions | undefined, isNew: boolean): string {
96
+ if (options?.message?.trim()) return options.message.trim();
97
+ return isNew ? `Add ${queryId}` : `Update ${queryId}`;
98
+ }
99
+
100
+ /**
101
+ * Generate consistent commit message for delete operations.
102
+ */
103
+ protected getDeleteMessage(queryId: string): string {
104
+ return `Delete ${queryId}`;
105
+ }
106
+
107
+ // Abstract methods that must be implemented by concrete providers
108
+ abstract validateAccess(config: GitWorkspaceConfig): Promise<void>;
109
+ abstract listFolder(config: GitWorkspaceConfig, folderId?: string): Promise<import("../types").FolderEntry[]>;
110
+ abstract readQuery(config: GitWorkspaceConfig, queryId: string): Promise<import("../types").ReadResult>;
111
+ abstract writeQuery(
112
+ config: GitWorkspaceConfig,
113
+ queryId: string,
114
+ queryText: string,
115
+ options?: WriteQueryOptions,
116
+ ): Promise<void>;
117
+ abstract listVersions(config: GitWorkspaceConfig, queryId: string): Promise<import("../types").VersionInfo[]>;
118
+ abstract readVersion(
119
+ config: GitWorkspaceConfig,
120
+ queryId: string,
121
+ versionId: string,
122
+ ): Promise<import("../types").ReadResult>;
123
+ abstract deleteQuery(config: GitWorkspaceConfig, queryId: string): Promise<void>;
124
+ }