@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
package/package.json
CHANGED
package/src/PersistentConfig.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Storage as YStorage } from "@matdata/yasgui-utils";
|
|
2
2
|
import Yasgui, { EndpointButton, EndpointConfig } from "./";
|
|
3
3
|
import * as Tab from "./Tab";
|
|
4
|
+
import type { WorkspaceConfig } from "./queryManagement/types";
|
|
4
5
|
export var storageNamespace = "triply";
|
|
5
6
|
export interface PersistedJson {
|
|
6
7
|
endpointHistory: string[];
|
|
@@ -12,6 +13,8 @@ export interface PersistedJson {
|
|
|
12
13
|
autoCaptureEnabled?: boolean;
|
|
13
14
|
customEndpointButtons?: EndpointButton[]; // Legacy, kept for backwards compatibility
|
|
14
15
|
endpointConfigs?: EndpointConfig[]; // New endpoint-based storage with auth
|
|
16
|
+
workspaces?: WorkspaceConfig[];
|
|
17
|
+
activeWorkspaceId?: string;
|
|
15
18
|
theme?: "light" | "dark";
|
|
16
19
|
orientation?: "vertical" | "horizontal";
|
|
17
20
|
showSnippetsBar?: boolean;
|
|
@@ -28,6 +31,8 @@ function getDefaults(): PersistedJson {
|
|
|
28
31
|
autoCaptureEnabled: true,
|
|
29
32
|
customEndpointButtons: [],
|
|
30
33
|
endpointConfigs: [],
|
|
34
|
+
workspaces: [],
|
|
35
|
+
activeWorkspaceId: undefined,
|
|
31
36
|
};
|
|
32
37
|
}
|
|
33
38
|
|
|
@@ -216,6 +221,62 @@ export default class PersistentConfig {
|
|
|
216
221
|
const filtered = configs.filter((c) => c.endpoint !== endpoint);
|
|
217
222
|
this.setEndpointConfigs(filtered);
|
|
218
223
|
}
|
|
224
|
+
|
|
225
|
+
public getWorkspaces(): WorkspaceConfig[] {
|
|
226
|
+
return this.persistedJson.workspaces || [];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
public getWorkspace(workspaceId: string): WorkspaceConfig | undefined {
|
|
230
|
+
return this.getWorkspaces().find((w) => w.id === workspaceId);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
public setWorkspaces(workspaces: WorkspaceConfig[]) {
|
|
234
|
+
this.persistedJson.workspaces = workspaces;
|
|
235
|
+
this.toStorage();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
public addOrUpdateWorkspace(workspace: WorkspaceConfig) {
|
|
239
|
+
const workspaces = this.getWorkspaces();
|
|
240
|
+
const existingIndex = workspaces.findIndex((w) => w.id === workspace.id);
|
|
241
|
+
const now = new Date().toISOString();
|
|
242
|
+
|
|
243
|
+
if (existingIndex >= 0) {
|
|
244
|
+
const existing = workspaces[existingIndex];
|
|
245
|
+
workspaces[existingIndex] = {
|
|
246
|
+
...existing,
|
|
247
|
+
...workspace,
|
|
248
|
+
createdAt: existing.createdAt || workspace.createdAt || now,
|
|
249
|
+
updatedAt: now,
|
|
250
|
+
};
|
|
251
|
+
} else {
|
|
252
|
+
workspaces.push({
|
|
253
|
+
...workspace,
|
|
254
|
+
createdAt: workspace.createdAt || now,
|
|
255
|
+
updatedAt: now,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
this.setWorkspaces(workspaces);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
public deleteWorkspace(workspaceId: string) {
|
|
263
|
+
const workspaces = this.getWorkspaces();
|
|
264
|
+
const filtered = workspaces.filter((w) => w.id !== workspaceId);
|
|
265
|
+
this.setWorkspaces(filtered);
|
|
266
|
+
|
|
267
|
+
if (this.getActiveWorkspaceId() === workspaceId) {
|
|
268
|
+
this.setActiveWorkspaceId(undefined);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
public getActiveWorkspaceId(): string | undefined {
|
|
273
|
+
return this.persistedJson.activeWorkspaceId;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
public setActiveWorkspaceId(workspaceId: string | undefined) {
|
|
277
|
+
this.persistedJson.activeWorkspaceId = workspaceId;
|
|
278
|
+
this.toStorage();
|
|
279
|
+
}
|
|
219
280
|
public static clear() {
|
|
220
281
|
const storage = new YStorage(storageNamespace);
|
|
221
282
|
storage.removeNamespace();
|
package/src/Tab.ts
CHANGED
|
@@ -10,24 +10,13 @@ import EndpointSelect from "./endpointSelect";
|
|
|
10
10
|
import "./tab.scss";
|
|
11
11
|
import { getRandomId, default as Yasgui, YasguiRequestConfig } from "./";
|
|
12
12
|
import * as OAuth2Utils from "./OAuth2Utils";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const VERTICAL_LAYOUT_ICON = `<svg viewBox="0 0 24 24">
|
|
21
|
-
<rect x="2" y="2" width="20" height="8" stroke="currentColor" stroke-width="2" fill="none"/>
|
|
22
|
-
<rect x="2" y="12" width="20" height="10" stroke="currentColor" stroke-width="2" fill="none"/>
|
|
23
|
-
</svg>`;
|
|
24
|
-
|
|
25
|
-
// Overflow dropdown icon (three horizontal dots / ellipsis menu)
|
|
26
|
-
const OVERFLOW_ICON = `<svg viewBox="0 0 24 24" fill="currentColor">
|
|
27
|
-
<circle cx="5" cy="12" r="2"/>
|
|
28
|
-
<circle cx="12" cy="12" r="2"/>
|
|
29
|
-
<circle cx="19" cy="12" r="2"/>
|
|
30
|
-
</svg>`;
|
|
13
|
+
import type { ManagedTabMetadata } from "./queryManagement/types";
|
|
14
|
+
import { hashQueryText } from "./queryManagement/textHash";
|
|
15
|
+
import SaveManagedQueryModal from "./queryManagement/SaveManagedQueryModal";
|
|
16
|
+
import { saveManagedQuery } from "./queryManagement/saveManagedQuery";
|
|
17
|
+
import { getWorkspaceBackend } from "./queryManagement/backends/getWorkspaceBackend";
|
|
18
|
+
import { asWorkspaceBackendError } from "./queryManagement/backends/errors";
|
|
19
|
+
import { normalizeQueryFilename } from "./queryManagement/normalizeQueryFilename";
|
|
31
20
|
|
|
32
21
|
export interface PersistedJsonYasr extends YasrPersistentConfig {
|
|
33
22
|
responseSummary: Parser.ResponseSummary;
|
|
@@ -46,6 +35,7 @@ export interface PersistedJson {
|
|
|
46
35
|
};
|
|
47
36
|
requestConfig: YasguiRequestConfig;
|
|
48
37
|
orientation?: "vertical" | "horizontal";
|
|
38
|
+
managedQuery?: ManagedTabMetadata;
|
|
49
39
|
}
|
|
50
40
|
|
|
51
41
|
export interface Tab {
|
|
@@ -112,6 +102,228 @@ export class Tab extends EventEmitter {
|
|
|
112
102
|
return this.persistentJson.id;
|
|
113
103
|
}
|
|
114
104
|
|
|
105
|
+
public getManagedQueryMetadata(): ManagedTabMetadata | undefined {
|
|
106
|
+
return this.persistentJson.managedQuery;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public setManagedQueryMetadata(metadata: ManagedTabMetadata | undefined) {
|
|
110
|
+
if (metadata) {
|
|
111
|
+
this.persistentJson.managedQuery = metadata;
|
|
112
|
+
} else {
|
|
113
|
+
delete this.persistentJson.managedQuery;
|
|
114
|
+
}
|
|
115
|
+
this.emit("change", this, this.persistentJson);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public isManagedQueryTab(): boolean {
|
|
119
|
+
return !!this.persistentJson.managedQuery;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public hasUnsavedManagedChanges(): boolean {
|
|
123
|
+
const meta = this.getManagedQueryMetadata();
|
|
124
|
+
if (!meta) return false;
|
|
125
|
+
if (!meta.lastSavedTextHash) return false;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const current = this.yasqe ? this.yasqe.getValue() : this.persistentJson.yasqe.value;
|
|
129
|
+
const currentHash = hashQueryText(current);
|
|
130
|
+
return currentHash !== meta.lastSavedTextHash;
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private getDefaultSaveModalValues(): { workspaceId?: string; folderPath?: string; filename?: string; name?: string } {
|
|
137
|
+
const meta = this.getManagedQueryMetadata();
|
|
138
|
+
if (!meta) return { name: this.name() };
|
|
139
|
+
if (meta.backendType !== "git") return { workspaceId: meta.workspaceId };
|
|
140
|
+
const path = (meta.queryRef as any)?.path as string | undefined;
|
|
141
|
+
if (!path) return { workspaceId: meta.workspaceId };
|
|
142
|
+
|
|
143
|
+
const parts = path.split("/");
|
|
144
|
+
const filename = parts.pop();
|
|
145
|
+
const folderPath = parts.join("/");
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
workspaceId: meta.workspaceId,
|
|
149
|
+
folderPath,
|
|
150
|
+
filename,
|
|
151
|
+
name: this.name(),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private getManagedQueryIdFromMetadata(meta: ManagedTabMetadata): string | undefined {
|
|
156
|
+
if (meta.backendType === "git") return (meta.queryRef as any)?.path as string | undefined;
|
|
157
|
+
return (meta.queryRef as any)?.managedQueryIri as string | undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private versionRefFromVersionTag(backendType: "git" | "sparql", versionTag: string | undefined) {
|
|
161
|
+
if (!versionTag) return undefined;
|
|
162
|
+
if (backendType === "git") return { commitSha: versionTag };
|
|
163
|
+
return { managedQueryVersionIri: versionTag };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private getQueryTextForSave(): string {
|
|
167
|
+
// Saving can be triggered while the tab exists but the editor isn't initialized yet.
|
|
168
|
+
// In that case fall back to the persisted tab value.
|
|
169
|
+
try {
|
|
170
|
+
if (this.yasqe) return this.yasqe.getValue();
|
|
171
|
+
} catch {
|
|
172
|
+
// ignore
|
|
173
|
+
}
|
|
174
|
+
return this.persistentJson.yasqe.value;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public async saveManagedQueryOrSaveAsManagedQuery(): Promise<void> {
|
|
178
|
+
const meta = this.getManagedQueryMetadata();
|
|
179
|
+
if (!meta) {
|
|
180
|
+
await this.saveAsManagedQuery();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const queryId = this.getManagedQueryIdFromMetadata(meta);
|
|
185
|
+
if (!queryId) {
|
|
186
|
+
await this.saveAsManagedQuery();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const workspace = this.yasgui.persistentConfig.getWorkspace(meta.workspaceId);
|
|
191
|
+
if (!workspace) {
|
|
192
|
+
window.alert("Selected workspace no longer exists");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const backend = getWorkspaceBackend(workspace, { persistentConfig: this.yasgui.persistentConfig });
|
|
197
|
+
|
|
198
|
+
const expectedVersionTag = (() => {
|
|
199
|
+
if (!meta?.lastSavedVersionRef) return undefined;
|
|
200
|
+
if (meta.backendType === "git") return (meta.lastSavedVersionRef as any)?.commitSha;
|
|
201
|
+
return (meta.lastSavedVersionRef as any)?.managedQueryVersionIri;
|
|
202
|
+
})();
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
await backend.writeQuery(queryId, this.getQueryTextForSave(), { expectedVersionTag });
|
|
206
|
+
} catch (e) {
|
|
207
|
+
const err = asWorkspaceBackendError(e);
|
|
208
|
+
if (err.code === "CONFLICT") {
|
|
209
|
+
if (meta.backendType === "git") {
|
|
210
|
+
// Best-effort self-heal: some providers use a file sha for optimistic concurrency.
|
|
211
|
+
// If our stored version tag is stale/incorrect but the remote content is unchanged,
|
|
212
|
+
// refresh the tag and retry once.
|
|
213
|
+
try {
|
|
214
|
+
const latest = await backend.readQuery(queryId);
|
|
215
|
+
const latestHash = hashQueryText(latest.queryText);
|
|
216
|
+
if (meta.lastSavedTextHash && meta.lastSavedTextHash === latestHash && latest.versionTag) {
|
|
217
|
+
await backend.writeQuery(queryId, this.getQueryTextForSave(), { expectedVersionTag: latest.versionTag });
|
|
218
|
+
} else {
|
|
219
|
+
window.alert(
|
|
220
|
+
"Save conflict. Resolve the conflict externally (e.g., pull/rebase/merge) and then try saving again.",
|
|
221
|
+
);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
window.alert(
|
|
226
|
+
"Save conflict. Resolve the conflict externally (e.g., pull/rebase/merge) and then try saving again.",
|
|
227
|
+
);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
window.alert("Save conflict. Refresh the query and try again.");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
window.alert(err.message);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const read = await backend.readQuery(queryId);
|
|
240
|
+
const lastSavedTextHash = hashQueryText(read.queryText);
|
|
241
|
+
const lastSavedVersionRef = this.versionRefFromVersionTag(meta.backendType, read.versionTag);
|
|
242
|
+
this.setManagedQueryMetadata({
|
|
243
|
+
...meta,
|
|
244
|
+
lastSavedTextHash,
|
|
245
|
+
lastSavedVersionRef,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Ensure Query Browser reflects the updated version/metadata.
|
|
249
|
+
this.yasgui.queryBrowser.invalidateAndRefresh(meta.workspaceId);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
public async saveAsManagedQuery(): Promise<void> {
|
|
253
|
+
const modal = new SaveManagedQueryModal(this.yasgui);
|
|
254
|
+
|
|
255
|
+
const defaults = this.getDefaultSaveModalValues();
|
|
256
|
+
let result:
|
|
257
|
+
| {
|
|
258
|
+
workspaceId: string;
|
|
259
|
+
folderPath: string;
|
|
260
|
+
name: string;
|
|
261
|
+
filename: string;
|
|
262
|
+
message?: string;
|
|
263
|
+
}
|
|
264
|
+
| undefined;
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const modalDefaults: any = {
|
|
268
|
+
workspaceId: defaults.workspaceId,
|
|
269
|
+
folderPath: defaults.folderPath || "",
|
|
270
|
+
name: defaults.name || this.name(),
|
|
271
|
+
};
|
|
272
|
+
// Only provide a filename default when we have one.
|
|
273
|
+
// Otherwise the modal will derive a suggested filename from the provided name.
|
|
274
|
+
if (defaults.filename) modalDefaults.filename = defaults.filename;
|
|
275
|
+
|
|
276
|
+
result = await modal.show(modalDefaults);
|
|
277
|
+
} catch {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const workspace = this.yasgui.persistentConfig.getWorkspace(result.workspaceId);
|
|
282
|
+
if (!workspace) {
|
|
283
|
+
window.alert("Selected workspace no longer exists");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const backend = getWorkspaceBackend(workspace, { persistentConfig: this.yasgui.persistentConfig });
|
|
288
|
+
const meta = this.getManagedQueryMetadata();
|
|
289
|
+
|
|
290
|
+
const expectedVersionTag = (() => {
|
|
291
|
+
if (!meta?.lastSavedVersionRef) return undefined;
|
|
292
|
+
if (meta.backendType === "git") return (meta.lastSavedVersionRef as any)?.commitSha;
|
|
293
|
+
return (meta.lastSavedVersionRef as any)?.managedQueryVersionIri;
|
|
294
|
+
})();
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
modal.notifySaveInProgress();
|
|
298
|
+
const { managedMetadata } = await saveManagedQuery({
|
|
299
|
+
backend,
|
|
300
|
+
backendType: workspace.type,
|
|
301
|
+
workspaceId: workspace.id,
|
|
302
|
+
workspaceIri: workspace.type === "sparql" ? workspace.workspaceIri : undefined,
|
|
303
|
+
folderPath: result.folderPath,
|
|
304
|
+
name: result.name,
|
|
305
|
+
filename: result.filename,
|
|
306
|
+
queryText: this.getQueryTextForSave(),
|
|
307
|
+
associatedEndpoint: workspace.type === "sparql" ? this.getEndpoint() : undefined,
|
|
308
|
+
message: result.message,
|
|
309
|
+
expectedVersionTag,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
modal.notifySaveComplete();
|
|
313
|
+
this.setManagedQueryMetadata(managedMetadata);
|
|
314
|
+
if (result.name && result.name.trim()) {
|
|
315
|
+
this.setName(result.name.trim());
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Ensure the saved query shows up immediately in the Query Browser.
|
|
319
|
+
this.yasgui.queryBrowser.invalidateAndRefresh(workspace.id);
|
|
320
|
+
} catch (e) {
|
|
321
|
+
modal.notifySaveComplete();
|
|
322
|
+
const err = asWorkspaceBackendError(e);
|
|
323
|
+
window.alert(err.message);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
115
327
|
private draw() {
|
|
116
328
|
if (this.rootEl) return; //aready drawn
|
|
117
329
|
this.rootEl = document.createElement("div");
|
|
@@ -179,6 +391,19 @@ export class Tab extends EventEmitter {
|
|
|
179
391
|
}
|
|
180
392
|
|
|
181
393
|
private handleKeyDown = (event: KeyboardEvent) => {
|
|
394
|
+
if (event.defaultPrevented) return;
|
|
395
|
+
|
|
396
|
+
const saveModalOpen = !!document.querySelector(".saveManagedQueryModalOverlay.open");
|
|
397
|
+
if (!saveModalOpen) {
|
|
398
|
+
const isSaveShortcut =
|
|
399
|
+
(event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey && event.key.toLowerCase() === "s";
|
|
400
|
+
if (isSaveShortcut) {
|
|
401
|
+
event.preventDefault();
|
|
402
|
+
void this.saveManagedQueryOrSaveAsManagedQuery();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
182
407
|
// F11 - Toggle Yasqe fullscreen
|
|
183
408
|
if (event.key === "F11") {
|
|
184
409
|
event.preventDefault();
|
|
@@ -236,6 +461,26 @@ export class Tab extends EventEmitter {
|
|
|
236
461
|
}
|
|
237
462
|
|
|
238
463
|
public close() {
|
|
464
|
+
if (this.isManagedQueryTab() && this.hasUnsavedManagedChanges()) {
|
|
465
|
+
const wantsSave = window.confirm("This managed query has unsaved changes. Save before closing?");
|
|
466
|
+
if (wantsSave) {
|
|
467
|
+
void this.saveManagedQueryOrSaveAsManagedQuery().then(() => {
|
|
468
|
+
// Only close if we actually saved (metadata updated so no longer dirty)
|
|
469
|
+
if (!this.hasUnsavedManagedChanges()) {
|
|
470
|
+
this.closeNow();
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const discard = window.confirm("Discard changes and close the tab?");
|
|
477
|
+
if (!discard) return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
this.closeNow();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private closeNow() {
|
|
239
484
|
this.detachKeyboardListeners();
|
|
240
485
|
if (this.yasqe) this.yasqe.abortQuery();
|
|
241
486
|
if (this.yasgui.getTab() === this) {
|
|
@@ -306,8 +551,11 @@ export class Tab extends EventEmitter {
|
|
|
306
551
|
if (!this.orientationToggleButton) return;
|
|
307
552
|
|
|
308
553
|
// Show the icon for the layout we'll switch TO (not the current layout)
|
|
554
|
+
// fa-columns for horizontal (side-by-side), fa-grip-lines for vertical (stacked)
|
|
309
555
|
this.orientationToggleButton.innerHTML =
|
|
310
|
-
this.currentOrientation === "vertical"
|
|
556
|
+
this.currentOrientation === "vertical"
|
|
557
|
+
? '<i class="fas fa-grip-lines-vertical"></i>'
|
|
558
|
+
: '<i class="fas fa-grip-lines"></i>';
|
|
311
559
|
this.orientationToggleButton.title =
|
|
312
560
|
this.currentOrientation === "vertical" ? "Switch to horizontal layout" : "Switch to vertical layout";
|
|
313
561
|
}
|
|
@@ -483,7 +731,7 @@ export class Tab extends EventEmitter {
|
|
|
483
731
|
if (!this.endpointOverflowButton) {
|
|
484
732
|
this.endpointOverflowButton = document.createElement("button");
|
|
485
733
|
addClass(this.endpointOverflowButton, "endpointOverflowBtn");
|
|
486
|
-
this.endpointOverflowButton.innerHTML =
|
|
734
|
+
this.endpointOverflowButton.innerHTML = '<i class="fas fa-ellipsis-vertical"></i>';
|
|
487
735
|
this.endpointOverflowButton.title = "More endpoints";
|
|
488
736
|
this.endpointOverflowButton.setAttribute("aria-label", "More endpoint options");
|
|
489
737
|
this.endpointOverflowButton.setAttribute("aria-haspopup", "true");
|
|
@@ -693,6 +941,111 @@ export class Tab extends EventEmitter {
|
|
|
693
941
|
return this;
|
|
694
942
|
}
|
|
695
943
|
|
|
944
|
+
private suggestManagedFilenameFromName(name: string): string {
|
|
945
|
+
const trimmed = name.trim();
|
|
946
|
+
const safe = trimmed.replace(/[\\/]/g, "-");
|
|
947
|
+
return normalizeQueryFilename(safe);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* User-triggered tab rename.
|
|
952
|
+
* For managed queries, also renames the managed query entry in the Query Browser.
|
|
953
|
+
*/
|
|
954
|
+
public async renameTab(newName: string): Promise<void> {
|
|
955
|
+
const nextName = newName.trim();
|
|
956
|
+
if (!nextName) return;
|
|
957
|
+
if (nextName === this.name()) return;
|
|
958
|
+
|
|
959
|
+
const meta = this.getManagedQueryMetadata();
|
|
960
|
+
if (!meta) {
|
|
961
|
+
this.setName(nextName);
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const workspace = this.yasgui.persistentConfig.getWorkspace(meta.workspaceId);
|
|
966
|
+
if (!workspace) {
|
|
967
|
+
window.alert("Selected workspace no longer exists");
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const backend = getWorkspaceBackend(workspace, { persistentConfig: this.yasgui.persistentConfig });
|
|
972
|
+
|
|
973
|
+
if (meta.backendType === "sparql") {
|
|
974
|
+
const queryId = this.getManagedQueryIdFromMetadata(meta);
|
|
975
|
+
if (!queryId) return;
|
|
976
|
+
if (!backend.renameQuery) {
|
|
977
|
+
window.alert("This workspace does not support renaming queries");
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
try {
|
|
982
|
+
this.getTabListEl().setAsRenaming(true);
|
|
983
|
+
await backend.renameQuery(queryId, nextName);
|
|
984
|
+
this.setName(nextName);
|
|
985
|
+
this.yasgui.queryBrowser.invalidateAndRefresh(meta.workspaceId);
|
|
986
|
+
} catch (e) {
|
|
987
|
+
const err = asWorkspaceBackendError(e);
|
|
988
|
+
window.alert(err.message);
|
|
989
|
+
} finally {
|
|
990
|
+
this.getTabListEl().setAsRenaming(false);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Git: rename underlying file path so the Query Browser label changes.
|
|
997
|
+
const oldPath = (meta.queryRef as any)?.path as string | undefined;
|
|
998
|
+
if (!oldPath) {
|
|
999
|
+
this.setName(nextName);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const parts = oldPath.split("/").filter(Boolean);
|
|
1004
|
+
const oldFilename = parts.pop() || oldPath;
|
|
1005
|
+
const folderPrefix = parts.join("/");
|
|
1006
|
+
|
|
1007
|
+
const newFilename = this.suggestManagedFilenameFromName(nextName);
|
|
1008
|
+
const newPath = folderPrefix ? `${folderPrefix}/${newFilename}` : newFilename;
|
|
1009
|
+
|
|
1010
|
+
if (newPath === oldPath) {
|
|
1011
|
+
// Nothing to rename on the backend; still allow tab label change.
|
|
1012
|
+
this.setName(nextName);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
try {
|
|
1017
|
+
this.getTabListEl().setAsRenaming(true);
|
|
1018
|
+
await backend.writeQuery(newPath, this.getQueryTextForSave(), {
|
|
1019
|
+
message: `Rename ${oldFilename} to ${newFilename}`,
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
if (!backend.deleteQuery) {
|
|
1023
|
+
window.alert("This workspace does not support deleting queries, so the old file could not be removed.");
|
|
1024
|
+
} else {
|
|
1025
|
+
await backend.deleteQuery(oldPath);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const read = await backend.readQuery(newPath);
|
|
1029
|
+
const lastSavedTextHash = hashQueryText(read.queryText);
|
|
1030
|
+
const lastSavedVersionRef = this.versionRefFromVersionTag("git", read.versionTag);
|
|
1031
|
+
|
|
1032
|
+
this.setManagedQueryMetadata({
|
|
1033
|
+
...meta,
|
|
1034
|
+
queryRef: { ...(meta.queryRef as any), path: newPath },
|
|
1035
|
+
lastSavedTextHash,
|
|
1036
|
+
lastSavedVersionRef,
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
this.setName(nextName);
|
|
1040
|
+
this.yasgui.queryBrowser.invalidateAndRefresh(meta.workspaceId);
|
|
1041
|
+
} catch (e) {
|
|
1042
|
+
const err = asWorkspaceBackendError(e);
|
|
1043
|
+
window.alert(err.message);
|
|
1044
|
+
} finally {
|
|
1045
|
+
this.getTabListEl().setAsRenaming(false);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
696
1049
|
public hasResults() {
|
|
697
1050
|
return !!this.yasr?.results;
|
|
698
1051
|
}
|
|
@@ -917,6 +1270,20 @@ export class Tab extends EventEmitter {
|
|
|
917
1270
|
return processedReqConfig as PlainRequestConfig;
|
|
918
1271
|
},
|
|
919
1272
|
};
|
|
1273
|
+
|
|
1274
|
+
// Override editor-level save shortcut.
|
|
1275
|
+
// Yasqe binds Ctrl+S to `yasqe.saveQuery()` (local storage) by default, which can prevent our document-level handler.
|
|
1276
|
+
const existingExtraKeys = (yasqeConf as any).extraKeys;
|
|
1277
|
+
const mergedExtraKeys: Record<string, any> =
|
|
1278
|
+
existingExtraKeys && typeof existingExtraKeys === "object" ? { ...existingExtraKeys } : {};
|
|
1279
|
+
mergedExtraKeys["Ctrl-S"] = () => {
|
|
1280
|
+
const saveModalOpen = !!document.querySelector(".saveManagedQueryModalOverlay.open");
|
|
1281
|
+
if (saveModalOpen) return;
|
|
1282
|
+
void this.saveManagedQueryOrSaveAsManagedQuery();
|
|
1283
|
+
};
|
|
1284
|
+
mergedExtraKeys["Cmd-S"] = mergedExtraKeys["Ctrl-S"];
|
|
1285
|
+
(yasqeConf as any).extraKeys = mergedExtraKeys;
|
|
1286
|
+
|
|
920
1287
|
if (!yasqeConf.hintConfig) {
|
|
921
1288
|
yasqeConf.hintConfig = {};
|
|
922
1289
|
}
|
|
@@ -928,6 +1295,14 @@ export class Tab extends EventEmitter {
|
|
|
928
1295
|
}
|
|
929
1296
|
this.yasqe = new Yasqe(this.yasqeWrapperEl, yasqeConf);
|
|
930
1297
|
|
|
1298
|
+
// Hook up the save button to managed query save
|
|
1299
|
+
this.yasqe.on("saveManagedQuery", () => {
|
|
1300
|
+
void this.saveManagedQueryOrSaveAsManagedQuery();
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
// Show/hide save button based on workspace configuration
|
|
1304
|
+
this.updateSaveButtonVisibility();
|
|
1305
|
+
|
|
931
1306
|
this.yasqe.on("blur", this.handleYasqeBlur);
|
|
932
1307
|
this.yasqe.on("query", this.handleYasqeQuery);
|
|
933
1308
|
this.yasqe.on("queryBefore", this.handleYasqeQueryBefore);
|
|
@@ -943,6 +1318,42 @@ export class Tab extends EventEmitter {
|
|
|
943
1318
|
this.attachYasqeMouseHandler();
|
|
944
1319
|
}
|
|
945
1320
|
|
|
1321
|
+
private updateSaveButtonVisibility() {
|
|
1322
|
+
if (!this.yasqe) return;
|
|
1323
|
+
const workspaces = this.yasgui.persistentConfig.getWorkspaces();
|
|
1324
|
+
const hasWorkspaces = workspaces && workspaces.length > 0;
|
|
1325
|
+
this.yasqe.setSaveButtonVisible(hasWorkspaces);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
private initSaveManagedQueryIcon() {
|
|
1329
|
+
if (!this.yasqe) return;
|
|
1330
|
+
|
|
1331
|
+
const wrapper = this.yasqe.getWrapperElement();
|
|
1332
|
+
const buttons = wrapper?.querySelector(".yasqe_buttons");
|
|
1333
|
+
if (!buttons) return;
|
|
1334
|
+
|
|
1335
|
+
// Avoid duplicates if Yasqe ever re-renders
|
|
1336
|
+
if (buttons.querySelector(".yasqe_saveManagedQueryButton")) return;
|
|
1337
|
+
|
|
1338
|
+
const queryBtn = buttons.querySelector(".yasqe_queryButton");
|
|
1339
|
+
if (!queryBtn) return;
|
|
1340
|
+
|
|
1341
|
+
const saveBtn = document.createElement("button");
|
|
1342
|
+
saveBtn.type = "button";
|
|
1343
|
+
saveBtn.className = "yasqe_saveManagedQueryButton";
|
|
1344
|
+
saveBtn.title = "Save managed query";
|
|
1345
|
+
saveBtn.setAttribute("aria-label", "Save managed query");
|
|
1346
|
+
saveBtn.innerHTML = '<i class="fas fa-save" aria-hidden="true"></i>';
|
|
1347
|
+
|
|
1348
|
+
saveBtn.addEventListener("click", (e) => {
|
|
1349
|
+
e.preventDefault();
|
|
1350
|
+
e.stopPropagation();
|
|
1351
|
+
void this.saveManagedQueryOrSaveAsManagedQuery();
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
buttons.insertBefore(saveBtn, queryBtn);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
946
1357
|
private destroyYasqe() {
|
|
947
1358
|
// As Yasqe extends of CM instead of eventEmitter, it doesn't expose the removeAllListeners function, so we should unregister all events manually
|
|
948
1359
|
this.yasqe?.off("blur", this.handleYasqeBlur);
|
package/src/TabContextMenu.ts
CHANGED
|
@@ -15,6 +15,7 @@ export default class TabContextMenu {
|
|
|
15
15
|
private newTabEl!: HTMLElement;
|
|
16
16
|
private renameTabEl!: HTMLElement;
|
|
17
17
|
private copyTabEl!: HTMLElement;
|
|
18
|
+
private saveManagedQueryEl!: HTMLElement;
|
|
18
19
|
private closeTabEl!: HTMLElement;
|
|
19
20
|
private closeOtherTabsEl!: HTMLElement;
|
|
20
21
|
private reOpenOldTab!: HTMLElement;
|
|
@@ -49,6 +50,8 @@ export default class TabContextMenu {
|
|
|
49
50
|
|
|
50
51
|
this.copyTabEl = this.getMenuItemEl("Copy Tab");
|
|
51
52
|
|
|
53
|
+
this.saveManagedQueryEl = this.getMenuItemEl("Save as managed query");
|
|
54
|
+
|
|
52
55
|
this.closeTabEl = this.getMenuItemEl("Close Tab");
|
|
53
56
|
|
|
54
57
|
this.closeOtherTabsEl = this.getMenuItemEl("Close other tabs");
|
|
@@ -59,6 +62,7 @@ export default class TabContextMenu {
|
|
|
59
62
|
dropDownList.appendChild(this.newTabEl);
|
|
60
63
|
dropDownList.appendChild(this.renameTabEl);
|
|
61
64
|
dropDownList.appendChild(this.copyTabEl);
|
|
65
|
+
dropDownList.appendChild(this.saveManagedQueryEl);
|
|
62
66
|
// Add divider
|
|
63
67
|
dropDownList.appendChild(document.createElement("hr"));
|
|
64
68
|
dropDownList.appendChild(this.closeTabEl);
|
|
@@ -105,6 +109,12 @@ export default class TabContextMenu {
|
|
|
105
109
|
this.yasgui.addTab(true, config);
|
|
106
110
|
};
|
|
107
111
|
|
|
112
|
+
this.saveManagedQueryEl.onclick = async () => {
|
|
113
|
+
if (!tab) return;
|
|
114
|
+
await tab.saveManagedQueryOrSaveAsManagedQuery();
|
|
115
|
+
this.closeConfigMenu();
|
|
116
|
+
};
|
|
117
|
+
|
|
108
118
|
// Close tab functionality
|
|
109
119
|
this.closeTabEl.onclick = () => tab?.close();
|
|
110
120
|
|