@matdata/yasgui 5.1.0 → 5.3.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 +128 -96
- package/build/ts/src/ConfigExportImport.d.ts +16 -0
- package/build/ts/src/ConfigExportImport.js +304 -0
- package/build/ts/src/ConfigExportImport.js.map +1 -0
- package/build/ts/src/PersistentConfig.d.ts +8 -1
- package/build/ts/src/PersistentConfig.js +15 -0
- package/build/ts/src/PersistentConfig.js.map +1 -1
- package/build/ts/src/Tab.d.ts +9 -0
- package/build/ts/src/Tab.js +83 -1
- package/build/ts/src/Tab.js.map +1 -1
- package/build/ts/src/TabSettingsModal.d.ts +6 -0
- package/build/ts/src/TabSettingsModal.js +405 -1
- package/build/ts/src/TabSettingsModal.js.map +1 -1
- package/build/ts/src/defaults.js +2 -0
- package/build/ts/src/defaults.js.map +1 -1
- package/build/ts/src/index.d.ts +6 -0
- package/build/ts/src/index.js.map +1 -1
- package/build/yasgui.min.css +1 -1
- package/build/yasgui.min.css.map +3 -3
- package/build/yasgui.min.js +157 -105
- package/build/yasgui.min.js.map +4 -4
- package/package.json +1 -1
- package/src/ConfigExportImport.ts +391 -0
- package/src/PersistentConfig.ts +27 -1
- package/src/Tab.ts +123 -2
- package/src/TabSettingsModal.scss +180 -0
- package/src/TabSettingsModal.ts +499 -3
- package/src/defaults.ts +2 -0
- package/src/endpointSelect.scss +34 -0
- package/src/index.scss +2 -2
- package/src/index.ts +11 -0
- package/src/sortablejs.d.ts +36 -0
- package/src/tab.scss +50 -2
- package/src/themes.scss +23 -0
package/package.json
CHANGED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export/Import Configuration Module
|
|
3
|
+
* Handles serialization and deserialization of YASGUI configuration to/from RDF Turtle format
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { PersistedJson } from "./PersistentConfig";
|
|
7
|
+
|
|
8
|
+
// YASGUI Configuration Ontology
|
|
9
|
+
export const YASGUI_NS = "https://yasgui.matdata.eu/ontology#";
|
|
10
|
+
export const RDF_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
|
|
11
|
+
export const RDFS_NS = "http://www.w3.org/2000/01/rdf-schema#";
|
|
12
|
+
export const XSD_NS = "http://www.w3.org/2001/XMLSchema#";
|
|
13
|
+
export const DCTERMS_NS = "http://purl.org/dc/terms/";
|
|
14
|
+
export const SD_NS = "http://www.w3.org/ns/sparql-service-description#";
|
|
15
|
+
export const SP_NS = "http://spinrdf.org/sp#";
|
|
16
|
+
export const HTTP_NS = "http://www.w3.org/2011/http#";
|
|
17
|
+
export const SCHEMA_NS = "https://schema.org/";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Serialize configuration to Turtle format
|
|
21
|
+
*/
|
|
22
|
+
export function serializeToTurtle(config: PersistedJson): string {
|
|
23
|
+
const lines: string[] = [];
|
|
24
|
+
|
|
25
|
+
// Prefixes
|
|
26
|
+
lines.push(`@prefix yasgui: <${YASGUI_NS}> .`);
|
|
27
|
+
lines.push(`@prefix rdf: <${RDF_NS}> .`);
|
|
28
|
+
lines.push(`@prefix rdfs: <${RDFS_NS}> .`);
|
|
29
|
+
lines.push(`@prefix xsd: <${XSD_NS}> .`);
|
|
30
|
+
lines.push(`@prefix dcterms: <${DCTERMS_NS}> .`);
|
|
31
|
+
lines.push(`@prefix sd: <${SD_NS}> .`);
|
|
32
|
+
lines.push(`@prefix sp: <${SP_NS}> .`);
|
|
33
|
+
lines.push(`@prefix http: <${HTTP_NS}> .`);
|
|
34
|
+
lines.push(`@prefix schema: <${SCHEMA_NS}> .`);
|
|
35
|
+
lines.push("");
|
|
36
|
+
|
|
37
|
+
// Main configuration node
|
|
38
|
+
const now = new Date().toISOString();
|
|
39
|
+
lines.push(`[] a yasgui:Configuration ;`);
|
|
40
|
+
lines.push(` dcterms:created "${now}"^^xsd:dateTime ;`);
|
|
41
|
+
|
|
42
|
+
// Theme
|
|
43
|
+
if (config.theme) {
|
|
44
|
+
lines.push(` yasgui:theme "${config.theme}" ;`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Orientation
|
|
48
|
+
if (config.orientation) {
|
|
49
|
+
lines.push(` yasgui:orientation "${config.orientation}" ;`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Endpoint history
|
|
53
|
+
if (config.endpointHistory && config.endpointHistory.length > 0) {
|
|
54
|
+
lines.push(` yasgui:endpointHistory (`);
|
|
55
|
+
config.endpointHistory.forEach((endpoint) => {
|
|
56
|
+
lines.push(` "${escapeTurtleString(endpoint)}"`);
|
|
57
|
+
});
|
|
58
|
+
lines.push(` ) ;`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Active tab
|
|
62
|
+
if (config.active) {
|
|
63
|
+
lines.push(` yasgui:activeTab "${escapeTurtleString(config.active)}" ;`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Prefixes
|
|
67
|
+
if (config.prefixes) {
|
|
68
|
+
lines.push(` yasgui:prefixesValue """${escapeTurtleString(config.prefixes)}""" ;`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Auto capture enabled
|
|
72
|
+
if (config.autoCaptureEnabled !== undefined) {
|
|
73
|
+
lines.push(` yasgui:autoCaptureEnabled "${config.autoCaptureEnabled}"^^xsd:boolean ;`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Custom endpoint buttons
|
|
77
|
+
if (config.customEndpointButtons && config.customEndpointButtons.length > 0) {
|
|
78
|
+
lines.push(` yasgui:customEndpointButton [`);
|
|
79
|
+
config.customEndpointButtons.forEach((button, index) => {
|
|
80
|
+
const isLast = index === config.customEndpointButtons!.length - 1;
|
|
81
|
+
lines.push(` rdfs:label "${escapeTurtleString(button.label)}" ;`);
|
|
82
|
+
lines.push(` sd:endpoint "${escapeTurtleString(button.endpoint)}"${isLast ? "" : " ;"}`);
|
|
83
|
+
if (!isLast) {
|
|
84
|
+
lines.push(` ] , [`);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
lines.push(` ] ;`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Tabs
|
|
91
|
+
if (config.tabs && config.tabs.length > 0) {
|
|
92
|
+
lines.push(` yasgui:tab [`);
|
|
93
|
+
config.tabs.forEach((tabId, tabIndex) => {
|
|
94
|
+
const tabConfig = config.tabConfig[tabId];
|
|
95
|
+
const isLastTab = tabIndex === config.tabs.length - 1;
|
|
96
|
+
|
|
97
|
+
lines.push(` dcterms:identifier "${escapeTurtleString(tabId)}" ;`);
|
|
98
|
+
lines.push(` rdfs:label "${escapeTurtleString(tabConfig.name)}" ;`);
|
|
99
|
+
|
|
100
|
+
// Orientation (if different from default)
|
|
101
|
+
if (tabConfig.orientation) {
|
|
102
|
+
lines.push(` yasgui:orientation "${escapeTurtleString(tabConfig.orientation)}" ;`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Query
|
|
106
|
+
if (tabConfig.yasqe?.value) {
|
|
107
|
+
lines.push(` sp:text """${escapeTurtleString(tabConfig.yasqe.value)}""" ;`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Editor height
|
|
111
|
+
if (tabConfig.yasqe?.editorHeight) {
|
|
112
|
+
lines.push(` schema:height "${escapeTurtleString(tabConfig.yasqe.editorHeight)}" ;`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Request config
|
|
116
|
+
if (tabConfig.requestConfig) {
|
|
117
|
+
const reqConfig = tabConfig.requestConfig;
|
|
118
|
+
if (reqConfig.endpoint && typeof reqConfig.endpoint === "string") {
|
|
119
|
+
lines.push(` sd:endpoint "${escapeTurtleString(reqConfig.endpoint)}" ;`);
|
|
120
|
+
}
|
|
121
|
+
if (reqConfig.method && typeof reqConfig.method === "string") {
|
|
122
|
+
lines.push(` yasgui:requestMethod "${escapeTurtleString(reqConfig.method)}" ;`);
|
|
123
|
+
}
|
|
124
|
+
if (reqConfig.acceptHeaderSelect && typeof reqConfig.acceptHeaderSelect === "string") {
|
|
125
|
+
lines.push(` yasgui:acceptHeaderSelect [`);
|
|
126
|
+
lines.push(` http:headerName "Accept" ;`);
|
|
127
|
+
lines.push(` http:headerValue "${escapeTurtleString(reqConfig.acceptHeaderSelect)}"`);
|
|
128
|
+
lines.push(` ] ;`);
|
|
129
|
+
}
|
|
130
|
+
if (reqConfig.acceptHeaderGraph && typeof reqConfig.acceptHeaderGraph === "string") {
|
|
131
|
+
lines.push(` yasgui:acceptHeaderGraph [`);
|
|
132
|
+
lines.push(` http:headerName "Accept" ;`);
|
|
133
|
+
lines.push(` http:headerValue "${escapeTurtleString(reqConfig.acceptHeaderGraph)}"`);
|
|
134
|
+
lines.push(` ] ;`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// YASR settings
|
|
139
|
+
if (tabConfig.yasr?.settings) {
|
|
140
|
+
const yasrSettings = tabConfig.yasr.settings;
|
|
141
|
+
if (yasrSettings.selectedPlugin) {
|
|
142
|
+
lines.push(` yasgui:selectedPlugin "${escapeTurtleString(yasrSettings.selectedPlugin)}" ;`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Remove trailing semicolon from last property
|
|
147
|
+
const lastLine = lines[lines.length - 1];
|
|
148
|
+
if (lastLine.endsWith(" ;")) {
|
|
149
|
+
lines[lines.length - 1] = lastLine.slice(0, -2);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!isLastTab) {
|
|
153
|
+
lines.push(` ] , [`);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
lines.push(` ] .`);
|
|
157
|
+
} else {
|
|
158
|
+
// Remove trailing semicolon if no tabs
|
|
159
|
+
const lastLine = lines[lines.length - 1];
|
|
160
|
+
if (lastLine.endsWith(" ;")) {
|
|
161
|
+
lines[lines.length - 1] = lastLine.slice(0, -2) + " .";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return lines.join("\n");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Escape special characters in Turtle strings
|
|
170
|
+
*/
|
|
171
|
+
function escapeTurtleString(str: string): string {
|
|
172
|
+
return str
|
|
173
|
+
.replace(/\\/g, "\\\\")
|
|
174
|
+
.replace(/"/g, '\\"')
|
|
175
|
+
.replace(/\n/g, "\\n")
|
|
176
|
+
.replace(/\r/g, "\\r")
|
|
177
|
+
.replace(/\t/g, "\\t");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Unescape Turtle string
|
|
182
|
+
*/
|
|
183
|
+
function unescapeTurtleString(str: string): string {
|
|
184
|
+
return str
|
|
185
|
+
.replace(/\\n/g, "\n")
|
|
186
|
+
.replace(/\\r/g, "\r")
|
|
187
|
+
.replace(/\\t/g, "\t")
|
|
188
|
+
.replace(/\\"/g, '"')
|
|
189
|
+
.replace(/\\\\/g, "\\");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Parse Turtle format back to configuration
|
|
194
|
+
* This is a simplified parser focused on the structure we generate.
|
|
195
|
+
*
|
|
196
|
+
* Note: This parser is designed to handle the specific Turtle format
|
|
197
|
+
* produced by serializeToTurtle(). For production use with arbitrary
|
|
198
|
+
* Turtle input, consider using a robust RDF library like N3.js.
|
|
199
|
+
* The current implementation uses regex patterns that work well for
|
|
200
|
+
* our serialization output but may not handle all valid Turtle syntax.
|
|
201
|
+
*/
|
|
202
|
+
export function parseFromTurtle(turtle: string): Partial<PersistedJson> {
|
|
203
|
+
const config: Partial<PersistedJson> = {
|
|
204
|
+
endpointHistory: [],
|
|
205
|
+
tabs: [],
|
|
206
|
+
tabConfig: {},
|
|
207
|
+
customEndpointButtons: [],
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
// Extract endpoint history
|
|
212
|
+
const endpointHistoryMatch = turtle.match(/yasgui:endpointHistory\s*\(([\s\S]*?)\)/);
|
|
213
|
+
if (endpointHistoryMatch) {
|
|
214
|
+
const endpoints = endpointHistoryMatch[1].match(/"([^"]*)"/g);
|
|
215
|
+
if (endpoints) {
|
|
216
|
+
config.endpointHistory = endpoints.map((e) => unescapeTurtleString(e.slice(1, -1)));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Extract active tab
|
|
221
|
+
const activeTabMatch = turtle.match(/yasgui:activeTab\s+"([^"]*)"/);
|
|
222
|
+
if (activeTabMatch) {
|
|
223
|
+
config.active = unescapeTurtleString(activeTabMatch[1]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Extract prefixes
|
|
227
|
+
const prefixesMatch = turtle.match(/yasgui:prefixesValue\s+"""([\s\S]*?)"""/);
|
|
228
|
+
if (prefixesMatch) {
|
|
229
|
+
config.prefixes = unescapeTurtleString(prefixesMatch[1]);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Extract auto capture enabled
|
|
233
|
+
const autoCaptureMatch = turtle.match(/yasgui:autoCaptureEnabled\s+"([^"]*)"/);
|
|
234
|
+
if (autoCaptureMatch) {
|
|
235
|
+
config.autoCaptureEnabled = autoCaptureMatch[1] === "true";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Extract theme
|
|
239
|
+
const themeMatch = turtle.match(/yasgui:theme\s+"([^"]*)"/);
|
|
240
|
+
if (themeMatch && (themeMatch[1] === "light" || themeMatch[1] === "dark")) {
|
|
241
|
+
config.theme = themeMatch[1] as "light" | "dark";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Extract orientation
|
|
245
|
+
const orientationMatch = turtle.match(/yasgui:orientation\s+"([^"]*)"/);
|
|
246
|
+
if (orientationMatch && (orientationMatch[1] === "vertical" || orientationMatch[1] === "horizontal")) {
|
|
247
|
+
config.orientation = orientationMatch[1] as "vertical" | "horizontal";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Extract custom endpoint buttons
|
|
251
|
+
const buttonPattern =
|
|
252
|
+
/yasgui:customEndpointButton\s+\[([\s\S]*?)rdfs:label\s+"([^"]*)"\s*;\s*sd:endpoint\s+"([^"]*)"/g;
|
|
253
|
+
let buttonMatch;
|
|
254
|
+
while ((buttonMatch = buttonPattern.exec(turtle)) !== null) {
|
|
255
|
+
config.customEndpointButtons!.push({
|
|
256
|
+
label: unescapeTurtleString(buttonMatch[2]),
|
|
257
|
+
endpoint: unescapeTurtleString(buttonMatch[3]),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Extract tabs - simplified parsing
|
|
262
|
+
const tabPattern =
|
|
263
|
+
/dcterms:identifier\s+"([^"]*)"\s*;\s*rdfs:label\s+"([^"]*)"\s*;[\s\S]*?(?=dcterms:identifier|yasgui:customEndpointButton|\]\.|\]\s*,\s*\[)/g;
|
|
264
|
+
const tabBlocks = turtle.match(/yasgui:tab\s+\[([\s\S]*)\]\s*\./);
|
|
265
|
+
|
|
266
|
+
if (tabBlocks) {
|
|
267
|
+
const tabsContent = tabBlocks[1];
|
|
268
|
+
const tabIdMatches = [...tabsContent.matchAll(/dcterms:identifier\s+"([^"]*)"/g)];
|
|
269
|
+
const tabNameMatches = [...tabsContent.matchAll(/rdfs:label\s+"([^"]*)"/g)];
|
|
270
|
+
const queryMatches = [...tabsContent.matchAll(/sp:text\s+"""([\s\S]*?)"""/g)];
|
|
271
|
+
const endpointMatches = [...tabsContent.matchAll(/sd:endpoint\s+"([^"]*)"/g)];
|
|
272
|
+
const methodMatches = [...tabsContent.matchAll(/yasgui:requestMethod\s+"([^"]*)"/g)];
|
|
273
|
+
|
|
274
|
+
// Extract tab-level orientation
|
|
275
|
+
const tabOrientationMatches = [...tabsContent.matchAll(/yasgui:orientation\s+"([^"]*)"/g)];
|
|
276
|
+
|
|
277
|
+
tabIdMatches.forEach((match, index) => {
|
|
278
|
+
const tabId = unescapeTurtleString(match[1]);
|
|
279
|
+
const tabName = tabNameMatches[index] ? unescapeTurtleString(tabNameMatches[index][1]) : "Query";
|
|
280
|
+
const query = queryMatches[index] ? unescapeTurtleString(queryMatches[index][1]) : "";
|
|
281
|
+
const endpoint = endpointMatches[index] ? unescapeTurtleString(endpointMatches[index][1]) : "";
|
|
282
|
+
const method = methodMatches[index] ? unescapeTurtleString(methodMatches[index][1]) : "POST";
|
|
283
|
+
const orientation = tabOrientationMatches[index]
|
|
284
|
+
? (unescapeTurtleString(tabOrientationMatches[index][1]) as "vertical" | "horizontal")
|
|
285
|
+
: undefined;
|
|
286
|
+
|
|
287
|
+
config.tabs!.push(tabId);
|
|
288
|
+
config.tabConfig![tabId] = {
|
|
289
|
+
id: tabId,
|
|
290
|
+
name: tabName,
|
|
291
|
+
yasqe: {
|
|
292
|
+
value: query,
|
|
293
|
+
},
|
|
294
|
+
requestConfig: {
|
|
295
|
+
queryArgument: undefined,
|
|
296
|
+
endpoint: endpoint,
|
|
297
|
+
method: method as "GET" | "POST",
|
|
298
|
+
acceptHeaderGraph: "text/turtle",
|
|
299
|
+
acceptHeaderSelect: "application/sparql-results+json",
|
|
300
|
+
acceptHeaderUpdate: "application/sparql-results+json",
|
|
301
|
+
namedGraphs: [],
|
|
302
|
+
defaultGraphs: [],
|
|
303
|
+
args: [],
|
|
304
|
+
headers: {},
|
|
305
|
+
withCredentials: false,
|
|
306
|
+
adjustQueryBeforeRequest: false,
|
|
307
|
+
},
|
|
308
|
+
yasr: {
|
|
309
|
+
settings: {},
|
|
310
|
+
response: undefined,
|
|
311
|
+
},
|
|
312
|
+
orientation: orientation,
|
|
313
|
+
};
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
} catch (error) {
|
|
317
|
+
console.error("Error parsing Turtle configuration:", error);
|
|
318
|
+
throw new Error("Failed to parse configuration. Please check the format.");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return config;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Download configuration as a file
|
|
326
|
+
*/
|
|
327
|
+
export function downloadConfigAsFile(config: PersistedJson, filename: string = "yasgui-config.ttl") {
|
|
328
|
+
const turtle = serializeToTurtle(config);
|
|
329
|
+
const blob = new Blob([turtle], { type: "text/turtle;charset=utf-8" });
|
|
330
|
+
const url = URL.createObjectURL(blob);
|
|
331
|
+
|
|
332
|
+
const link = document.createElement("a");
|
|
333
|
+
link.href = url;
|
|
334
|
+
link.download = filename;
|
|
335
|
+
document.body.appendChild(link);
|
|
336
|
+
link.click();
|
|
337
|
+
document.body.removeChild(link);
|
|
338
|
+
URL.revokeObjectURL(url);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Copy configuration to clipboard
|
|
343
|
+
*/
|
|
344
|
+
export async function copyConfigToClipboard(config: PersistedJson): Promise<void> {
|
|
345
|
+
const turtle = serializeToTurtle(config);
|
|
346
|
+
try {
|
|
347
|
+
await navigator.clipboard.writeText(turtle);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
// Fallback for older browsers
|
|
350
|
+
const textArea = document.createElement("textarea");
|
|
351
|
+
textArea.value = turtle;
|
|
352
|
+
textArea.style.position = "fixed";
|
|
353
|
+
textArea.style.left = "-999999px";
|
|
354
|
+
document.body.appendChild(textArea);
|
|
355
|
+
textArea.select();
|
|
356
|
+
try {
|
|
357
|
+
document.execCommand("copy");
|
|
358
|
+
} finally {
|
|
359
|
+
document.body.removeChild(textArea);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Read configuration from file
|
|
366
|
+
*/
|
|
367
|
+
export function readConfigFromFile(file: File): Promise<string> {
|
|
368
|
+
return new Promise((resolve, reject) => {
|
|
369
|
+
const reader = new FileReader();
|
|
370
|
+
reader.onload = (e) => {
|
|
371
|
+
if (e.target?.result) {
|
|
372
|
+
resolve(e.target.result as string);
|
|
373
|
+
} else {
|
|
374
|
+
reject(new Error("Failed to read file"));
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
reader.onerror = () => reject(new Error("Failed to read file"));
|
|
378
|
+
reader.readAsText(file);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Read configuration from clipboard
|
|
384
|
+
*/
|
|
385
|
+
export async function readConfigFromClipboard(): Promise<string> {
|
|
386
|
+
try {
|
|
387
|
+
return await navigator.clipboard.readText();
|
|
388
|
+
} catch (error) {
|
|
389
|
+
throw new Error("Failed to read from clipboard. Please paste the content manually.");
|
|
390
|
+
}
|
|
391
|
+
}
|
package/src/PersistentConfig.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Storage as YStorage } from "@matdata/yasgui-utils";
|
|
2
|
-
import Yasgui from "./";
|
|
2
|
+
import Yasgui, { EndpointButton } from "./";
|
|
3
3
|
import * as Tab from "./Tab";
|
|
4
4
|
export var storageNamespace = "triply";
|
|
5
5
|
export interface PersistedJson {
|
|
@@ -10,6 +10,9 @@ export interface PersistedJson {
|
|
|
10
10
|
lastClosedTab: { index: number; tab: Tab.PersistedJson } | undefined;
|
|
11
11
|
prefixes?: string;
|
|
12
12
|
autoCaptureEnabled?: boolean;
|
|
13
|
+
customEndpointButtons?: EndpointButton[];
|
|
14
|
+
theme?: "light" | "dark";
|
|
15
|
+
orientation?: "vertical" | "horizontal";
|
|
13
16
|
}
|
|
14
17
|
function getDefaults(): PersistedJson {
|
|
15
18
|
return {
|
|
@@ -20,6 +23,7 @@ function getDefaults(): PersistedJson {
|
|
|
20
23
|
lastClosedTab: undefined,
|
|
21
24
|
prefixes: "",
|
|
22
25
|
autoCaptureEnabled: true,
|
|
26
|
+
customEndpointButtons: [],
|
|
23
27
|
};
|
|
24
28
|
}
|
|
25
29
|
|
|
@@ -152,8 +156,30 @@ export default class PersistentConfig {
|
|
|
152
156
|
this.persistedJson.autoCaptureEnabled = enabled;
|
|
153
157
|
this.toStorage();
|
|
154
158
|
}
|
|
159
|
+
public getCustomEndpointButtons(): EndpointButton[] {
|
|
160
|
+
return this.persistedJson.customEndpointButtons || [];
|
|
161
|
+
}
|
|
162
|
+
public setCustomEndpointButtons(buttons: EndpointButton[]) {
|
|
163
|
+
this.persistedJson.customEndpointButtons = buttons;
|
|
164
|
+
this.toStorage();
|
|
165
|
+
}
|
|
155
166
|
public static clear() {
|
|
156
167
|
const storage = new YStorage(storageNamespace);
|
|
157
168
|
storage.removeNamespace();
|
|
158
169
|
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get the current persisted configuration (for export purposes)
|
|
173
|
+
*/
|
|
174
|
+
public getPersistedConfig(): PersistedJson {
|
|
175
|
+
return this.persistedJson;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Update the persisted configuration (for import purposes)
|
|
180
|
+
*/
|
|
181
|
+
public updatePersistedConfig(config: Partial<PersistedJson>) {
|
|
182
|
+
Object.assign(this.persistedJson, config);
|
|
183
|
+
this.toStorage();
|
|
184
|
+
}
|
|
159
185
|
}
|
package/src/Tab.ts
CHANGED
|
@@ -9,6 +9,17 @@ import * as shareLink from "./linkUtils";
|
|
|
9
9
|
import EndpointSelect from "./endpointSelect";
|
|
10
10
|
import "./tab.scss";
|
|
11
11
|
import { getRandomId, default as Yasgui, YasguiRequestConfig } from "./";
|
|
12
|
+
|
|
13
|
+
// Layout orientation toggle icons
|
|
14
|
+
const HORIZONTAL_LAYOUT_ICON = `<svg viewBox="0 0 24 24" class="svgImg">
|
|
15
|
+
<rect x="2" y="4" width="9" height="16" stroke="currentColor" stroke-width="2" fill="none"/>
|
|
16
|
+
<rect x="13" y="4" width="9" height="16" stroke="currentColor" stroke-width="2" fill="none"/>
|
|
17
|
+
</svg>`;
|
|
18
|
+
|
|
19
|
+
const VERTICAL_LAYOUT_ICON = `<svg viewBox="0 0 24 24" class="svgImg">
|
|
20
|
+
<rect x="2" y="2" width="20" height="8" stroke="currentColor" stroke-width="2" fill="none"/>
|
|
21
|
+
<rect x="2" y="12" width="20" height="10" stroke="currentColor" stroke-width="2" fill="none"/>
|
|
22
|
+
</svg>`;
|
|
12
23
|
export interface PersistedJsonYasr extends YasrPersistentConfig {
|
|
13
24
|
responseSummary: Parser.ResponseSummary;
|
|
14
25
|
}
|
|
@@ -24,6 +35,7 @@ export interface PersistedJson {
|
|
|
24
35
|
response: Parser.ResponseSummary | undefined;
|
|
25
36
|
};
|
|
26
37
|
requestConfig: YasguiRequestConfig;
|
|
38
|
+
orientation?: "vertical" | "horizontal";
|
|
27
39
|
}
|
|
28
40
|
export interface Tab {
|
|
29
41
|
on(event: string | symbol, listener: (...args: any[]) => void): this;
|
|
@@ -57,12 +69,16 @@ export class Tab extends EventEmitter {
|
|
|
57
69
|
private yasqeWrapperEl: HTMLDivElement | undefined;
|
|
58
70
|
private yasrWrapperEl: HTMLDivElement | undefined;
|
|
59
71
|
private endpointSelect: EndpointSelect | undefined;
|
|
72
|
+
private endpointButtonsContainer: HTMLDivElement | undefined;
|
|
60
73
|
private settingsModal?: TabSettingsModal;
|
|
74
|
+
private currentOrientation: "vertical" | "horizontal";
|
|
75
|
+
private orientationToggleButton?: HTMLButtonElement;
|
|
61
76
|
constructor(yasgui: Yasgui, conf: PersistedJson) {
|
|
62
77
|
super();
|
|
63
78
|
if (!conf || conf.id === undefined) throw new Error("Expected a valid configuration to initialize tab with");
|
|
64
79
|
this.yasgui = yasgui;
|
|
65
80
|
this.persistentJson = conf;
|
|
81
|
+
this.currentOrientation = this.yasgui.config.orientation || "vertical";
|
|
66
82
|
}
|
|
67
83
|
public name() {
|
|
68
84
|
return this.persistentJson.name;
|
|
@@ -81,6 +97,9 @@ export class Tab extends EventEmitter {
|
|
|
81
97
|
this.rootEl.setAttribute("role", "tabpanel");
|
|
82
98
|
this.rootEl.setAttribute("aria-labelledby", "tab-" + this.persistentJson.id);
|
|
83
99
|
|
|
100
|
+
// Apply orientation class
|
|
101
|
+
addClass(this.rootEl, `orientation-${this.currentOrientation}`);
|
|
102
|
+
|
|
84
103
|
// We group controlbar and Yasqe, so that users can easily .appendChild() to the .editorwrapper div
|
|
85
104
|
// to add a div that goes alongside the controlbar and editor, while YASR still goes full width
|
|
86
105
|
// Useful for adding an infos div that goes alongside the editor without needing to rebuild the whole Yasgui class
|
|
@@ -99,6 +118,7 @@ export class Tab extends EventEmitter {
|
|
|
99
118
|
|
|
100
119
|
//yasr
|
|
101
120
|
this.yasrWrapperEl = document.createElement("div");
|
|
121
|
+
this.yasrWrapperEl.className = "yasrWrapperEl";
|
|
102
122
|
|
|
103
123
|
this.initTabSettingsMenu();
|
|
104
124
|
this.rootEl.appendChild(editorWrapper);
|
|
@@ -151,8 +171,8 @@ export class Tab extends EventEmitter {
|
|
|
151
171
|
}
|
|
152
172
|
}
|
|
153
173
|
}
|
|
154
|
-
//
|
|
155
|
-
else if (event.
|
|
174
|
+
// F9 - Switch between fullscreen modes
|
|
175
|
+
else if (event.key === "F9") {
|
|
156
176
|
event.preventDefault();
|
|
157
177
|
const yasqeFullscreen = this.yasqe?.getIsFullscreen();
|
|
158
178
|
const yasrFullscreen = this.yasr?.getIsFullscreen();
|
|
@@ -222,10 +242,63 @@ export class Tab extends EventEmitter {
|
|
|
222
242
|
}
|
|
223
243
|
private initControlbar() {
|
|
224
244
|
this.initEndpointSelectField();
|
|
245
|
+
this.initOrientationToggle();
|
|
246
|
+
this.initEndpointButtons();
|
|
225
247
|
if (this.yasgui.config.endpointInfo && this.controlBarEl) {
|
|
226
248
|
this.controlBarEl.appendChild(this.yasgui.config.endpointInfo());
|
|
227
249
|
}
|
|
228
250
|
}
|
|
251
|
+
|
|
252
|
+
private initOrientationToggle() {
|
|
253
|
+
if (!this.controlBarEl) return;
|
|
254
|
+
|
|
255
|
+
this.orientationToggleButton = document.createElement("button");
|
|
256
|
+
this.orientationToggleButton.className = "tabContextButton orientationToggle";
|
|
257
|
+
this.orientationToggleButton.setAttribute("aria-label", "Toggle layout orientation");
|
|
258
|
+
this.orientationToggleButton.title = "Toggle layout orientation";
|
|
259
|
+
|
|
260
|
+
this.updateOrientationToggleIcon();
|
|
261
|
+
|
|
262
|
+
this.orientationToggleButton.addEventListener("click", () => {
|
|
263
|
+
this.toggleOrientation();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
this.controlBarEl.appendChild(this.orientationToggleButton);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private updateOrientationToggleIcon() {
|
|
270
|
+
if (!this.orientationToggleButton) return;
|
|
271
|
+
|
|
272
|
+
// Show the icon for the layout we'll switch TO (not the current layout)
|
|
273
|
+
this.orientationToggleButton.innerHTML =
|
|
274
|
+
this.currentOrientation === "vertical" ? HORIZONTAL_LAYOUT_ICON : VERTICAL_LAYOUT_ICON;
|
|
275
|
+
this.orientationToggleButton.title =
|
|
276
|
+
this.currentOrientation === "vertical" ? "Switch to horizontal layout" : "Switch to vertical layout";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
public toggleOrientation() {
|
|
280
|
+
if (!this.rootEl) return;
|
|
281
|
+
|
|
282
|
+
// Remove old orientation class
|
|
283
|
+
removeClass(this.rootEl, `orientation-${this.currentOrientation}`);
|
|
284
|
+
|
|
285
|
+
// Toggle orientation
|
|
286
|
+
this.currentOrientation = this.currentOrientation === "vertical" ? "horizontal" : "vertical";
|
|
287
|
+
|
|
288
|
+
// Add new orientation class
|
|
289
|
+
addClass(this.rootEl, `orientation-${this.currentOrientation}`);
|
|
290
|
+
|
|
291
|
+
// Update button icon
|
|
292
|
+
this.updateOrientationToggleIcon();
|
|
293
|
+
|
|
294
|
+
// Refresh components to adjust to new layout
|
|
295
|
+
if (this.yasqe) {
|
|
296
|
+
this.yasqe.refresh();
|
|
297
|
+
}
|
|
298
|
+
if (this.yasr) {
|
|
299
|
+
this.yasr.refresh();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
229
302
|
public getYasqe() {
|
|
230
303
|
return this.yasqe;
|
|
231
304
|
}
|
|
@@ -253,6 +326,54 @@ export class Tab extends EventEmitter {
|
|
|
253
326
|
});
|
|
254
327
|
}
|
|
255
328
|
|
|
329
|
+
private initEndpointButtons() {
|
|
330
|
+
if (!this.controlBarEl) throw new Error("Need to initialize wrapper elements before drawing endpoint buttons");
|
|
331
|
+
|
|
332
|
+
// Create container if it doesn't exist
|
|
333
|
+
if (!this.endpointButtonsContainer) {
|
|
334
|
+
this.endpointButtonsContainer = document.createElement("div");
|
|
335
|
+
addClass(this.endpointButtonsContainer, "endpointButtonsContainer");
|
|
336
|
+
this.controlBarEl.appendChild(this.endpointButtonsContainer);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
this.refreshEndpointButtons();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
public refreshEndpointButtons() {
|
|
343
|
+
if (!this.endpointButtonsContainer) return;
|
|
344
|
+
|
|
345
|
+
// Clear existing buttons
|
|
346
|
+
this.endpointButtonsContainer.innerHTML = "";
|
|
347
|
+
|
|
348
|
+
// Merge config buttons with custom user buttons
|
|
349
|
+
const configButtons = this.yasgui.config.endpointButtons || [];
|
|
350
|
+
const customButtons = this.yasgui.persistentConfig.getCustomEndpointButtons();
|
|
351
|
+
const allButtons = [...configButtons, ...customButtons];
|
|
352
|
+
|
|
353
|
+
if (allButtons.length === 0) {
|
|
354
|
+
// Hide container if no buttons
|
|
355
|
+
this.endpointButtonsContainer.style.display = "none";
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Show container
|
|
360
|
+
this.endpointButtonsContainer.style.display = "flex";
|
|
361
|
+
|
|
362
|
+
allButtons.forEach((buttonConfig) => {
|
|
363
|
+
const button = document.createElement("button");
|
|
364
|
+
addClass(button, "endpointButton");
|
|
365
|
+
button.textContent = buttonConfig.label;
|
|
366
|
+
button.title = `Set endpoint to ${buttonConfig.endpoint}`;
|
|
367
|
+
button.setAttribute("aria-label", `Set endpoint to ${buttonConfig.endpoint}`);
|
|
368
|
+
|
|
369
|
+
button.addEventListener("click", () => {
|
|
370
|
+
this.setEndpoint(buttonConfig.endpoint);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
this.endpointButtonsContainer!.appendChild(button);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
256
377
|
private checkEndpointForCors(endpoint: string) {
|
|
257
378
|
if (this.yasgui.config.corsProxy && !(endpoint in Yasgui.corsEnabled)) {
|
|
258
379
|
const askUrl = new URL(endpoint);
|