@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,554 @@
1
+ import type Yasgui from "../index";
2
+ import { addClass, removeClass } from "@matdata/yasgui-utils";
3
+ import { getWorkspaceBackend } from "./backends/getWorkspaceBackend";
4
+ import { normalizeQueryFilename } from "./normalizeQueryFilename";
5
+ import type { FolderEntry, WorkspaceConfig } from "./types";
6
+
7
+ import "./SaveManagedQueryModal.scss";
8
+
9
+ export interface SaveManagedQueryModalResult {
10
+ workspaceId: string;
11
+ folderPath: string;
12
+ name: string;
13
+ filename: string;
14
+ message?: string;
15
+ }
16
+
17
+ export default class SaveManagedQueryModal {
18
+ private yasgui: Yasgui;
19
+ private overlayEl: HTMLDivElement;
20
+ private modalEl: HTMLDivElement;
21
+
22
+ private formEl: HTMLFormElement;
23
+ private workspaceSelectEl: HTMLSelectElement;
24
+ private folderPathEl: HTMLInputElement;
25
+ private folderPickerToggleEl: HTMLButtonElement;
26
+ private folderPickerEl: HTMLDivElement;
27
+ private folderPickerPathEl: HTMLDivElement;
28
+ private folderPickerListEl: HTMLDivElement;
29
+ private folderPickerErrorEl: HTMLDivElement;
30
+ private newFolderNameEl: HTMLInputElement;
31
+
32
+ private nameEl: HTMLInputElement;
33
+ private filenameEl: HTMLInputElement;
34
+ private messageEl: HTMLInputElement;
35
+
36
+ private nameRowEl: HTMLDivElement;
37
+ private filenameRowEl: HTMLDivElement;
38
+ private messageRowEl: HTMLDivElement;
39
+ private messageLabelEl: HTMLLabelElement;
40
+
41
+ private saveBtn!: HTMLButtonElement;
42
+ private cancelBtn!: HTMLButtonElement;
43
+ private saveBtnOriginalText = "Save";
44
+
45
+ private filenameTouched = false;
46
+ private folderPickerOpen = false;
47
+ private folderBrowsePath = "";
48
+
49
+ private resolve?: (value: SaveManagedQueryModalResult) => void;
50
+ private reject?: (reason?: unknown) => void;
51
+
52
+ constructor(yasgui: Yasgui) {
53
+ this.yasgui = yasgui;
54
+
55
+ this.overlayEl = document.createElement("div");
56
+ addClass(this.overlayEl, "saveManagedQueryModalOverlay");
57
+
58
+ this.modalEl = document.createElement("div");
59
+ addClass(this.modalEl, "saveManagedQueryModal");
60
+ this.modalEl.addEventListener("click", (e) => e.stopPropagation());
61
+
62
+ const headerEl = document.createElement("div");
63
+ addClass(headerEl, "saveManagedQueryModalHeader");
64
+
65
+ const titleEl = document.createElement("h2");
66
+ titleEl.textContent = "Save as managed query";
67
+
68
+ const closeBtn = document.createElement("button");
69
+ closeBtn.type = "button";
70
+ closeBtn.className = "closeButton";
71
+ closeBtn.setAttribute("aria-label", "Close");
72
+ closeBtn.innerHTML = "×";
73
+ closeBtn.addEventListener("click", () => this.cancel());
74
+
75
+ headerEl.appendChild(titleEl);
76
+ headerEl.appendChild(closeBtn);
77
+
78
+ this.formEl = document.createElement("form");
79
+ this.formEl.addEventListener("submit", (e) => {
80
+ e.preventDefault();
81
+ this.submit();
82
+ });
83
+
84
+ const bodyEl = document.createElement("div");
85
+ addClass(bodyEl, "saveManagedQueryModalBody");
86
+
87
+ this.workspaceSelectEl = document.createElement("select");
88
+ this.workspaceSelectEl.setAttribute("aria-label", "Workspace");
89
+ this.workspaceSelectEl.addEventListener("change", () => {
90
+ this.applyWorkspaceTypeUI();
91
+ if (!this.folderPickerOpen) return;
92
+ this.folderBrowsePath = "";
93
+ this.folderPathEl.value = "";
94
+ void this.refreshFolderPicker();
95
+ });
96
+
97
+ this.folderPathEl = document.createElement("input");
98
+ this.folderPathEl.type = "text";
99
+ this.folderPathEl.placeholder = "Select a folder (optional)";
100
+ this.folderPathEl.setAttribute("aria-label", "Folder path");
101
+ this.folderPathEl.readOnly = true;
102
+
103
+ this.folderPickerToggleEl = document.createElement("button");
104
+ this.folderPickerToggleEl.type = "button";
105
+ this.folderPickerToggleEl.textContent = "Choose…";
106
+ addClass(this.folderPickerToggleEl, "folderPickerToggle");
107
+ this.folderPickerToggleEl.addEventListener("click", () => {
108
+ this.folderPickerOpen = !this.folderPickerOpen;
109
+ if (this.folderPickerOpen) {
110
+ addClass(this.folderPickerEl, "open");
111
+ this.folderBrowsePath = this.folderPathEl.value.trim();
112
+ void this.refreshFolderPicker();
113
+ } else {
114
+ removeClass(this.folderPickerEl, "open");
115
+ }
116
+ });
117
+
118
+ this.folderPickerEl = document.createElement("div");
119
+ addClass(this.folderPickerEl, "folderPicker");
120
+ this.folderPickerPathEl = document.createElement("div");
121
+ addClass(this.folderPickerPathEl, "folderPickerPath");
122
+ this.folderPickerErrorEl = document.createElement("div");
123
+ addClass(this.folderPickerErrorEl, "folderPickerError");
124
+ this.folderPickerListEl = document.createElement("div");
125
+ addClass(this.folderPickerListEl, "folderPickerList");
126
+
127
+ const folderPickerActionsEl = document.createElement("div");
128
+ addClass(folderPickerActionsEl, "folderPickerActions");
129
+
130
+ const upBtn = document.createElement("button");
131
+ upBtn.type = "button";
132
+ upBtn.textContent = "Up";
133
+ upBtn.addEventListener("click", () => {
134
+ const parts = this.folderBrowsePath.split("/").filter(Boolean);
135
+ parts.pop();
136
+ this.folderBrowsePath = parts.join("/");
137
+ this.folderPathEl.value = this.folderBrowsePath;
138
+ void this.refreshFolderPicker();
139
+ });
140
+
141
+ const rootBtn = document.createElement("button");
142
+ rootBtn.type = "button";
143
+ rootBtn.textContent = "Root";
144
+ rootBtn.addEventListener("click", () => {
145
+ this.folderBrowsePath = "";
146
+ this.folderPathEl.value = "";
147
+ void this.refreshFolderPicker();
148
+ });
149
+
150
+ folderPickerActionsEl.appendChild(rootBtn);
151
+ folderPickerActionsEl.appendChild(upBtn);
152
+
153
+ const newFolderRowEl = document.createElement("div");
154
+ addClass(newFolderRowEl, "newFolderRow");
155
+ this.newFolderNameEl = document.createElement("input");
156
+ this.newFolderNameEl.type = "text";
157
+ this.newFolderNameEl.placeholder = "New folder name";
158
+ this.newFolderNameEl.setAttribute("aria-label", "New folder name");
159
+ this.newFolderNameEl.addEventListener("keydown", (e) => {
160
+ if (e.key === "Enter") {
161
+ e.preventDefault();
162
+ e.stopPropagation();
163
+ this.createNewFolderSelection();
164
+ }
165
+ });
166
+
167
+ const createFolderBtn = document.createElement("button");
168
+ createFolderBtn.type = "button";
169
+ createFolderBtn.textContent = "Create";
170
+ createFolderBtn.addEventListener("click", () => this.createNewFolderSelection());
171
+
172
+ newFolderRowEl.appendChild(this.newFolderNameEl);
173
+ newFolderRowEl.appendChild(createFolderBtn);
174
+
175
+ this.folderPickerEl.appendChild(this.folderPickerPathEl);
176
+ this.folderPickerEl.appendChild(folderPickerActionsEl);
177
+ this.folderPickerEl.appendChild(this.folderPickerErrorEl);
178
+ this.folderPickerEl.appendChild(this.folderPickerListEl);
179
+ this.folderPickerEl.appendChild(newFolderRowEl);
180
+
181
+ this.nameEl = document.createElement("input");
182
+ this.nameEl.type = "text";
183
+ this.nameEl.placeholder = "Name";
184
+ this.nameEl.setAttribute("aria-label", "Name");
185
+ this.nameEl.addEventListener("input", () => {
186
+ if (this.filenameTouched) return;
187
+ const suggested = this.suggestFilenameFromName(this.nameEl.value);
188
+ if (suggested) this.filenameEl.value = suggested.replace(/\.sparql$/i, "");
189
+ });
190
+
191
+ this.filenameEl = document.createElement("input");
192
+ this.filenameEl.type = "text";
193
+ this.filenameEl.placeholder = "Filename (e.g., my-query)";
194
+ this.filenameEl.setAttribute("aria-label", "Filename");
195
+ this.filenameEl.addEventListener("input", () => {
196
+ this.filenameTouched = true;
197
+ });
198
+
199
+ this.messageEl = document.createElement("input");
200
+ this.messageEl.type = "text";
201
+ this.messageEl.placeholder = "Save message (optional)";
202
+ this.messageEl.setAttribute("aria-label", "Save message");
203
+
204
+ const workspaceRow = this.row("Workspace", this.workspaceSelectEl);
205
+ const folderRow = this.folderRow();
206
+ this.nameRowEl = this.row("Name", this.nameEl);
207
+ this.filenameRowEl = this.row("Filename", this.filenameEl);
208
+
209
+ // Build the message row with a mutable label (Message vs Description)
210
+ this.messageRowEl = document.createElement("div");
211
+ addClass(this.messageRowEl, "saveManagedQueryModalRow");
212
+ this.messageLabelEl = document.createElement("label");
213
+ this.messageLabelEl.textContent = "Message";
214
+ this.messageRowEl.appendChild(this.messageLabelEl);
215
+ this.messageRowEl.appendChild(this.messageEl);
216
+
217
+ bodyEl.appendChild(workspaceRow);
218
+ bodyEl.appendChild(folderRow);
219
+ bodyEl.appendChild(this.folderPickerEl);
220
+ bodyEl.appendChild(this.nameRowEl);
221
+ bodyEl.appendChild(this.filenameRowEl);
222
+ bodyEl.appendChild(this.messageRowEl);
223
+
224
+ const footerEl = document.createElement("div");
225
+ addClass(footerEl, "saveManagedQueryModalFooter");
226
+
227
+ this.cancelBtn = document.createElement("button");
228
+ this.cancelBtn.type = "button";
229
+ this.cancelBtn.textContent = "Cancel";
230
+ this.cancelBtn.addEventListener("click", () => this.cancel());
231
+
232
+ this.saveBtn = document.createElement("button");
233
+ this.saveBtn.type = "submit";
234
+ this.saveBtn.textContent = "Save";
235
+ this.saveBtnOriginalText = "Save";
236
+ addClass(this.saveBtn, "primary");
237
+ // handled by form submit
238
+
239
+ footerEl.appendChild(this.cancelBtn);
240
+ footerEl.appendChild(this.saveBtn);
241
+
242
+ this.formEl.appendChild(bodyEl);
243
+ this.formEl.appendChild(footerEl);
244
+
245
+ this.modalEl.appendChild(headerEl);
246
+ this.modalEl.appendChild(this.formEl);
247
+
248
+ this.overlayEl.appendChild(this.modalEl);
249
+
250
+ this.overlayEl.addEventListener("click", () => this.cancel());
251
+ document.addEventListener("keydown", (e) => {
252
+ if (!this.isOpen()) return;
253
+ if (e.key === "Escape") {
254
+ e.preventDefault();
255
+ this.cancel();
256
+ }
257
+ });
258
+
259
+ this.close();
260
+ }
261
+
262
+ private applyWorkspaceTypeUI() {
263
+ const workspace = this.getSelectedWorkspace();
264
+ const isGit = workspace?.type === "git";
265
+ const isSparql = workspace?.type === "sparql";
266
+
267
+ // SPARQL: no filename field; Message becomes Description.
268
+ // Git: no name and no message/description.
269
+ this.nameRowEl.style.display = isGit ? "none" : "";
270
+ this.filenameRowEl.style.display = isSparql ? "none" : "";
271
+ this.messageRowEl.style.display = isGit ? "none" : "";
272
+
273
+ if (isSparql) {
274
+ this.messageLabelEl.textContent = "Description";
275
+ this.messageEl.placeholder = "Description (optional)";
276
+ this.messageEl.setAttribute("aria-label", "Description");
277
+ } else {
278
+ this.messageLabelEl.textContent = "Message";
279
+ this.messageEl.placeholder = "Save message (optional)";
280
+ this.messageEl.setAttribute("aria-label", "Save message");
281
+ }
282
+ }
283
+
284
+ private folderRow(): HTMLDivElement {
285
+ const rowEl = document.createElement("div");
286
+ addClass(rowEl, "saveManagedQueryModalRow");
287
+
288
+ const labelEl = document.createElement("label");
289
+ labelEl.textContent = "Folder";
290
+
291
+ const containerEl = document.createElement("div");
292
+ addClass(containerEl, "folderPathContainer");
293
+ containerEl.appendChild(this.folderPathEl);
294
+ containerEl.appendChild(this.folderPickerToggleEl);
295
+
296
+ rowEl.appendChild(labelEl);
297
+ rowEl.appendChild(containerEl);
298
+ return rowEl;
299
+ }
300
+
301
+ private row(label: string, inputEl: HTMLElement): HTMLDivElement {
302
+ const rowEl = document.createElement("div");
303
+ addClass(rowEl, "saveManagedQueryModalRow");
304
+
305
+ const labelEl = document.createElement("label");
306
+ labelEl.textContent = label;
307
+
308
+ rowEl.appendChild(labelEl);
309
+ rowEl.appendChild(inputEl);
310
+
311
+ return rowEl;
312
+ }
313
+
314
+ private isOpen(): boolean {
315
+ return this.overlayEl.classList.contains("open");
316
+ }
317
+
318
+ private open() {
319
+ addClass(this.overlayEl, "open");
320
+ }
321
+
322
+ private close() {
323
+ removeClass(this.overlayEl, "open");
324
+ }
325
+
326
+ private cancel() {
327
+ this.close();
328
+ this.overlayEl.remove();
329
+ this.reject?.(new Error("cancelled"));
330
+ this.resolve = undefined;
331
+ this.reject = undefined;
332
+ }
333
+
334
+ private setLoading(isLoading: boolean) {
335
+ if (isLoading) {
336
+ this.saveBtn.disabled = true;
337
+ this.cancelBtn.disabled = true;
338
+ this.saveBtn.textContent = "Saving…";
339
+ addClass(this.saveBtn, "saving");
340
+ } else {
341
+ this.saveBtn.disabled = false;
342
+ this.cancelBtn.disabled = false;
343
+ this.saveBtn.textContent = this.saveBtnOriginalText;
344
+ removeClass(this.saveBtn, "saving");
345
+ }
346
+ }
347
+
348
+ public notifySaveInProgress() {
349
+ this.setLoading(true);
350
+ }
351
+
352
+ public notifySaveComplete() {
353
+ this.setLoading(false);
354
+ }
355
+
356
+ private submit() {
357
+ const workspaceId = this.workspaceSelectEl.value;
358
+ const name = this.nameEl.value.trim();
359
+ const filename = this.filenameEl.value.trim();
360
+ const folderPath = this.folderPathEl.value.trim();
361
+ const message = this.messageEl.value.trim();
362
+
363
+ const workspace = this.getSelectedWorkspace();
364
+ const isGit = workspace?.type === "git";
365
+ const isSparql = workspace?.type === "sparql";
366
+
367
+ if (!workspaceId) {
368
+ window.alert("Please select a workspace");
369
+ return;
370
+ }
371
+
372
+ let resolvedName = name;
373
+ let resolvedFilename = filename;
374
+
375
+ if (isGit) {
376
+ resolvedFilename = resolvedFilename.replace(/\.sparql$/i, "");
377
+ if (!resolvedFilename) {
378
+ window.alert("Please enter a filename");
379
+ return;
380
+ }
381
+ // Derive a tab label for convenience.
382
+ const base = resolvedFilename.replace(/^.*\//, "");
383
+ resolvedName = base || resolvedFilename;
384
+ } else if (isSparql) {
385
+ if (!resolvedName) {
386
+ window.alert("Please enter a name");
387
+ return;
388
+ }
389
+ resolvedFilename = this.suggestFilenameFromName(resolvedName) || normalizeQueryFilename(resolvedName);
390
+ } else {
391
+ // Fallback (should not happen): require both.
392
+ if (!resolvedFilename) {
393
+ window.alert("Please enter a filename");
394
+ return;
395
+ }
396
+ if (!resolvedName) {
397
+ window.alert("Please enter a name");
398
+ return;
399
+ }
400
+ }
401
+
402
+ this.close();
403
+ this.overlayEl.remove();
404
+
405
+ this.resolve?.({
406
+ workspaceId,
407
+ folderPath,
408
+ name: resolvedName,
409
+ filename: resolvedFilename,
410
+ message: isGit ? undefined : message || undefined,
411
+ });
412
+
413
+ this.resolve = undefined;
414
+ this.reject = undefined;
415
+ }
416
+
417
+ public async show(defaults?: Partial<SaveManagedQueryModalResult>): Promise<SaveManagedQueryModalResult> {
418
+ const workspaces = this.yasgui.persistentConfig.getWorkspaces();
419
+ const activeWorkspaceId = this.yasgui.persistentConfig.getActiveWorkspaceId();
420
+ const selectedWorkspaceId = defaults?.workspaceId || activeWorkspaceId || (workspaces[0]?.id ?? "");
421
+
422
+ const orderedWorkspaces = selectedWorkspaceId
423
+ ? [
424
+ ...workspaces.filter((w) => w.id === selectedWorkspaceId),
425
+ ...workspaces.filter((w) => w.id !== selectedWorkspaceId),
426
+ ]
427
+ : workspaces;
428
+
429
+ this.workspaceSelectEl.innerHTML = "";
430
+ for (const w of orderedWorkspaces) {
431
+ const opt = document.createElement("option");
432
+ opt.value = w.id;
433
+ opt.textContent = w.label;
434
+ this.workspaceSelectEl.appendChild(opt);
435
+ }
436
+
437
+ this.workspaceSelectEl.value = selectedWorkspaceId;
438
+
439
+ this.applyWorkspaceTypeUI();
440
+
441
+ this.folderPathEl.value = defaults?.folderPath ?? "";
442
+ this.filenameTouched = false;
443
+
444
+ const defaultName = defaults?.name ?? "";
445
+ this.nameEl.value = defaultName;
446
+
447
+ const defaultFilename =
448
+ defaults?.filename ?? (defaultName ? (this.suggestFilenameFromName(defaultName) ?? "") : "");
449
+ this.filenameEl.value = defaultFilename.replace(/\.sparql$/i, "");
450
+ this.messageEl.value = defaults?.message ?? "";
451
+
452
+ this.folderPickerOpen = false;
453
+ removeClass(this.folderPickerEl, "open");
454
+ this.folderBrowsePath = this.folderPathEl.value.trim();
455
+ this.folderPickerErrorEl.textContent = "";
456
+ this.folderPickerListEl.innerHTML = "";
457
+
458
+ document.body.appendChild(this.overlayEl);
459
+ this.open();
460
+
461
+ const selected = this.getSelectedWorkspace();
462
+ if (selected?.type === "git") {
463
+ this.filenameEl.focus();
464
+ } else {
465
+ this.nameEl.focus();
466
+ }
467
+
468
+ return await new Promise<SaveManagedQueryModalResult>((resolve, reject) => {
469
+ this.resolve = resolve;
470
+ this.reject = reject;
471
+ });
472
+ }
473
+
474
+ private suggestFilenameFromName(name: string): string | undefined {
475
+ const trimmed = name.trim();
476
+ if (!trimmed) return undefined;
477
+ const safe = trimmed.replace(/[\\/]/g, "-");
478
+ try {
479
+ return normalizeQueryFilename(safe);
480
+ } catch {
481
+ return undefined;
482
+ }
483
+ }
484
+
485
+ private getSelectedWorkspace(): WorkspaceConfig | undefined {
486
+ const workspaceId = this.workspaceSelectEl.value;
487
+ return this.yasgui.persistentConfig.getWorkspace(workspaceId);
488
+ }
489
+
490
+ private async refreshFolderPicker(): Promise<void> {
491
+ this.folderPickerErrorEl.textContent = "";
492
+ this.folderPickerListEl.innerHTML = "";
493
+ this.folderPickerPathEl.textContent = this.folderBrowsePath
494
+ ? `Current: ${this.folderBrowsePath}`
495
+ : "Current: (root)";
496
+
497
+ const workspace = this.getSelectedWorkspace();
498
+ if (!workspace) {
499
+ this.folderPickerErrorEl.textContent = "Workspace not found";
500
+ return;
501
+ }
502
+
503
+ const backend = getWorkspaceBackend(workspace, { persistentConfig: this.yasgui.persistentConfig });
504
+
505
+ let entries: FolderEntry[] = [];
506
+ try {
507
+ entries = await backend.listFolder(this.folderBrowsePath || undefined);
508
+ } catch (e) {
509
+ // For newly-staged folders that don't exist yet, treat as empty.
510
+ const err = e as any;
511
+ if (err?.code && err.code !== "NOT_FOUND") {
512
+ this.folderPickerErrorEl.textContent = err.message || String(e);
513
+ return;
514
+ }
515
+ entries = [];
516
+ }
517
+
518
+ const folders = entries
519
+ .filter((x) => x.kind === "folder")
520
+ .sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
521
+
522
+ if (folders.length === 0) {
523
+ const emptyEl = document.createElement("div");
524
+ addClass(emptyEl, "folderPickerEmpty");
525
+ emptyEl.textContent = "No subfolders";
526
+ this.folderPickerListEl.appendChild(emptyEl);
527
+ return;
528
+ }
529
+
530
+ for (const f of folders) {
531
+ const btn = document.createElement("button");
532
+ btn.type = "button";
533
+ addClass(btn, "folderPickerItem");
534
+ btn.textContent = f.label;
535
+ btn.addEventListener("click", () => {
536
+ this.folderBrowsePath = f.id;
537
+ this.folderPathEl.value = f.id;
538
+ void this.refreshFolderPicker();
539
+ });
540
+ this.folderPickerListEl.appendChild(btn);
541
+ }
542
+ }
543
+
544
+ private createNewFolderSelection() {
545
+ const name = this.newFolderNameEl.value.trim();
546
+ if (!name) return;
547
+
548
+ const safe = name.replace(/[\\/]/g, "-");
549
+ this.folderBrowsePath = this.folderBrowsePath ? `${this.folderBrowsePath}/${safe}` : safe;
550
+ this.folderPathEl.value = this.folderBrowsePath;
551
+ this.newFolderNameEl.value = "";
552
+ void this.refreshFolderPicker();
553
+ }
554
+ }