@matdata/yasgui 5.17.0 → 5.18.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.js +6 -0
- package/build/ts/src/Tab.js.map +1 -1
- package/build/ts/src/TabContextMenu.d.ts +1 -0
- package/build/ts/src/TabContextMenu.js +42 -0
- package/build/ts/src/TabContextMenu.js.map +1 -1
- package/build/ts/src/TabSettingsModal.d.ts +0 -1
- package/build/ts/src/TabSettingsModal.js +22 -25
- package/build/ts/src/TabSettingsModal.js.map +1 -1
- package/build/ts/src/index.d.ts +6 -0
- package/build/ts/src/index.js +42 -0
- package/build/ts/src/index.js.map +1 -1
- package/build/ts/src/prefixUtils.d.ts +1 -0
- package/build/ts/src/prefixUtils.js +23 -0
- package/build/ts/src/prefixUtils.js.map +1 -0
- package/build/ts/src/queryManagement/QueryBrowser.d.ts +8 -0
- package/build/ts/src/queryManagement/QueryBrowser.js +283 -116
- package/build/ts/src/queryManagement/QueryBrowser.js.map +1 -1
- package/build/ts/src/queryManagement/QueryHistoryModal.d.ts +35 -0
- package/build/ts/src/queryManagement/QueryHistoryModal.js +266 -0
- package/build/ts/src/queryManagement/QueryHistoryModal.js.map +1 -0
- package/build/ts/src/queryManagement/WorkspaceSettingsForm.js +8 -5
- package/build/ts/src/queryManagement/WorkspaceSettingsForm.js.map +1 -1
- package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.d.ts +1 -0
- package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.js +33 -0
- package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.js.map +1 -1
- package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.d.ts +1 -0
- package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.js +3 -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/queryManagement/countManagedQueries.d.ts +2 -0
- package/build/ts/src/queryManagement/countManagedQueries.js +35 -0
- package/build/ts/src/queryManagement/countManagedQueries.js.map +1 -0
- package/build/ts/src/tabNavigationHistory.d.ts +3 -0
- package/build/ts/src/tabNavigationHistory.js +13 -0
- package/build/ts/src/tabNavigationHistory.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 +1 -1
- package/build/yasgui.min.css.map +3 -3
- package/build/yasgui.min.js +169 -169
- package/build/yasgui.min.js.map +4 -4
- package/package.json +1 -1
- package/src/Tab.ts +9 -0
- package/src/TabContextMenu.ts +42 -0
- package/src/TabSettingsModal.ts +22 -29
- package/src/index.ts +50 -0
- package/src/prefixUtils.ts +26 -0
- package/src/queryManagement/QueryBrowser.scss +85 -0
- package/src/queryManagement/QueryBrowser.ts +429 -213
- package/src/queryManagement/QueryHistoryModal.scss +239 -0
- package/src/queryManagement/QueryHistoryModal.ts +346 -0
- package/src/queryManagement/WorkspaceSettingsForm.ts +8 -5
- package/src/queryManagement/backends/GitWorkspaceBackend.ts +39 -0
- package/src/queryManagement/backends/SparqlWorkspaceBackend.ts +5 -0
- package/src/queryManagement/backends/WorkspaceBackend.ts +8 -0
- package/src/queryManagement/countManagedQueries.ts +28 -0
- package/src/tabNavigationHistory.ts +17 -0
- package/src/themes.scss +2 -0
- package/src/version.ts +1 -1
|
@@ -9,6 +9,7 @@ import { hashQueryText } from "./textHash";
|
|
|
9
9
|
import type { BackendType, VersionRef, ManagedTabMetadata } from "./types";
|
|
10
10
|
import { normalizeQueryFilename } from "./normalizeQueryFilename";
|
|
11
11
|
import SaveManagedQueryModal from "./SaveManagedQueryModal";
|
|
12
|
+
import QueryHistoryModal from "./QueryHistoryModal";
|
|
12
13
|
|
|
13
14
|
import "./QueryBrowser.scss";
|
|
14
15
|
|
|
@@ -50,6 +51,11 @@ export default class QueryBrowser {
|
|
|
50
51
|
private lastPointerPos: { x: number; y: number } | undefined;
|
|
51
52
|
private folderPickerModal?: SaveManagedQueryModal;
|
|
52
53
|
|
|
54
|
+
private queryContextMenuEl?: HTMLElement;
|
|
55
|
+
private queryContextMenuCleanup?: () => void;
|
|
56
|
+
|
|
57
|
+
private queryHistoryModal?: QueryHistoryModal;
|
|
58
|
+
|
|
53
59
|
private entrySignature(entry: FolderEntry): string {
|
|
54
60
|
const parent = entry.parentId || "";
|
|
55
61
|
// Include label + parent so renames/moves force a re-render.
|
|
@@ -235,6 +241,7 @@ export default class QueryBrowser {
|
|
|
235
241
|
removeClass(this.rootEl, "open");
|
|
236
242
|
this.rootEl.setAttribute("aria-hidden", "true");
|
|
237
243
|
this.rootEl.style.display = "none";
|
|
244
|
+
this.closeQueryContextMenu();
|
|
238
245
|
|
|
239
246
|
if (this.openerEl) {
|
|
240
247
|
this.openerEl.focus();
|
|
@@ -571,258 +578,467 @@ export default class QueryBrowser {
|
|
|
571
578
|
tab.setManagedQueryMetadata(managedMetadata);
|
|
572
579
|
}
|
|
573
580
|
|
|
574
|
-
private
|
|
575
|
-
|
|
576
|
-
|
|
581
|
+
private closeQueryContextMenu() {
|
|
582
|
+
if (this.queryContextMenuEl) {
|
|
583
|
+
this.queryContextMenuEl.remove();
|
|
584
|
+
this.queryContextMenuEl = undefined;
|
|
585
|
+
}
|
|
586
|
+
if (this.queryContextMenuCleanup) {
|
|
587
|
+
this.queryContextMenuCleanup();
|
|
588
|
+
this.queryContextMenuCleanup = undefined;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
577
591
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
592
|
+
private startInlineRename(row: HTMLElement, currentLabel: string): Promise<string | null> {
|
|
593
|
+
return new Promise((resolve) => {
|
|
594
|
+
const labelEl = row.querySelector<HTMLElement>(".yasgui-query-browser__tree-label");
|
|
595
|
+
if (!labelEl) {
|
|
596
|
+
resolve(null);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const input = document.createElement("input");
|
|
601
|
+
input.type = "text";
|
|
602
|
+
input.value = currentLabel;
|
|
603
|
+
addClass(input, "yasgui-query-browser__rename-input");
|
|
604
|
+
|
|
605
|
+
let settled = false;
|
|
606
|
+
const finish = (value: string | null) => {
|
|
607
|
+
if (settled) return;
|
|
608
|
+
settled = true;
|
|
609
|
+
input.remove();
|
|
610
|
+
labelEl.style.display = "";
|
|
611
|
+
resolve(value);
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
input.addEventListener("click", (e) => e.stopPropagation());
|
|
615
|
+
input.addEventListener("keydown", (e) => {
|
|
616
|
+
e.stopPropagation();
|
|
617
|
+
if (e.key === "Enter") {
|
|
618
|
+
e.preventDefault();
|
|
619
|
+
finish(input.value.trim() || null);
|
|
620
|
+
} else if (e.key === "Escape") {
|
|
586
621
|
e.preventDefault();
|
|
587
|
-
|
|
622
|
+
finish(null);
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
input.addEventListener("blur", () => finish(null));
|
|
626
|
+
|
|
627
|
+
labelEl.style.display = "none";
|
|
628
|
+
labelEl.insertAdjacentElement("afterend", input);
|
|
629
|
+
input.focus();
|
|
630
|
+
input.select();
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
private showInlineConfirm(row: HTMLElement, message: string): Promise<boolean> {
|
|
635
|
+
return new Promise((resolve) => {
|
|
636
|
+
const savedChildren = Array.from(row.childNodes);
|
|
637
|
+
while (row.firstChild) row.removeChild(row.firstChild);
|
|
638
|
+
|
|
639
|
+
const confirmEl = document.createElement("span");
|
|
640
|
+
addClass(confirmEl, "yasgui-query-browser__inline-confirm");
|
|
641
|
+
confirmEl.textContent = message;
|
|
642
|
+
confirmEl.addEventListener("click", (e) => e.stopPropagation());
|
|
643
|
+
|
|
644
|
+
const yes = document.createElement("button");
|
|
645
|
+
yes.type = "button";
|
|
646
|
+
yes.textContent = "Yes";
|
|
647
|
+
addClass(yes, "yasgui-query-browser__inline-confirm-btn");
|
|
648
|
+
addClass(yes, "yasgui-query-browser__inline-confirm-btn--danger");
|
|
649
|
+
|
|
650
|
+
const no = document.createElement("button");
|
|
651
|
+
no.type = "button";
|
|
652
|
+
no.textContent = "No";
|
|
653
|
+
addClass(no, "yasgui-query-browser__inline-confirm-btn");
|
|
654
|
+
|
|
655
|
+
let settled = false;
|
|
656
|
+
const finish = (value: boolean) => {
|
|
657
|
+
if (settled) return;
|
|
658
|
+
settled = true;
|
|
659
|
+
while (row.firstChild) row.removeChild(row.firstChild);
|
|
660
|
+
for (const child of savedChildren) row.appendChild(child);
|
|
661
|
+
resolve(value);
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
yes.addEventListener("click", (e) => {
|
|
665
|
+
e.stopPropagation();
|
|
666
|
+
finish(true);
|
|
667
|
+
});
|
|
668
|
+
no.addEventListener("click", (e) => {
|
|
669
|
+
e.stopPropagation();
|
|
670
|
+
finish(false);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
row.appendChild(confirmEl);
|
|
674
|
+
row.appendChild(document.createTextNode(" "));
|
|
675
|
+
row.appendChild(yes);
|
|
676
|
+
row.appendChild(document.createTextNode(" "));
|
|
677
|
+
row.appendChild(no);
|
|
678
|
+
no.focus();
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
private openQueryContextMenu(
|
|
683
|
+
event: MouseEvent,
|
|
684
|
+
backend: ReturnType<typeof getWorkspaceBackend>,
|
|
685
|
+
entry: FolderEntry,
|
|
686
|
+
row: HTMLElement,
|
|
687
|
+
) {
|
|
688
|
+
event.preventDefault();
|
|
689
|
+
event.stopPropagation();
|
|
690
|
+
|
|
691
|
+
this.closeQueryContextMenu();
|
|
692
|
+
|
|
693
|
+
const menu = document.createElement("ul");
|
|
694
|
+
addClass(menu, "yasgui-query-browser__context-menu");
|
|
695
|
+
|
|
696
|
+
const makeItem = (label: string, handler: () => void | Promise<void>, isDanger = false) => {
|
|
697
|
+
const li = document.createElement("li");
|
|
698
|
+
addClass(li, "yasgui-query-browser__context-menu-item");
|
|
699
|
+
if (isDanger) addClass(li, "yasgui-query-browser__context-menu-item--danger");
|
|
700
|
+
li.textContent = label;
|
|
701
|
+
li.addEventListener("click", async (e) => {
|
|
702
|
+
e.stopPropagation();
|
|
703
|
+
this.closeQueryContextMenu();
|
|
704
|
+
await handler();
|
|
705
|
+
});
|
|
706
|
+
return li;
|
|
707
|
+
};
|
|
588
708
|
|
|
589
|
-
|
|
709
|
+
// Copy URI
|
|
710
|
+
if (backend.getQueryUri) {
|
|
711
|
+
const uri = backend.getQueryUri(entry.id);
|
|
712
|
+
if (uri) {
|
|
713
|
+
menu.appendChild(
|
|
714
|
+
makeItem("Copy URI", async () => {
|
|
715
|
+
try {
|
|
716
|
+
await navigator.clipboard.writeText(uri);
|
|
717
|
+
} catch {
|
|
718
|
+
window.prompt("Copy this URI:", uri);
|
|
719
|
+
}
|
|
720
|
+
}),
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// History
|
|
726
|
+
menu.appendChild(
|
|
727
|
+
makeItem("History", async () => {
|
|
728
|
+
if (!this.queryHistoryModal) this.queryHistoryModal = new QueryHistoryModal(this.yasgui);
|
|
729
|
+
await this.queryHistoryModal.show(this.selectedWorkspaceId!, backend, backend.type, entry.id, entry.label);
|
|
730
|
+
}),
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
// Rename
|
|
734
|
+
if (backend.renameQuery) {
|
|
735
|
+
const renameQuery = backend.renameQuery.bind(backend);
|
|
736
|
+
menu.appendChild(
|
|
737
|
+
makeItem("Rename", async () => {
|
|
738
|
+
const next = await this.startInlineRename(row, entry.label);
|
|
590
739
|
if (!next) return;
|
|
591
740
|
const trimmed = next.trim();
|
|
592
741
|
if (!trimmed || trimmed === entry.label) return;
|
|
593
742
|
|
|
743
|
+
const gitRenameInfo = (() => {
|
|
744
|
+
if (backend.type !== "git") return undefined;
|
|
745
|
+
const parts = entry.id.split("/").filter(Boolean);
|
|
746
|
+
parts.pop();
|
|
747
|
+
const folderPrefix = parts.join("/");
|
|
748
|
+
const safe = trimmed.replace(/[\\/]/g, "-");
|
|
749
|
+
const newFilename = normalizeQueryFilename(safe);
|
|
750
|
+
const newPath = folderPrefix ? `${folderPrefix}/${newFilename}` : newFilename;
|
|
751
|
+
return { oldPath: entry.id, newPath };
|
|
752
|
+
})();
|
|
753
|
+
|
|
594
754
|
try {
|
|
595
|
-
await
|
|
755
|
+
await renameQuery(entry.id, trimmed);
|
|
756
|
+
|
|
757
|
+
if (gitRenameInfo && gitRenameInfo.newPath && gitRenameInfo.oldPath) {
|
|
758
|
+
for (const tab of Object.values(this.yasgui._tabs)) {
|
|
759
|
+
const meta = (tab as any).getManagedQueryMetadata?.() as ManagedTabMetadata | undefined;
|
|
760
|
+
if (!meta) continue;
|
|
761
|
+
if (meta.backendType !== "git") continue;
|
|
762
|
+
if (meta.workspaceId !== this.selectedWorkspaceId) continue;
|
|
763
|
+
const currentPath = (meta.queryRef as any)?.path as string | undefined;
|
|
764
|
+
if (currentPath !== gitRenameInfo.oldPath) continue;
|
|
765
|
+
|
|
766
|
+
try {
|
|
767
|
+
const read = await backend.readQuery(gitRenameInfo.newPath);
|
|
768
|
+
const lastSavedTextHash = hashQueryText(read.queryText);
|
|
769
|
+
const lastSavedVersionRef = this.versionRefFromVersionTag("git", read.versionTag);
|
|
770
|
+
|
|
771
|
+
(tab as any).setManagedQueryMetadata?.({
|
|
772
|
+
...meta,
|
|
773
|
+
queryRef: { ...(meta.queryRef as any), path: gitRenameInfo.newPath },
|
|
774
|
+
lastSavedTextHash,
|
|
775
|
+
lastSavedVersionRef,
|
|
776
|
+
});
|
|
777
|
+
(tab as any).setName?.(trimmed);
|
|
778
|
+
} catch {
|
|
779
|
+
// Best-effort: if refreshing metadata fails, the Query Browser still reflects the rename.
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
this.queryPreviewById.delete(entry.id);
|
|
596
785
|
this.folderEntriesById.clear();
|
|
597
786
|
this.invalidateRenderCache();
|
|
598
787
|
await this.refresh();
|
|
599
788
|
} catch (err) {
|
|
600
|
-
|
|
789
|
+
this.setStatus(asWorkspaceBackendError(err).message);
|
|
601
790
|
}
|
|
602
|
-
})
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
if (backend.deleteFolder) {
|
|
607
|
-
const deleteBtn = document.createElement("button");
|
|
608
|
-
deleteBtn.type = "button";
|
|
609
|
-
addClass(deleteBtn, "yasgui-query-browser__action");
|
|
610
|
-
addClass(deleteBtn, "yasgui-query-browser__action--danger");
|
|
611
|
-
deleteBtn.textContent = "Delete";
|
|
612
|
-
deleteBtn.setAttribute("aria-label", `Delete folder ${entry.label}`);
|
|
613
|
-
deleteBtn.addEventListener("click", async (e) => {
|
|
614
|
-
e.preventDefault();
|
|
615
|
-
e.stopPropagation();
|
|
791
|
+
}),
|
|
792
|
+
);
|
|
793
|
+
}
|
|
616
794
|
|
|
617
|
-
|
|
618
|
-
|
|
795
|
+
// Move
|
|
796
|
+
if (backend.moveQuery) {
|
|
797
|
+
const moveQuery = backend.moveQuery.bind(backend);
|
|
798
|
+
menu.appendChild(
|
|
799
|
+
makeItem("Move", async () => {
|
|
800
|
+
const currentFolderPath = entry.parentId || "";
|
|
801
|
+
if (!this.folderPickerModal) this.folderPickerModal = new SaveManagedQueryModal(this.yasgui);
|
|
802
|
+
const newFolderPath = await this.folderPickerModal.showFolderPickerOnly(
|
|
803
|
+
this.selectedWorkspaceId!,
|
|
804
|
+
currentFolderPath,
|
|
805
|
+
);
|
|
806
|
+
if (newFolderPath === undefined) return;
|
|
807
|
+
if (newFolderPath === currentFolderPath) return;
|
|
619
808
|
|
|
620
809
|
try {
|
|
621
|
-
await
|
|
810
|
+
const newQueryId = await moveQuery(entry.id, newFolderPath);
|
|
811
|
+
|
|
812
|
+
if (backend.type === "git" && newQueryId !== entry.id) {
|
|
813
|
+
for (const tab of Object.values(this.yasgui._tabs)) {
|
|
814
|
+
const meta = (tab as any).getManagedQueryMetadata?.() as ManagedTabMetadata | undefined;
|
|
815
|
+
if (!meta) continue;
|
|
816
|
+
if (meta.backendType !== "git") continue;
|
|
817
|
+
if (meta.workspaceId !== this.selectedWorkspaceId) continue;
|
|
818
|
+
const currentPath = (meta.queryRef as any)?.path as string | undefined;
|
|
819
|
+
if (currentPath !== entry.id) continue;
|
|
820
|
+
|
|
821
|
+
try {
|
|
822
|
+
const read = await backend.readQuery(newQueryId);
|
|
823
|
+
const lastSavedTextHash = hashQueryText(read.queryText);
|
|
824
|
+
const lastSavedVersionRef = this.versionRefFromVersionTag("git", read.versionTag);
|
|
825
|
+
|
|
826
|
+
(tab as any).setManagedQueryMetadata?.({
|
|
827
|
+
...meta,
|
|
828
|
+
queryRef: { ...(meta.queryRef as any), path: newQueryId },
|
|
829
|
+
lastSavedTextHash,
|
|
830
|
+
lastSavedVersionRef,
|
|
831
|
+
});
|
|
832
|
+
} catch {
|
|
833
|
+
// Best-effort: if refreshing metadata fails, the Query Browser still reflects the move.
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
this.queryPreviewById.delete(entry.id);
|
|
622
839
|
this.folderEntriesById.clear();
|
|
623
840
|
this.invalidateRenderCache();
|
|
624
841
|
await this.refresh();
|
|
625
842
|
} catch (err) {
|
|
626
|
-
|
|
843
|
+
this.setStatus(asWorkspaceBackendError(err).message);
|
|
627
844
|
}
|
|
628
|
-
})
|
|
629
|
-
|
|
630
|
-
|
|
845
|
+
}),
|
|
846
|
+
);
|
|
847
|
+
}
|
|
631
848
|
|
|
632
|
-
|
|
633
|
-
|
|
849
|
+
// Delete
|
|
850
|
+
if (backend.deleteQuery) {
|
|
851
|
+
const deleteQuery = backend.deleteQuery.bind(backend);
|
|
852
|
+
menu.appendChild(
|
|
853
|
+
makeItem(
|
|
854
|
+
"Delete",
|
|
855
|
+
async () => {
|
|
856
|
+
const ok = await this.showInlineConfirm(row, `Delete '${entry.label}'?`);
|
|
857
|
+
if (!ok) return;
|
|
858
|
+
|
|
859
|
+
try {
|
|
860
|
+
await deleteQuery(entry.id);
|
|
861
|
+
this.queryPreviewById.delete(entry.id);
|
|
862
|
+
this.folderEntriesById.clear();
|
|
863
|
+
this.invalidateRenderCache();
|
|
864
|
+
await this.refresh();
|
|
865
|
+
} catch (err) {
|
|
866
|
+
this.setStatus(asWorkspaceBackendError(err).message);
|
|
867
|
+
}
|
|
868
|
+
},
|
|
869
|
+
true,
|
|
870
|
+
),
|
|
871
|
+
);
|
|
634
872
|
}
|
|
635
873
|
|
|
636
|
-
if (
|
|
874
|
+
if (!menu.childElementCount) return;
|
|
875
|
+
|
|
876
|
+
this.rootEl.appendChild(menu);
|
|
877
|
+
this.queryContextMenuEl = menu;
|
|
878
|
+
|
|
879
|
+
// Position near the cursor, clamping to the viewport.
|
|
880
|
+
const VIEWPORT_PADDING = 4;
|
|
881
|
+
menu.style.position = "fixed";
|
|
882
|
+
menu.style.zIndex = "10001";
|
|
883
|
+
menu.style.left = `${event.clientX}px`;
|
|
884
|
+
menu.style.top = `${event.clientY}px`;
|
|
885
|
+
requestAnimationFrame(() => {
|
|
886
|
+
const rect = menu.getBoundingClientRect();
|
|
887
|
+
if (rect.right > window.innerWidth - VIEWPORT_PADDING) {
|
|
888
|
+
menu.style.left = `${Math.max(VIEWPORT_PADDING, event.clientX - rect.width)}px`;
|
|
889
|
+
}
|
|
890
|
+
if (rect.bottom > window.innerHeight - VIEWPORT_PADDING) {
|
|
891
|
+
menu.style.top = `${Math.max(VIEWPORT_PADDING, event.clientY - rect.height)}px`;
|
|
892
|
+
}
|
|
893
|
+
});
|
|
637
894
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
renameBtn.addEventListener("click", async (e) => {
|
|
645
|
-
e.preventDefault();
|
|
895
|
+
const onOutsideClick = (e: MouseEvent) => {
|
|
896
|
+
if (!this.queryContextMenuEl) return;
|
|
897
|
+
if (!menu.contains(e.target as Node)) this.closeQueryContextMenu();
|
|
898
|
+
};
|
|
899
|
+
const onEsc = (e: KeyboardEvent) => {
|
|
900
|
+
if (e.key === "Escape") {
|
|
646
901
|
e.stopPropagation();
|
|
902
|
+
this.closeQueryContextMenu();
|
|
903
|
+
}
|
|
904
|
+
};
|
|
647
905
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
// also update any already-open managed tabs that reference this query.
|
|
655
|
-
const gitRenameInfo = (() => {
|
|
656
|
-
if (backend.type !== "git") return undefined;
|
|
657
|
-
const parts = entry.id.split("/").filter(Boolean);
|
|
658
|
-
parts.pop();
|
|
659
|
-
const folderPrefix = parts.join("/");
|
|
660
|
-
const safe = trimmed.replace(/[\\/]/g, "-");
|
|
661
|
-
const newFilename = normalizeQueryFilename(safe);
|
|
662
|
-
const newPath = folderPrefix ? `${folderPrefix}/${newFilename}` : newFilename;
|
|
663
|
-
return { oldPath: entry.id, newPath };
|
|
664
|
-
})();
|
|
665
|
-
|
|
666
|
-
// Show loading state
|
|
667
|
-
const originalText = renameBtn.textContent;
|
|
668
|
-
renameBtn.disabled = true;
|
|
669
|
-
renameBtn.textContent = "Renaming…";
|
|
670
|
-
addClass(renameBtn, "loading");
|
|
671
|
-
|
|
672
|
-
try {
|
|
673
|
-
await backend.renameQuery!(entry.id, trimmed);
|
|
674
|
-
|
|
675
|
-
if (gitRenameInfo && gitRenameInfo.newPath && gitRenameInfo.oldPath) {
|
|
676
|
-
for (const tab of Object.values(this.yasgui._tabs)) {
|
|
677
|
-
const meta = (tab as any).getManagedQueryMetadata?.() as ManagedTabMetadata | undefined;
|
|
678
|
-
if (!meta) continue;
|
|
679
|
-
if (meta.backendType !== "git") continue;
|
|
680
|
-
if (meta.workspaceId !== this.selectedWorkspaceId) continue;
|
|
681
|
-
const currentPath = (meta.queryRef as any)?.path as string | undefined;
|
|
682
|
-
if (currentPath !== gitRenameInfo.oldPath) continue;
|
|
683
|
-
|
|
684
|
-
try {
|
|
685
|
-
const read = await backend.readQuery(gitRenameInfo.newPath);
|
|
686
|
-
const lastSavedTextHash = hashQueryText(read.queryText);
|
|
687
|
-
const lastSavedVersionRef = this.versionRefFromVersionTag("git", read.versionTag);
|
|
688
|
-
|
|
689
|
-
(tab as any).setManagedQueryMetadata?.({
|
|
690
|
-
...meta,
|
|
691
|
-
queryRef: { ...(meta.queryRef as any), path: gitRenameInfo.newPath },
|
|
692
|
-
lastSavedTextHash,
|
|
693
|
-
lastSavedVersionRef,
|
|
694
|
-
});
|
|
695
|
-
(tab as any).setName?.(trimmed);
|
|
696
|
-
} catch {
|
|
697
|
-
// Best-effort: if refreshing metadata fails, the Query Browser still reflects the rename.
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
}
|
|
906
|
+
// Use requestAnimationFrame to defer listener registration past the current event cycle,
|
|
907
|
+
// ensuring the contextmenu event that opened the menu is not immediately caught.
|
|
908
|
+
requestAnimationFrame(() => {
|
|
909
|
+
document.addEventListener("click", onOutsideClick);
|
|
910
|
+
document.addEventListener("keydown", onEsc);
|
|
911
|
+
});
|
|
701
912
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
// Restore button state on error
|
|
708
|
-
renameBtn.disabled = false;
|
|
709
|
-
renameBtn.textContent = originalText || "Rename";
|
|
710
|
-
removeClass(renameBtn, "loading");
|
|
711
|
-
window.alert(asWorkspaceBackendError(err).message);
|
|
712
|
-
}
|
|
713
|
-
});
|
|
714
|
-
actions.appendChild(renameBtn);
|
|
715
|
-
}
|
|
913
|
+
this.queryContextMenuCleanup = () => {
|
|
914
|
+
document.removeEventListener("click", onOutsideClick);
|
|
915
|
+
document.removeEventListener("keydown", onEsc);
|
|
916
|
+
};
|
|
917
|
+
}
|
|
716
918
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
919
|
+
private openFolderContextMenu(
|
|
920
|
+
event: MouseEvent,
|
|
921
|
+
backend: ReturnType<typeof getWorkspaceBackend>,
|
|
922
|
+
entry: FolderEntry,
|
|
923
|
+
row: HTMLElement,
|
|
924
|
+
) {
|
|
925
|
+
event.preventDefault();
|
|
926
|
+
event.stopPropagation();
|
|
927
|
+
|
|
928
|
+
this.closeQueryContextMenu();
|
|
929
|
+
|
|
930
|
+
const menu = document.createElement("ul");
|
|
931
|
+
addClass(menu, "yasgui-query-browser__context-menu");
|
|
932
|
+
|
|
933
|
+
const makeItem = (label: string, handler: () => void | Promise<void>, isDanger = false) => {
|
|
934
|
+
const li = document.createElement("li");
|
|
935
|
+
addClass(li, "yasgui-query-browser__context-menu-item");
|
|
936
|
+
if (isDanger) addClass(li, "yasgui-query-browser__context-menu-item--danger");
|
|
937
|
+
li.textContent = label;
|
|
938
|
+
li.addEventListener("click", async (e) => {
|
|
725
939
|
e.stopPropagation();
|
|
940
|
+
this.closeQueryContextMenu();
|
|
941
|
+
await handler();
|
|
942
|
+
});
|
|
943
|
+
return li;
|
|
944
|
+
};
|
|
726
945
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
this.
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
-
}
|
|
946
|
+
if (backend.renameFolder) {
|
|
947
|
+
menu.appendChild(
|
|
948
|
+
makeItem("Rename", async () => {
|
|
949
|
+
const next = await this.startInlineRename(row, entry.label);
|
|
950
|
+
if (!next) return;
|
|
951
|
+
const trimmed = next.trim();
|
|
952
|
+
if (!trimmed || trimmed === entry.label) return;
|
|
953
|
+
|
|
954
|
+
try {
|
|
955
|
+
await backend.renameFolder!(entry.id, trimmed);
|
|
956
|
+
this.folderEntriesById.clear();
|
|
957
|
+
this.invalidateRenderCache();
|
|
958
|
+
await this.refresh();
|
|
959
|
+
} catch (err) {
|
|
960
|
+
this.setStatus(asWorkspaceBackendError(err).message);
|
|
770
961
|
}
|
|
962
|
+
}),
|
|
963
|
+
);
|
|
964
|
+
}
|
|
771
965
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
966
|
+
if (backend.deleteFolder) {
|
|
967
|
+
menu.appendChild(
|
|
968
|
+
makeItem(
|
|
969
|
+
"Delete",
|
|
970
|
+
async () => {
|
|
971
|
+
const ok = await this.showInlineConfirm(row, `Delete folder '${entry.label}' and everything inside it?`);
|
|
972
|
+
if (!ok) return;
|
|
973
|
+
|
|
974
|
+
try {
|
|
975
|
+
await backend.deleteFolder!(entry.id);
|
|
976
|
+
this.folderEntriesById.clear();
|
|
977
|
+
this.invalidateRenderCache();
|
|
978
|
+
await this.refresh();
|
|
979
|
+
} catch (err) {
|
|
980
|
+
this.setStatus(asWorkspaceBackendError(err).message);
|
|
981
|
+
}
|
|
982
|
+
},
|
|
983
|
+
true,
|
|
984
|
+
),
|
|
985
|
+
);
|
|
785
986
|
}
|
|
786
987
|
|
|
787
|
-
if (
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
988
|
+
if (!menu.childElementCount) return;
|
|
989
|
+
|
|
990
|
+
this.rootEl.appendChild(menu);
|
|
991
|
+
this.queryContextMenuEl = menu;
|
|
992
|
+
|
|
993
|
+
const VIEWPORT_PADDING = 4;
|
|
994
|
+
menu.style.position = "fixed";
|
|
995
|
+
menu.style.zIndex = "10001";
|
|
996
|
+
menu.style.left = `${event.clientX}px`;
|
|
997
|
+
menu.style.top = `${event.clientY}px`;
|
|
998
|
+
requestAnimationFrame(() => {
|
|
999
|
+
const rect = menu.getBoundingClientRect();
|
|
1000
|
+
if (rect.right > window.innerWidth - VIEWPORT_PADDING) {
|
|
1001
|
+
menu.style.left = `${Math.max(VIEWPORT_PADDING, event.clientX - rect.width)}px`;
|
|
1002
|
+
}
|
|
1003
|
+
if (rect.bottom > window.innerHeight - VIEWPORT_PADDING) {
|
|
1004
|
+
menu.style.top = `${Math.max(VIEWPORT_PADDING, event.clientY - rect.height)}px`;
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
const onOutsideClick = (e: MouseEvent) => {
|
|
1009
|
+
if (!this.queryContextMenuEl) return;
|
|
1010
|
+
if (!menu.contains(e.target as Node)) this.closeQueryContextMenu();
|
|
1011
|
+
};
|
|
1012
|
+
const onEsc = (e: KeyboardEvent) => {
|
|
1013
|
+
if (e.key === "Escape") {
|
|
796
1014
|
e.stopPropagation();
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
removeClass(deleteBtn, "loading");
|
|
817
|
-
window.alert(asWorkspaceBackendError(err).message);
|
|
818
|
-
}
|
|
1015
|
+
this.closeQueryContextMenu();
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
requestAnimationFrame(() => {
|
|
1020
|
+
document.addEventListener("click", onOutsideClick);
|
|
1021
|
+
document.addEventListener("keydown", onEsc);
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
this.queryContextMenuCleanup = () => {
|
|
1025
|
+
document.removeEventListener("click", onOutsideClick);
|
|
1026
|
+
document.removeEventListener("keydown", onEsc);
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
private addQueryRowActions(row: HTMLElement, backend: ReturnType<typeof getWorkspaceBackend>, entry: FolderEntry) {
|
|
1031
|
+
if (entry.kind === "folder") {
|
|
1032
|
+
row.addEventListener("contextmenu", (e) => this.openFolderContextMenu(e, backend, entry, row), {
|
|
1033
|
+
passive: false,
|
|
819
1034
|
});
|
|
820
|
-
|
|
1035
|
+
return;
|
|
821
1036
|
}
|
|
822
1037
|
|
|
823
|
-
if (
|
|
824
|
-
|
|
825
|
-
|
|
1038
|
+
if (entry.kind !== "query") return;
|
|
1039
|
+
|
|
1040
|
+
// Query actions are shown in a right-click context menu to preserve space for the query name.
|
|
1041
|
+
row.addEventListener("contextmenu", (e) => this.openQueryContextMenu(e, backend, entry, row), { passive: false });
|
|
826
1042
|
}
|
|
827
1043
|
|
|
828
1044
|
private renderFlatEntries(backend: ReturnType<typeof getWorkspaceBackend>, entries: FolderEntry[]) {
|