feat(editor): locate the current file from the tab context menu
This commit is contained in:
1 parent
f3a3424738
commit
56cc4049e3
8 files changed
+127
-4
No files matched your search
@@ -494,6 +494,15 @@ const isAiPanelMaximized = ref(false);
|
||||
const isHistoryPanelMaximized = ref(false);
|
||||
const isZenMode = ref(false);
|
||||
const showSqlLibraryPanel = ref(safeLocalStorageGet("dbx-sql-library-open") === "true");
|
||||
const sqlLibraryLocateRequest = ref<{ id: number; fileId: string } | null>(null);
|
||||
let sqlLibraryLocateRequestId = 0;
|
||||
watch(
|
||||
showSqlLibraryPanel,
|
||||
(open) => {
|
||||
if (!open) sqlLibraryLocateRequest.value = null;
|
||||
},
|
||||
{ flush: "sync" },
|
||||
);
|
||||
const sqlFileLocateRequest = ref<{ id: number; path: string } | null>(null);
|
||||
let sqlFileLocateRequestId = 0;
|
||||
const showSqlFilePanel = ref(safeLocalStorageGet("dbx-sql-file-panel-open") === "true");
|
||||
@@ -3740,6 +3749,17 @@ function finishSqlFileLocate(requestId: number) {
|
||||
if (sqlFileLocateRequest.value?.id === requestId) sqlFileLocateRequest.value = null;
|
||||
}
|
||||
|
||||
function locateSqlLibraryFile(tab: QueryTab) {
|
||||
if (!tab.savedSqlId || isDetachedWindowContext) return;
|
||||
isZenMode.value = false;
|
||||
openRightSidebarPanel("sqlLibrary");
|
||||
sqlLibraryLocateRequest.value = { id: ++sqlLibraryLocateRequestId, fileId: tab.savedSqlId };
|
||||
}
|
||||
|
||||
function finishSqlLibraryLocate(requestId: number) {
|
||||
if (sqlLibraryLocateRequest.value?.id === requestId) sqlLibraryLocateRequest.value = null;
|
||||
}
|
||||
|
||||
async function locateTabInSidebar(tab: QueryTab) {
|
||||
setSidebarOpen(true);
|
||||
await nextTick();
|
||||
@@ -5586,6 +5606,7 @@ onUnmounted(() => {
|
||||
:content-suppressed="activeTab?.mode === 'plugin-workbench'"
|
||||
@locate-tab="locateTabInSidebar"
|
||||
@locate-sql-file="locateSqlFile"
|
||||
@locate-sql-library-file="locateSqlLibraryFile"
|
||||
@close-tab="
|
||||
(tabId: string) => {
|
||||
if (tabId === queryStore.activeTabId) void closeActiveSurface();
|
||||
@@ -5819,7 +5840,7 @@ onUnmounted(() => {
|
||||
<div class="panel-resize-handle panel-resize-handle--left" @pointerdown="startSqlLibraryResize" />
|
||||
<div class="h-full min-h-0 overflow-hidden rounded-[inherit]" @mousedown="rememberAuxiliarySearchSurface('sqlLibrary')">
|
||||
<div data-sql-library-panel class="h-full min-h-0">
|
||||
<SqlLibraryPanel @close="closeRightSidebarPanel('sqlLibrary')" />
|
||||
<SqlLibraryPanel :locate-request="sqlLibraryLocateRequest" @locate-handled="finishSqlLibraryLocate" @close="closeRightSidebarPanel('sqlLibrary')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -192,6 +192,7 @@ const groupExecutableSql = computed(() => {
|
||||
@activate-tab="$emit('activate-tab', $event)"
|
||||
@locate-tab="$emit('locate-tab', $event)"
|
||||
@locate-sql-file="$emit('locate-sql-file', $event)"
|
||||
@locate-sql-library-file="$emit('locate-sql-library-file', $event)"
|
||||
@toggle-zen-mode="$emit('toggle-zen-mode')"
|
||||
@start-resize="$emit('start-resize', $event)"
|
||||
@toggle-collapse="$emit('toggle-collapse')"
|
||||
|
||||
@@ -145,6 +145,7 @@ const emit = defineEmits<{
|
||||
"activate-tab": [tabId: string];
|
||||
"locate-tab": [tab: QueryTab];
|
||||
"locate-sql-file": [tab: QueryTab];
|
||||
"locate-sql-library-file": [tab: QueryTab];
|
||||
"toggle-zen-mode": [];
|
||||
"start-resize": [event: PointerEvent];
|
||||
"toggle-collapse": [];
|
||||
@@ -1705,6 +1706,12 @@ function getTabMenuItems(tab: QueryTab): ContextMenuItem[] {
|
||||
visible: !!queryTabExternalFilePath(tab),
|
||||
action: () => emit("locate-sql-file", tab),
|
||||
},
|
||||
{
|
||||
label: t("sqlFileTree.locateActiveFile"),
|
||||
icon: Crosshair,
|
||||
visible: !!tab.savedSqlId,
|
||||
action: () => emit("locate-sql-library-file", tab),
|
||||
},
|
||||
{ label: "", separator: true },
|
||||
createPinTabMenuItem({
|
||||
label: tab.pinned ? t("contextMenu.unpinTab") : t("contextMenu.pinTab"),
|
||||
|
||||
@@ -78,8 +78,13 @@ const queryStore = useQueryStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const { databaseOptions, loadingDatabaseOptions, loadDatabaseOptions, catalogOptions, loadingCatalogOptions, loadCatalogOptions, catalogDatabaseOptions, loadingCatalogDatabaseOptions, loadCatalogDatabaseOptions } = useDatabaseOptions();
|
||||
|
||||
const props = defineProps<{
|
||||
locateRequest?: { id: number; fileId: string } | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
"locate-handled": [id: number];
|
||||
}>();
|
||||
|
||||
const UNFILED_DROP_TARGET_ID = "__sql-library-unfiled__";
|
||||
@@ -541,8 +546,8 @@ const activeSavedSqlId = computed(() => queryStore.tabs.find((tab) => tab.id ===
|
||||
const libraryTree = ref<HTMLElement | null>(null);
|
||||
const currentSavedSqlFile = computed(() => (activeSavedSqlId.value ? savedSqlStore.getFile(activeSavedSqlId.value) : undefined));
|
||||
|
||||
async function locateCurrentFile() {
|
||||
const file = currentSavedSqlFile.value;
|
||||
async function locateFile(fileId: string) {
|
||||
const file = savedSqlStore.getFile(fileId);
|
||||
if (!file) return;
|
||||
if (searchInputPending.value || !treeRows.value.some((row) => row.type === "file" && row.file.id === file.id)) {
|
||||
searchInput.value?.clear();
|
||||
@@ -565,6 +570,25 @@ async function locateCurrentFile() {
|
||||
row?.scrollIntoView({ block: "center", inline: "nearest" });
|
||||
}
|
||||
|
||||
async function locateCurrentFile() {
|
||||
const file = currentSavedSqlFile.value;
|
||||
if (!file) return;
|
||||
await locateFile(file.id);
|
||||
}
|
||||
|
||||
watch(
|
||||
[() => savedSqlStore.loadState, () => props.locateRequest],
|
||||
async ([loadState, request]) => {
|
||||
if (!request || loadState === "loading") return;
|
||||
try {
|
||||
await locateFile(request.fileId);
|
||||
} finally {
|
||||
emit("locate-handled", request.id);
|
||||
}
|
||||
},
|
||||
{ immediate: true, flush: "post" },
|
||||
);
|
||||
|
||||
const hasCurrentSavedSqlExecutionTarget = computed(() => {
|
||||
const activeTab = queryStore.tabs.find((tab) => tab.id === queryStore.activeTabId);
|
||||
const target = savedSqlExecutionTargetFromTab(activeTab);
|
||||
@@ -1611,6 +1635,12 @@ function showDropAfter(targetId: string) {
|
||||
function showDropInside(targetId: string) {
|
||||
return dragState.active && dragState.targetId === targetId && dragState.dropPosition === "inside";
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
locateFile,
|
||||
locateCurrentFile,
|
||||
searchInput,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
+33
-1
@@ -126,7 +126,8 @@ function mountBar(
|
||||
fullTabTitle: "Full title",
|
||||
compactTabTitle: "Compact title",
|
||||
},
|
||||
sidebar: { locateActiveTab: "Locate" },
|
||||
sidebar: { locateActiveTab: "Locate", locateSqlFile: "Locate in SQL file list" },
|
||||
sqlFileTree: { locateActiveFile: "Locate current file" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -831,6 +832,37 @@ describe("EditorGroupTabBar behavior", () => {
|
||||
app.unmount();
|
||||
host.remove();
|
||||
});
|
||||
|
||||
it("shows locate current file menu item for saved sql tab and emits locate-sql-library-file", async () => {
|
||||
const store = useQueryStore();
|
||||
const queryTabId = store.createTab("pg-1", "app", "Query 1", "query");
|
||||
const savedSqlTabId = store.createTab("pg-1", "app", "Saved Query", "query");
|
||||
store.tabs.find((tab) => tab.id === savedSqlTabId)!.savedSqlId = "saved-sql-1";
|
||||
const mainGroup = store.groups[0];
|
||||
|
||||
const onLocateSqlLibraryFile = vi.fn();
|
||||
const { app, host } = mountBar(mainGroup.id, [queryTabId, savedSqlTabId], queryTabId, pinia, undefined, undefined, undefined, undefined, { "onLocate-sql-library-file": onLocateSqlLibraryFile });
|
||||
await settle();
|
||||
|
||||
const rawMenuFor = (tabId: string) => {
|
||||
const pill = tabPill(host, tabId);
|
||||
const stub = pill.closest<HTMLElement>(".ctx-menu-stub") as any;
|
||||
return (stub?.__ctxMenuItems ?? []) as Array<{ label: string; action?: () => void; visible?: boolean }>;
|
||||
};
|
||||
|
||||
const regularLocateItem = rawMenuFor(queryTabId).find((item) => item.label === "Locate current file");
|
||||
expect(regularLocateItem?.visible).toBe(false);
|
||||
|
||||
const savedSqlLocateItem = rawMenuFor(savedSqlTabId).find((item) => item.label === "Locate current file");
|
||||
expect(savedSqlLocateItem?.visible).toBe(true);
|
||||
|
||||
savedSqlLocateItem?.action?.();
|
||||
expect(onLocateSqlLibraryFile).toHaveBeenCalledTimes(1);
|
||||
expect(onLocateSqlLibraryFile).toHaveBeenCalledWith(expect.objectContaining({ id: savedSqlTabId, savedSqlId: "saved-sql-1" }));
|
||||
|
||||
app.unmount();
|
||||
host.remove();
|
||||
});
|
||||
});
|
||||
|
||||
describe("active tab reveal scrolling (issue #12010)", () => {
|
||||
|
||||
@@ -174,4 +174,34 @@ describe("SQL library locate and search preferences", () => {
|
||||
await nextTick();
|
||||
expect(button.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("handles locateRequest prop, uncollapses ancestors, scrolls into view and emits locate-handled", async () => {
|
||||
const onLocateHandled = vi.fn();
|
||||
const pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
const store = useSavedSqlStore();
|
||||
store.loadState = "loaded";
|
||||
store.folders = [
|
||||
{ id: "root", name: "Root", connectionId: "", createdAt: timestamp, updatedAt: timestamp },
|
||||
{ id: "child", parentFolderId: "root", name: "Child", connectionId: "", createdAt: timestamp, updatedAt: timestamp },
|
||||
];
|
||||
store.files = [file("unrelated", "unrelated.sql", "SELECT 1"), { ...file("nested-file", "nested.sql", "SELECT 42"), folderId: "child" }];
|
||||
vi.spyOn(store, "ensureAllFilesLoaded").mockResolvedValue(undefined);
|
||||
|
||||
const scroll = vi.spyOn(HTMLElement.prototype, "scrollIntoView").mockImplementation(() => {});
|
||||
const host = document.createElement("div");
|
||||
document.body.appendChild(host);
|
||||
const app = createApp(SqlLibraryPanel, {
|
||||
locateRequest: { id: 42, fileId: "nested-file" },
|
||||
"onLocate-handled": onLocateHandled,
|
||||
});
|
||||
app.use(pinia);
|
||||
app.use(i18n);
|
||||
app.mount(host);
|
||||
mounted.push({ app, host });
|
||||
|
||||
await vi.waitFor(() => expect(onLocateHandled).toHaveBeenCalledWith(42));
|
||||
expect(visibleFiles(host)).toContain("nested-file");
|
||||
expect(scroll.mock.instances.at(-1)).toBe(host.querySelector('[data-sql-library-file="nested-file"]'));
|
||||
});
|
||||
});
|
||||
@@ -85,6 +85,7 @@ export interface ContentAreaSurfaceEmits {
|
||||
closeTab: [tabId: string];
|
||||
"locate-tab": [tab: QueryTab];
|
||||
"locate-sql-file": [tab: QueryTab];
|
||||
"locate-sql-library-file": [tab: QueryTab];
|
||||
"update:activeOutputView": [tabId: string, value: TabOutputView];
|
||||
fixWithAi: [tabId: string, errorMessage: string];
|
||||
sendSelectionToAi: [tabId: string, sql: string];
|
||||
|
||||
@@ -10,6 +10,7 @@ export const contentSurfaceEventNames = [
|
||||
"closeTab",
|
||||
"locate-tab",
|
||||
"locate-sql-file",
|
||||
"locate-sql-library-file",
|
||||
"update:activeOutputView",
|
||||
"fixWithAi",
|
||||
"sendSelectionToAi",
|
||||
|
||||
Reference in new issue
Block a user