@matdata/yasgui 5.15.0 → 5.16.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/build/ts/src/Tab.d.ts +2 -0
- package/build/ts/src/Tab.js +46 -1
- package/build/ts/src/Tab.js.map +1 -1
- package/build/ts/src/TabContextMenu.d.ts +2 -1
- package/build/ts/src/TabContextMenu.js +11 -3
- package/build/ts/src/TabContextMenu.js.map +1 -1
- package/build/ts/src/TabElements.js +13 -3
- package/build/ts/src/TabElements.js.map +1 -1
- package/build/ts/src/TabSettingsModal.d.ts +8 -0
- package/build/ts/src/TabSettingsModal.js +89 -3
- package/build/ts/src/TabSettingsModal.js.map +1 -1
- package/build/ts/src/endpointSelect.js +1 -1
- package/build/ts/src/queryManagement/QueryBrowser.d.ts +1 -0
- package/build/ts/src/queryManagement/QueryBrowser.js +62 -0
- package/build/ts/src/queryManagement/QueryBrowser.js.map +1 -1
- package/build/ts/src/queryManagement/SaveManagedQueryModal.d.ts +6 -0
- package/build/ts/src/queryManagement/SaveManagedQueryModal.js +67 -3
- package/build/ts/src/queryManagement/SaveManagedQueryModal.js.map +1 -1
- package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.d.ts +1 -0
- package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.js +18 -0
- package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.js.map +1 -1
- package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.d.ts +1 -0
- package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.js +17 -0
- package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.js.map +1 -1
- package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.d.ts +1 -0
- package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.js +61 -0
- package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.js.map +1 -1
- package/build/ts/src/queryManagement/backends/WorkspaceBackend.d.ts +1 -0
- package/build/ts/src/version.d.ts +1 -1
- package/build/ts/src/version.js +1 -1
- package/build/yasgui.min.css +1 -1
- package/build/yasgui.min.css.map +3 -3
- package/build/yasgui.min.js +236 -210
- package/build/yasgui.min.js.map +4 -4
- package/package.json +1 -1
- package/src/Tab.ts +53 -1
- package/src/TabContextMenu.ts +15 -5
- package/src/TabElements.scss +17 -0
- package/src/TabElements.ts +17 -3
- package/src/TabSettingsModal.scss +73 -0
- package/src/TabSettingsModal.ts +117 -3
- package/src/endpointSelect.scss +0 -22
- package/src/endpointSelect.ts +1 -1
- package/src/queryManagement/QueryBrowser.ts +72 -0
- package/src/queryManagement/SaveManagedQueryModal.ts +82 -3
- package/src/queryManagement/backends/GitWorkspaceBackend.ts +19 -0
- package/src/queryManagement/backends/InMemoryWorkspaceBackend.ts +17 -0
- package/src/queryManagement/backends/SparqlWorkspaceBackend.ts +69 -0
- package/src/queryManagement/backends/WorkspaceBackend.ts +6 -0
- package/src/tab.scss +1 -0
- package/src/themes.scss +0 -10
- package/src/version.ts +1 -1
package/package.json
CHANGED
package/src/Tab.ts
CHANGED
|
@@ -175,6 +175,42 @@ export class Tab extends EventEmitter {
|
|
|
175
175
|
return this.persistentJson.yasqe.value;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
public async downloadAsRqFile() {
|
|
179
|
+
const query = this.getQueryTextForSave();
|
|
180
|
+
const tabName = this.name() || "query";
|
|
181
|
+
const filename = tabName.endsWith(".rq") || tabName.endsWith(".sparql") ? tabName : `${tabName}.rq`;
|
|
182
|
+
const blob = new Blob([query], { type: "application/sparql-query" });
|
|
183
|
+
|
|
184
|
+
// Use File System Access API if available so the user can choose the save location
|
|
185
|
+
if ("showSaveFilePicker" in window) {
|
|
186
|
+
try {
|
|
187
|
+
const showSaveFilePicker = (
|
|
188
|
+
window as Window & { showSaveFilePicker: (...args: unknown[]) => Promise<FileSystemFileHandle> }
|
|
189
|
+
).showSaveFilePicker;
|
|
190
|
+
const fileHandle = await showSaveFilePicker({
|
|
191
|
+
suggestedName: filename,
|
|
192
|
+
types: [{ description: "SPARQL Query", accept: { "application/sparql-query": [".rq", ".sparql"] } }],
|
|
193
|
+
});
|
|
194
|
+
const writable = await fileHandle.createWritable();
|
|
195
|
+
await writable.write(blob);
|
|
196
|
+
await writable.close();
|
|
197
|
+
return;
|
|
198
|
+
} catch (e: any) {
|
|
199
|
+
// User cancelled the picker – abort silently
|
|
200
|
+
if (e?.name === "AbortError") return;
|
|
201
|
+
// Other errors fall through to the legacy download below
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Fallback for browsers without File System Access API
|
|
206
|
+
const url = URL.createObjectURL(blob);
|
|
207
|
+
const a = document.createElement("a");
|
|
208
|
+
a.href = url;
|
|
209
|
+
a.download = filename;
|
|
210
|
+
a.click();
|
|
211
|
+
URL.revokeObjectURL(url);
|
|
212
|
+
}
|
|
213
|
+
|
|
178
214
|
public async saveManagedQueryOrSaveAsManagedQuery(): Promise<void> {
|
|
179
215
|
const meta = this.getManagedQueryMetadata();
|
|
180
216
|
if (!meta) {
|
|
@@ -535,7 +571,7 @@ export class Tab extends EventEmitter {
|
|
|
535
571
|
if (!this.controlBarEl) return;
|
|
536
572
|
|
|
537
573
|
this.orientationToggleButton = document.createElement("button");
|
|
538
|
-
this.orientationToggleButton.className = "tabContextButton orientationToggle";
|
|
574
|
+
this.orientationToggleButton.className = "tabContextButton orientationToggle desktopOnly";
|
|
539
575
|
this.orientationToggleButton.setAttribute("aria-label", "Toggle layout orientation");
|
|
540
576
|
this.orientationToggleButton.title = "Toggle layout orientation";
|
|
541
577
|
|
|
@@ -615,6 +651,10 @@ export class Tab extends EventEmitter {
|
|
|
615
651
|
return this.yasr;
|
|
616
652
|
}
|
|
617
653
|
|
|
654
|
+
public getCurrentOrientation() {
|
|
655
|
+
return this.currentOrientation;
|
|
656
|
+
}
|
|
657
|
+
|
|
618
658
|
private initTabSettingsMenu() {
|
|
619
659
|
if (!this.controlBarEl) throw new Error("Need to initialize wrapper elements before drawing tab settings");
|
|
620
660
|
this.settingsModal = new TabSettingsModal(this, this.controlBarEl);
|
|
@@ -1302,6 +1342,11 @@ export class Tab extends EventEmitter {
|
|
|
1302
1342
|
void this.saveManagedQueryOrSaveAsManagedQuery();
|
|
1303
1343
|
});
|
|
1304
1344
|
|
|
1345
|
+
// Hook up download as .rq file
|
|
1346
|
+
this.yasqe.on("downloadRqFile", () => {
|
|
1347
|
+
void this.downloadAsRqFile();
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1305
1350
|
// Show/hide save button based on workspace configuration
|
|
1306
1351
|
this.updateSaveButtonVisibility();
|
|
1307
1352
|
|
|
@@ -1627,6 +1672,13 @@ WHERE {
|
|
|
1627
1672
|
// Add default renderers to the end, to give our custom ones priority.
|
|
1628
1673
|
...(Yasr.defaults.errorRenderers || []),
|
|
1629
1674
|
],
|
|
1675
|
+
executeQuery: async (query: string, options?: { acceptHeader?: string }) => {
|
|
1676
|
+
if (!this.yasqe) throw new Error("No YASQE instance available");
|
|
1677
|
+
return Yasqe.Sparql.executeQuery(this.yasqe, undefined, {
|
|
1678
|
+
customQuery: query,
|
|
1679
|
+
customAccept: options?.acceptHeader,
|
|
1680
|
+
});
|
|
1681
|
+
},
|
|
1630
1682
|
};
|
|
1631
1683
|
// Allow getDownloadFilName to be overwritten by the global config
|
|
1632
1684
|
if (yasrConf.getDownloadFileName === undefined) {
|
package/src/TabContextMenu.ts
CHANGED
|
@@ -14,8 +14,9 @@ export default class TabContextMenu {
|
|
|
14
14
|
private contextEl!: HTMLElement;
|
|
15
15
|
private newTabEl!: HTMLElement;
|
|
16
16
|
private renameTabEl!: HTMLElement;
|
|
17
|
-
private
|
|
17
|
+
private duplicateTabEl!: HTMLElement;
|
|
18
18
|
private saveManagedQueryEl!: HTMLElement;
|
|
19
|
+
private saveAsRqFileEl!: HTMLElement;
|
|
19
20
|
private closeTabEl!: HTMLElement;
|
|
20
21
|
private closeOtherTabsEl!: HTMLElement;
|
|
21
22
|
private reOpenOldTab!: HTMLElement;
|
|
@@ -48,10 +49,12 @@ export default class TabContextMenu {
|
|
|
48
49
|
|
|
49
50
|
this.renameTabEl = this.getMenuItemEl("Rename Tab");
|
|
50
51
|
|
|
51
|
-
this.
|
|
52
|
+
this.duplicateTabEl = this.getMenuItemEl("Duplicate Tab");
|
|
52
53
|
|
|
53
54
|
this.saveManagedQueryEl = this.getMenuItemEl("Save as managed query");
|
|
54
55
|
|
|
56
|
+
this.saveAsRqFileEl = this.getMenuItemEl("Save as .rq file");
|
|
57
|
+
|
|
55
58
|
this.closeTabEl = this.getMenuItemEl("Close Tab");
|
|
56
59
|
|
|
57
60
|
this.closeOtherTabsEl = this.getMenuItemEl("Close other tabs");
|
|
@@ -61,8 +64,9 @@ export default class TabContextMenu {
|
|
|
61
64
|
// Add items to list
|
|
62
65
|
dropDownList.appendChild(this.newTabEl);
|
|
63
66
|
dropDownList.appendChild(this.renameTabEl);
|
|
64
|
-
dropDownList.appendChild(this.
|
|
67
|
+
dropDownList.appendChild(this.duplicateTabEl);
|
|
65
68
|
dropDownList.appendChild(this.saveManagedQueryEl);
|
|
69
|
+
dropDownList.appendChild(this.saveAsRqFileEl);
|
|
66
70
|
// Add divider
|
|
67
71
|
dropDownList.appendChild(document.createElement("hr"));
|
|
68
72
|
dropDownList.appendChild(this.closeTabEl);
|
|
@@ -101,8 +105,8 @@ export default class TabContextMenu {
|
|
|
101
105
|
// Set rename functionality
|
|
102
106
|
this.renameTabEl.onclick = () => currentTabEl.startRename();
|
|
103
107
|
|
|
104
|
-
//
|
|
105
|
-
this.
|
|
108
|
+
// Duplicate tab functionality
|
|
109
|
+
this.duplicateTabEl.onclick = () => {
|
|
106
110
|
if (!tab) return;
|
|
107
111
|
const config = cloneDeep(tab.getPersistedJson());
|
|
108
112
|
config.id = getRandomId();
|
|
@@ -115,6 +119,12 @@ export default class TabContextMenu {
|
|
|
115
119
|
this.closeConfigMenu();
|
|
116
120
|
};
|
|
117
121
|
|
|
122
|
+
this.saveAsRqFileEl.onclick = () => {
|
|
123
|
+
if (!tab) return;
|
|
124
|
+
tab.downloadAsRqFile();
|
|
125
|
+
this.closeConfigMenu();
|
|
126
|
+
};
|
|
127
|
+
|
|
118
128
|
// Close tab functionality
|
|
119
129
|
this.closeTabEl.onclick = () => tab?.close();
|
|
120
130
|
|
package/src/TabElements.scss
CHANGED
|
@@ -94,6 +94,10 @@ $minTabHeight: 35px;
|
|
|
94
94
|
position: relative;
|
|
95
95
|
$activeColor: #337ab7;
|
|
96
96
|
$hoverColor: color.adjust($activeColor, $lightness: 30%);
|
|
97
|
+
user-select: none;
|
|
98
|
+
-webkit-user-select: none;
|
|
99
|
+
-moz-user-select: none;
|
|
100
|
+
-ms-user-select: none;
|
|
97
101
|
|
|
98
102
|
// Distinguish managed-query tabs from legacy tabs via background.
|
|
99
103
|
// Legacy tabs keep their current styling.
|
|
@@ -190,6 +194,19 @@ $minTabHeight: 35px;
|
|
|
190
194
|
padding: 0px 24px 0px 30px;
|
|
191
195
|
white-space: nowrap;
|
|
192
196
|
overflow: hidden;
|
|
197
|
+
user-select: none;
|
|
198
|
+
-webkit-user-select: none;
|
|
199
|
+
-moz-user-select: none;
|
|
200
|
+
-ms-user-select: none;
|
|
201
|
+
-webkit-user-drag: none;
|
|
202
|
+
|
|
203
|
+
span {
|
|
204
|
+
user-select: none;
|
|
205
|
+
-webkit-user-select: none;
|
|
206
|
+
-moz-user-select: none;
|
|
207
|
+
-ms-user-select: none;
|
|
208
|
+
pointer-events: none;
|
|
209
|
+
}
|
|
193
210
|
|
|
194
211
|
&:hover {
|
|
195
212
|
border-bottom-color: $hoverColor;
|
package/src/TabElements.ts
CHANGED
|
@@ -107,7 +107,7 @@ export class TabListEl {
|
|
|
107
107
|
tabLinkEl.href = "#" + this.tabId;
|
|
108
108
|
tabLinkEl.id = "tab-" + this.tabId; // use the id for the tabpanel which is tabId to set the actual tab id
|
|
109
109
|
tabLinkEl.setAttribute("aria-controls", this.tabId); // respective tabPanel id
|
|
110
|
-
|
|
110
|
+
// Note: draggable attribute removed - CSS user-select:none prevents text selection during drag
|
|
111
111
|
tabLinkEl.addEventListener("blur", () => {
|
|
112
112
|
if (!this.tabEl) return;
|
|
113
113
|
if (this.tabEl.classList.contains("active")) {
|
|
@@ -133,6 +133,17 @@ export class TabListEl {
|
|
|
133
133
|
this.yasgui.selectTabId(this.tabId);
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
+
// Prevent anchor default drag behavior that interferes with SortableJS in some browsers
|
|
137
|
+
tabLinkEl.addEventListener("dragstart", (e) => {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
});
|
|
140
|
+
tabLinkEl.addEventListener("mousedown", (e) => {
|
|
141
|
+
// Only prevent default on middle/right click to avoid navigation
|
|
142
|
+
if (e.button !== 0) {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
136
147
|
//tab name
|
|
137
148
|
this.nameEl = document.createElement("span");
|
|
138
149
|
this.nameEl.textContent = name;
|
|
@@ -315,7 +326,7 @@ export class TabList {
|
|
|
315
326
|
queryBrowserButton.className = "queryBrowserToggle";
|
|
316
327
|
queryBrowserButton.setAttribute("aria-label", "Open query browser");
|
|
317
328
|
queryBrowserButton.title = "Open query browser";
|
|
318
|
-
queryBrowserButton.innerHTML = '<i class="
|
|
329
|
+
queryBrowserButton.innerHTML = '<i class="fa-regular fa-file-lines"></i>';
|
|
319
330
|
queryBrowserButton.addEventListener("click", () => {
|
|
320
331
|
this.yasgui.queryBrowser.toggle(queryBrowserButton);
|
|
321
332
|
});
|
|
@@ -326,12 +337,15 @@ export class TabList {
|
|
|
326
337
|
sortablejs.create(this._tabsListEl, {
|
|
327
338
|
group: "tabList",
|
|
328
339
|
animation: 100,
|
|
340
|
+
draggable: ".tab",
|
|
341
|
+
forceFallback: true,
|
|
342
|
+
fallbackTolerance: 3,
|
|
329
343
|
onUpdate: (_ev: any) => {
|
|
330
344
|
const tabs = this.deriveTabOrderFromEls();
|
|
331
345
|
this.yasgui.emit("tabOrderChanged", this.yasgui, tabs);
|
|
332
346
|
this.yasgui.persistentConfig.setTabOrder(tabs);
|
|
333
347
|
},
|
|
334
|
-
filter: ".queryBrowserToggle, .addTab, input, .renaming",
|
|
348
|
+
filter: ".queryBrowserToggle, .addTab, input, .renaming, .closeTab",
|
|
335
349
|
preventOnFilter: false,
|
|
336
350
|
onMove: (ev: any, _origEv: any) => {
|
|
337
351
|
return hasClass(ev.related, "tab");
|
|
@@ -1251,3 +1251,76 @@
|
|
|
1251
1251
|
transform: translateY(1px);
|
|
1252
1252
|
}
|
|
1253
1253
|
}
|
|
1254
|
+
|
|
1255
|
+
// Hamburger menu styles
|
|
1256
|
+
.hamburgerContainer {
|
|
1257
|
+
position: relative;
|
|
1258
|
+
display: none; // Hidden by default (desktop)
|
|
1259
|
+
|
|
1260
|
+
@media (max-width: 768px) {
|
|
1261
|
+
display: flex; // Show on mobile
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
.hamburgerMenuButton {
|
|
1266
|
+
&.active {
|
|
1267
|
+
color: var(--yasgui-accent-color, #337ab7);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
.hamburgerDropdown {
|
|
1272
|
+
position: absolute;
|
|
1273
|
+
top: 100%;
|
|
1274
|
+
left: 0;
|
|
1275
|
+
background: var(--yasgui-bg-primary, white);
|
|
1276
|
+
border: 1px solid var(--yasgui-border-color, #e0e0e0);
|
|
1277
|
+
border-radius: 4px;
|
|
1278
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
1279
|
+
min-width: 200px;
|
|
1280
|
+
z-index: 1000;
|
|
1281
|
+
margin-top: 5px;
|
|
1282
|
+
overflow: hidden;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
.hamburgerMenuItem {
|
|
1286
|
+
display: flex;
|
|
1287
|
+
align-items: center;
|
|
1288
|
+
gap: 12px;
|
|
1289
|
+
width: 100%;
|
|
1290
|
+
padding: 12px 16px;
|
|
1291
|
+
background: none;
|
|
1292
|
+
border: none;
|
|
1293
|
+
border-bottom: 1px solid var(--yasgui-border-color, #f0f0f0);
|
|
1294
|
+
text-align: left;
|
|
1295
|
+
cursor: pointer;
|
|
1296
|
+
color: var(--yasgui-text-primary, #333);
|
|
1297
|
+
transition: background-color 0.2s;
|
|
1298
|
+
|
|
1299
|
+
&:last-child {
|
|
1300
|
+
border-bottom: none;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
&:hover {
|
|
1304
|
+
background: var(--yasgui-bg-secondary, #f7f7f7);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
i {
|
|
1308
|
+
font-size: 16px;
|
|
1309
|
+
width: 20px;
|
|
1310
|
+
text-align: center;
|
|
1311
|
+
color: var(--yasgui-button-text, #505050);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
span {
|
|
1315
|
+
flex: 1;
|
|
1316
|
+
font-size: 14px;
|
|
1317
|
+
font-weight: 500;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// Responsive visibility classes
|
|
1322
|
+
.desktopOnly {
|
|
1323
|
+
@media (max-width: 768px) {
|
|
1324
|
+
display: none !important;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
package/src/TabSettingsModal.ts
CHANGED
|
@@ -39,6 +39,9 @@ export default class TabSettingsModal {
|
|
|
39
39
|
private prefixButton!: HTMLButtonElement;
|
|
40
40
|
private prefixTextarea!: HTMLTextAreaElement;
|
|
41
41
|
private autoCaptureCheckbox!: HTMLInputElement;
|
|
42
|
+
private hamburgerMenuButton!: HTMLButtonElement;
|
|
43
|
+
private hamburgerDropdown!: HTMLElement;
|
|
44
|
+
private isHamburgerOpen = false;
|
|
42
45
|
private mouseDownOnOverlay = false;
|
|
43
46
|
private handleKeyDown = (e: KeyboardEvent) => {
|
|
44
47
|
if (e.key !== "Escape") return;
|
|
@@ -55,7 +58,7 @@ export default class TabSettingsModal {
|
|
|
55
58
|
private init(controlBarEl: HTMLElement) {
|
|
56
59
|
// Settings button
|
|
57
60
|
this.settingsButton = document.createElement("button");
|
|
58
|
-
addClass(this.settingsButton, "tabContextButton");
|
|
61
|
+
addClass(this.settingsButton, "tabContextButton", "desktopOnly");
|
|
59
62
|
this.settingsButton.setAttribute("aria-label", "Settings");
|
|
60
63
|
this.settingsButton.title = "Settings";
|
|
61
64
|
this.settingsButton.innerHTML = '<i class="fas fa-cog"></i>';
|
|
@@ -65,7 +68,7 @@ export default class TabSettingsModal {
|
|
|
65
68
|
// Theme toggle button (if enabled)
|
|
66
69
|
if (this.tab.yasgui.config.showThemeToggle) {
|
|
67
70
|
this.themeToggleButton = document.createElement("button");
|
|
68
|
-
addClass(this.themeToggleButton, "tabContextButton", "themeToggle");
|
|
71
|
+
addClass(this.themeToggleButton, "tabContextButton", "themeToggle", "desktopOnly");
|
|
69
72
|
this.themeToggleButton.setAttribute("aria-label", "Toggle between light and dark theme");
|
|
70
73
|
this.themeToggleButton.title = "Toggle theme";
|
|
71
74
|
this.themeToggleButton.innerHTML = this.getThemeToggleIcon();
|
|
@@ -81,13 +84,124 @@ export default class TabSettingsModal {
|
|
|
81
84
|
this.prefixButton.setAttribute("aria-label", "Insert Prefixes");
|
|
82
85
|
this.prefixButton.title = "Insert saved prefixes into query";
|
|
83
86
|
this.prefixButton.innerHTML = '<i class="fas fa-arrow-up-right-from-square"></i>';
|
|
84
|
-
addClass(this.prefixButton, "tabContextButton", "prefixButton");
|
|
87
|
+
addClass(this.prefixButton, "tabContextButton", "prefixButton", "desktopOnly");
|
|
85
88
|
controlBarEl.appendChild(this.prefixButton);
|
|
86
89
|
this.prefixButton.onclick = () => this.insertPrefixesIntoQuery();
|
|
87
90
|
|
|
91
|
+
// Hamburger menu button and dropdown (mobile only)
|
|
92
|
+
const hamburgerContainer = document.createElement("div");
|
|
93
|
+
addClass(hamburgerContainer, "hamburgerContainer", "mobileOnly");
|
|
94
|
+
|
|
95
|
+
this.hamburgerMenuButton = document.createElement("button");
|
|
96
|
+
addClass(this.hamburgerMenuButton, "tabContextButton", "hamburgerMenuButton");
|
|
97
|
+
this.hamburgerMenuButton.setAttribute("aria-label", "Menu");
|
|
98
|
+
this.hamburgerMenuButton.title = "Open menu";
|
|
99
|
+
this.hamburgerMenuButton.innerHTML = '<i class="fas fa-bars"></i>';
|
|
100
|
+
this.hamburgerMenuButton.onclick = (e) => {
|
|
101
|
+
e.stopPropagation();
|
|
102
|
+
this.toggleHamburgerMenu();
|
|
103
|
+
};
|
|
104
|
+
hamburgerContainer.appendChild(this.hamburgerMenuButton);
|
|
105
|
+
|
|
106
|
+
// Create hamburger dropdown menu
|
|
107
|
+
this.createHamburgerDropdown(hamburgerContainer);
|
|
108
|
+
|
|
109
|
+
controlBarEl.appendChild(hamburgerContainer);
|
|
110
|
+
|
|
111
|
+
// Close hamburger menu when clicking outside
|
|
112
|
+
document.addEventListener("click", (e) => {
|
|
113
|
+
if (
|
|
114
|
+
this.isHamburgerOpen &&
|
|
115
|
+
!this.hamburgerDropdown.contains(e.target as Node) &&
|
|
116
|
+
e.target !== this.hamburgerMenuButton
|
|
117
|
+
) {
|
|
118
|
+
this.closeHamburgerMenu();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
88
122
|
this.createModal();
|
|
89
123
|
}
|
|
90
124
|
|
|
125
|
+
private createHamburgerDropdown(hamburgerContainer: HTMLElement) {
|
|
126
|
+
this.hamburgerDropdown = document.createElement("div");
|
|
127
|
+
addClass(this.hamburgerDropdown, "hamburgerDropdown");
|
|
128
|
+
this.hamburgerDropdown.style.display = "none";
|
|
129
|
+
|
|
130
|
+
// Settings menu item
|
|
131
|
+
const settingsItem = document.createElement("button");
|
|
132
|
+
addClass(settingsItem, "hamburgerMenuItem");
|
|
133
|
+
settingsItem.innerHTML = '<i class="fas fa-cog"></i><span>Settings</span>';
|
|
134
|
+
settingsItem.onclick = () => {
|
|
135
|
+
this.open();
|
|
136
|
+
this.closeHamburgerMenu();
|
|
137
|
+
};
|
|
138
|
+
this.hamburgerDropdown.appendChild(settingsItem);
|
|
139
|
+
|
|
140
|
+
// Prefix menu item
|
|
141
|
+
const prefixItem = document.createElement("button");
|
|
142
|
+
addClass(prefixItem, "hamburgerMenuItem");
|
|
143
|
+
prefixItem.innerHTML = '<i class="fas fa-arrow-up-right-from-square"></i><span>Insert Prefixes</span>';
|
|
144
|
+
prefixItem.onclick = () => {
|
|
145
|
+
this.insertPrefixesIntoQuery();
|
|
146
|
+
this.closeHamburgerMenu();
|
|
147
|
+
};
|
|
148
|
+
this.hamburgerDropdown.appendChild(prefixItem);
|
|
149
|
+
|
|
150
|
+
// Theme toggle menu item (if enabled)
|
|
151
|
+
if (this.tab.yasgui.config.showThemeToggle) {
|
|
152
|
+
const themeItem = document.createElement("button");
|
|
153
|
+
addClass(themeItem, "hamburgerMenuItem", "themeMenuItem");
|
|
154
|
+
themeItem.innerHTML = this.getThemeToggleIcon() + "<span>Toggle Theme</span>";
|
|
155
|
+
themeItem.onclick = () => {
|
|
156
|
+
this.tab.yasgui.toggleTheme();
|
|
157
|
+
themeItem.innerHTML = this.getThemeToggleIcon() + "<span>Toggle Theme</span>";
|
|
158
|
+
// Keep menu open for theme toggle so user can see the change
|
|
159
|
+
};
|
|
160
|
+
this.hamburgerDropdown.appendChild(themeItem);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Layout orientation menu item
|
|
164
|
+
const layoutItem = document.createElement("button");
|
|
165
|
+
addClass(layoutItem, "hamburgerMenuItem", "layoutMenuItem");
|
|
166
|
+
this.updateLayoutMenuItemContent(layoutItem);
|
|
167
|
+
layoutItem.onclick = () => {
|
|
168
|
+
this.tab.toggleOrientation();
|
|
169
|
+
this.updateLayoutMenuItemContent(layoutItem);
|
|
170
|
+
// Keep menu open for layout toggle so user can see the change
|
|
171
|
+
};
|
|
172
|
+
this.hamburgerDropdown.appendChild(layoutItem);
|
|
173
|
+
|
|
174
|
+
hamburgerContainer.appendChild(this.hamburgerDropdown);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private updateLayoutMenuItemContent(layoutItem: HTMLButtonElement) {
|
|
178
|
+
const orientation = this.tab.getCurrentOrientation();
|
|
179
|
+
const icon =
|
|
180
|
+
orientation === "vertical" ? '<i class="fas fa-grip-lines-vertical"></i>' : '<i class="fas fa-grip-lines"></i>';
|
|
181
|
+
const label = orientation === "vertical" ? "Horizontal Layout" : "Vertical Layout";
|
|
182
|
+
layoutItem.innerHTML = icon + "<span>" + label + "</span>";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private toggleHamburgerMenu() {
|
|
186
|
+
if (this.isHamburgerOpen) {
|
|
187
|
+
this.closeHamburgerMenu();
|
|
188
|
+
} else {
|
|
189
|
+
this.openHamburgerMenu();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private openHamburgerMenu() {
|
|
194
|
+
this.isHamburgerOpen = true;
|
|
195
|
+
this.hamburgerDropdown.style.display = "block";
|
|
196
|
+
addClass(this.hamburgerMenuButton, "active");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private closeHamburgerMenu() {
|
|
200
|
+
this.isHamburgerOpen = false;
|
|
201
|
+
this.hamburgerDropdown.style.display = "none";
|
|
202
|
+
removeClass(this.hamburgerMenuButton, "active");
|
|
203
|
+
}
|
|
204
|
+
|
|
91
205
|
private createModal() {
|
|
92
206
|
// Modal overlay
|
|
93
207
|
this.modalOverlay = document.createElement("div");
|
package/src/endpointSelect.scss
CHANGED
|
@@ -99,28 +99,6 @@
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
.clearEndpointBtn {
|
|
103
|
-
border: 1px solid #d1d1d1;
|
|
104
|
-
background-color: #d1d1d1;
|
|
105
|
-
color: #505050;
|
|
106
|
-
border-radius: 3px;
|
|
107
|
-
cursor: pointer;
|
|
108
|
-
|
|
109
|
-
padding: 4px 8px;
|
|
110
|
-
margin: 4px;
|
|
111
|
-
|
|
112
|
-
display: flex;
|
|
113
|
-
align-items: center;
|
|
114
|
-
justify-content: center;
|
|
115
|
-
|
|
116
|
-
opacity: 1;
|
|
117
|
-
transition: opacity ease-in 200ms;
|
|
118
|
-
|
|
119
|
-
&:hover {
|
|
120
|
-
opacity: 0.8;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
102
|
.endpointButtonsContainer {
|
|
125
103
|
display: flex;
|
|
126
104
|
align-items: center;
|
package/src/endpointSelect.ts
CHANGED
|
@@ -92,7 +92,7 @@ export class EndpointSelect extends EventEmitter {
|
|
|
92
92
|
// Create clear button
|
|
93
93
|
const clearBtn = document.createElement("button");
|
|
94
94
|
clearBtn.title = "Clear endpoint";
|
|
95
|
-
addClass(clearBtn, "
|
|
95
|
+
addClass(clearBtn, "tabContextButton");
|
|
96
96
|
clearBtn.innerText = "✖";
|
|
97
97
|
clearBtn.addEventListener("click", () => {
|
|
98
98
|
this.inputField.value = "";
|
|
@@ -8,6 +8,7 @@ import { getEndpointToAutoSwitch } from "./openManagedQuery";
|
|
|
8
8
|
import { hashQueryText } from "./textHash";
|
|
9
9
|
import type { BackendType, VersionRef, ManagedTabMetadata } from "./types";
|
|
10
10
|
import { normalizeQueryFilename } from "./normalizeQueryFilename";
|
|
11
|
+
import SaveManagedQueryModal from "./SaveManagedQueryModal";
|
|
11
12
|
|
|
12
13
|
import "./QueryBrowser.scss";
|
|
13
14
|
|
|
@@ -47,6 +48,7 @@ export default class QueryBrowser {
|
|
|
47
48
|
private lastRenderedSignature: string | undefined;
|
|
48
49
|
|
|
49
50
|
private lastPointerPos: { x: number; y: number } | undefined;
|
|
51
|
+
private folderPickerModal?: SaveManagedQueryModal;
|
|
50
52
|
|
|
51
53
|
private entrySignature(entry: FolderEntry): string {
|
|
52
54
|
const parent = entry.parentId || "";
|
|
@@ -712,6 +714,76 @@ export default class QueryBrowser {
|
|
|
712
714
|
actions.appendChild(renameBtn);
|
|
713
715
|
}
|
|
714
716
|
|
|
717
|
+
if (backend.moveQuery) {
|
|
718
|
+
const moveBtn = document.createElement("button");
|
|
719
|
+
moveBtn.type = "button";
|
|
720
|
+
addClass(moveBtn, "yasgui-query-browser__action");
|
|
721
|
+
moveBtn.textContent = "Move";
|
|
722
|
+
moveBtn.setAttribute("aria-label", `Move ${entry.label} to a different folder`);
|
|
723
|
+
moveBtn.addEventListener("click", async (e) => {
|
|
724
|
+
e.preventDefault();
|
|
725
|
+
e.stopPropagation();
|
|
726
|
+
|
|
727
|
+
const currentFolderPath = entry.parentId || "";
|
|
728
|
+
if (!this.folderPickerModal) this.folderPickerModal = new SaveManagedQueryModal(this.yasgui);
|
|
729
|
+
const newFolderPath = await this.folderPickerModal.showFolderPickerOnly(
|
|
730
|
+
this.selectedWorkspaceId!,
|
|
731
|
+
currentFolderPath,
|
|
732
|
+
);
|
|
733
|
+
if (newFolderPath === undefined) return;
|
|
734
|
+
if (newFolderPath === currentFolderPath) return;
|
|
735
|
+
|
|
736
|
+
// Show loading state
|
|
737
|
+
const originalText = moveBtn.textContent;
|
|
738
|
+
moveBtn.disabled = true;
|
|
739
|
+
moveBtn.textContent = "Moving…";
|
|
740
|
+
addClass(moveBtn, "loading");
|
|
741
|
+
|
|
742
|
+
try {
|
|
743
|
+
const newQueryId = await backend.moveQuery!(entry.id, newFolderPath);
|
|
744
|
+
|
|
745
|
+
// For git workspaces: update any already-open managed tabs referencing the old path.
|
|
746
|
+
if (backend.type === "git" && newQueryId !== entry.id) {
|
|
747
|
+
for (const tab of Object.values(this.yasgui._tabs)) {
|
|
748
|
+
const meta = (tab as any).getManagedQueryMetadata?.() as ManagedTabMetadata | undefined;
|
|
749
|
+
if (!meta) continue;
|
|
750
|
+
if (meta.backendType !== "git") continue;
|
|
751
|
+
if (meta.workspaceId !== this.selectedWorkspaceId) continue;
|
|
752
|
+
const currentPath = (meta.queryRef as any)?.path as string | undefined;
|
|
753
|
+
if (currentPath !== entry.id) continue;
|
|
754
|
+
|
|
755
|
+
try {
|
|
756
|
+
const read = await backend.readQuery(newQueryId);
|
|
757
|
+
const lastSavedTextHash = hashQueryText(read.queryText);
|
|
758
|
+
const lastSavedVersionRef = this.versionRefFromVersionTag("git", read.versionTag);
|
|
759
|
+
|
|
760
|
+
(tab as any).setManagedQueryMetadata?.({
|
|
761
|
+
...meta,
|
|
762
|
+
queryRef: { ...(meta.queryRef as any), path: newQueryId },
|
|
763
|
+
lastSavedTextHash,
|
|
764
|
+
lastSavedVersionRef,
|
|
765
|
+
});
|
|
766
|
+
} catch {
|
|
767
|
+
// Best-effort: if refreshing metadata fails, the Query Browser still reflects the move.
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
this.queryPreviewById.delete(entry.id);
|
|
773
|
+
this.folderEntriesById.clear();
|
|
774
|
+
this.invalidateRenderCache();
|
|
775
|
+
await this.refresh();
|
|
776
|
+
} catch (err) {
|
|
777
|
+
// Restore button state on error
|
|
778
|
+
moveBtn.disabled = false;
|
|
779
|
+
moveBtn.textContent = originalText || "Move";
|
|
780
|
+
removeClass(moveBtn, "loading");
|
|
781
|
+
window.alert(asWorkspaceBackendError(err).message);
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
actions.appendChild(moveBtn);
|
|
785
|
+
}
|
|
786
|
+
|
|
715
787
|
if (backend.deleteQuery) {
|
|
716
788
|
const deleteBtn = document.createElement("button");
|
|
717
789
|
deleteBtn.type = "button";
|