mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 10:28:44 +00:00
fix(mysql): open editor when creating a new event
This commit is contained in:
@@ -2343,6 +2343,7 @@ defineExpose({
|
||||
:initial-event-name="activeTab.objectBrowser?.eventName"
|
||||
:initial-event-read-only="activeTab.objectBrowser?.eventReadOnly"
|
||||
:initial-event-open-request-id="activeTab.objectBrowser?.eventOpenRequestId"
|
||||
:initial-event-create-request-id="activeTab.objectBrowser?.eventCreateRequestId"
|
||||
:initial-object-filter="activeTab.objectBrowser?.initialObjectFilter"
|
||||
:viewport="activeTab.objectBrowser?.viewport"
|
||||
@open-table="emit('openObjectTable', $event)"
|
||||
|
||||
@@ -155,6 +155,7 @@ import { loadObjectMetadataFacet } from "@/lib/metadata/objectMetadataCache";
|
||||
import { invalidateObjectMetadataCache } from "@/lib/metadata/objectMetadataCache";
|
||||
import { invalidateObjectDdl } from "@/lib/metadata/objectDdlCache";
|
||||
import { invalidateObjectBrowserRowsCache } from "@/lib/table/objectBrowserRowsCache";
|
||||
import { resolveInitialEventEditorRequest } from "@/lib/table/eventEditorRequest";
|
||||
|
||||
type ObjectFilter = ObjectBrowserFilter;
|
||||
type ObjectBrowserColumnKey = "select" | "name" | "type" | "estimatedRows" | "totalBytes" | "created_at" | "updated_at" | "comment";
|
||||
@@ -167,6 +168,8 @@ const props = defineProps<{
|
||||
initialEventName?: string;
|
||||
initialEventReadOnly?: boolean;
|
||||
initialEventOpenRequestId?: number;
|
||||
/** 显式"新建事件"请求号:每次菜单点击递增,用于打开/重新进入 CREATE 编辑器 */
|
||||
initialEventCreateRequestId?: number;
|
||||
initialObjectFilter?: "tables" | "events";
|
||||
viewport?: ObjectBrowserViewport;
|
||||
}>();
|
||||
@@ -2711,18 +2714,33 @@ function applyObjectBrowserRows(nextRows: ObjectBrowserRow[]) {
|
||||
}
|
||||
|
||||
function openInitialEventIfNeeded() {
|
||||
const name = props.initialEventName?.trim();
|
||||
const requestKey = `${props.initialEventOpenRequestId ?? 0}:${name}`;
|
||||
if (!name || openedInitialEvent.value === requestKey || loadingObjects.value) return;
|
||||
const name = props.initialEventName?.trim() ?? "";
|
||||
const decision = resolveInitialEventEditorRequest({
|
||||
eventCreateRequestId: props.initialEventCreateRequestId,
|
||||
eventName: props.initialEventName,
|
||||
eventOpenRequestId: props.initialEventOpenRequestId,
|
||||
openedRequestKey: openedInitialEvent.value,
|
||||
hasEventRow: rows.value.some((candidate) => candidate.type === "EVENT" && candidate.name === name),
|
||||
loadingObjects: loadingObjects.value,
|
||||
});
|
||||
if (decision.type === "ignore") return;
|
||||
openedInitialEvent.value = decision.requestKey;
|
||||
if (decision.type === "create") {
|
||||
// 新建事件:不依赖对象列表中的 EVENT row,直接进入 CREATE 编辑器。
|
||||
// MySqlEventEditor 收到空 name 时会以 CREATE 模式渲染。
|
||||
sidePanelGuard.start();
|
||||
sidePanelRow.value = null;
|
||||
sourceRow.value = null;
|
||||
sidePanelMode.value = "event-editor";
|
||||
return;
|
||||
}
|
||||
const row = rows.value.find((candidate) => candidate.type === "EVENT" && candidate.name === name);
|
||||
if (!row) return;
|
||||
openedInitialEvent.value = requestKey;
|
||||
openEventEditor(row);
|
||||
if (row) openEventEditor(row);
|
||||
}
|
||||
|
||||
function finishObjectBrowserRowsLoad() {
|
||||
loadingObjects.value = false;
|
||||
const preferredFilter = props.initialObjectFilter ?? (props.initialEventName ? "events" : "tables");
|
||||
const preferredFilter = props.initialObjectFilter ?? (props.initialEventName || props.initialEventCreateRequestId !== undefined ? "events" : "tables");
|
||||
if (!userHasSelectedFilter.value && objectCounts.value[preferredFilter] > 0) {
|
||||
// The default table filter is a presentation choice, not a user query
|
||||
// change, so preserve the tab's saved scroll offset across remounts.
|
||||
@@ -2733,8 +2751,8 @@ function finishObjectBrowserRowsLoad() {
|
||||
restoreObjectBrowserViewport();
|
||||
}
|
||||
|
||||
watch([() => props.initialEventName, () => props.initialEventOpenRequestId], ([name, requestId], [previousName, previousRequestId]) => {
|
||||
if (name !== previousName || requestId !== previousRequestId) openedInitialEvent.value = "";
|
||||
watch([() => props.initialEventName, () => props.initialEventOpenRequestId, () => props.initialEventCreateRequestId], ([name, requestId, createRequestId], [previousName, previousRequestId, previousCreateRequestId]) => {
|
||||
if (name !== previousName || requestId !== previousRequestId || createRequestId !== previousCreateRequestId) openedInitialEvent.value = "";
|
||||
openInitialEventIfNeeded();
|
||||
});
|
||||
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const objectBrowserSource = readFileSync(new URL("../ObjectBrowser.vue", import.meta.url), "utf8");
|
||||
const contentAreaSource = readFileSync(new URL("../../layout/ContentArea.vue", import.meta.url), "utf8");
|
||||
|
||||
function functionBody(name: string, source: string): string {
|
||||
const signature = new RegExp(`(?:async\\s+)?function\\s+${name}\\s*\\([^)]*\\)\\s*(?::\\s*[^\\{]+)?\\{`, "m").exec(source);
|
||||
if (!signature) throw new Error(`Missing function ${name}`);
|
||||
const bodyStart = signature.index + signature[0].length;
|
||||
let depth = 1;
|
||||
for (let index = bodyStart; index < source.length; index += 1) {
|
||||
if (source[index] === "{") depth += 1;
|
||||
else if (source[index] === "}") depth -= 1;
|
||||
if (depth === 0) return source.slice(bodyStart, index);
|
||||
}
|
||||
throw new Error(`Unclosed function ${name}`);
|
||||
}
|
||||
|
||||
describe("ObjectBrowser MySQL Event CREATE navigation", () => {
|
||||
it("accepts an explicit create request prop next to the edit props", () => {
|
||||
expect(objectBrowserSource).toContain("initialEventCreateRequestId?: number;");
|
||||
expect(objectBrowserSource).toMatch(/initialEventName\?: string;[\s\S]*?initialEventCreateRequestId\?: number;[\s\S]*?initialObjectFilter\?: "tables" \| "events";/);
|
||||
});
|
||||
|
||||
it("routes the initial event request through the shared resolver", () => {
|
||||
const openInitialEventIfNeeded = functionBody("openInitialEventIfNeeded", objectBrowserSource);
|
||||
expect(openInitialEventIfNeeded).toContain("resolveInitialEventEditorRequest({");
|
||||
expect(openInitialEventIfNeeded).toContain("eventCreateRequestId: props.initialEventCreateRequestId");
|
||||
expect(openInitialEventIfNeeded).toContain("openedRequestKey: openedInitialEvent.value");
|
||||
expect(openInitialEventIfNeeded).toContain("hasEventRow: rows.value.some");
|
||||
expect(openInitialEventIfNeeded).toContain("loadingObjects: loadingObjects.value");
|
||||
});
|
||||
|
||||
it("opens the CREATE editor without any existing EVENT row", () => {
|
||||
const openInitialEventIfNeeded = functionBody("openInitialEventIfNeeded", objectBrowserSource);
|
||||
expect(openInitialEventIfNeeded).toContain('if (decision.type === "create") {');
|
||||
expect(openInitialEventIfNeeded).toContain("sidePanelRow.value = null;");
|
||||
expect(openInitialEventIfNeeded).toContain('sidePanelMode.value = "event-editor";');
|
||||
expect(openInitialEventIfNeeded).toContain("openedInitialEvent.value = decision.requestKey;");
|
||||
// The editor must see an empty name so MySqlEventEditor renders CREATE mode
|
||||
expect(openInitialEventIfNeeded).not.toContain("sidePanelRow.value = row;");
|
||||
});
|
||||
|
||||
it("renders the editor with sidePanelRow name (empty in create mode -> CREATE SQL)", () => {
|
||||
expect(objectBrowserSource).toContain('<MySqlEventEditor :connection="props.connection" :database="props.database" :schema="sidePanelRow?.schema || selectedSchema || props.database" :name="sidePanelRow?.name"');
|
||||
});
|
||||
|
||||
it("re-triggers a create request when the request id changes on a reused tab", () => {
|
||||
expect(objectBrowserSource).toContain("() => props.initialEventCreateRequestId");
|
||||
expect(objectBrowserSource).toMatch(/watch\(\[\(\) => props\.initialEventName, \(\) => props\.initialEventOpenRequestId, \(\) => props\.initialEventCreateRequestId\][\s\S]*?openedInitialEvent\.value = "";[\s\S]*?openInitialEventIfNeeded\(\);/);
|
||||
});
|
||||
|
||||
it("lists the events filter as the preferred view for a create request", () => {
|
||||
expect(objectBrowserSource).toMatch(/const preferredFilter = props\.initialObjectFilter \?\? \(props\.initialEventName \|\| props\.initialEventCreateRequestId !== undefined \? "events" : "tables"\);/);
|
||||
});
|
||||
|
||||
it("ContentArea passes the create request id into ObjectBrowser", () => {
|
||||
expect(contentAreaSource).toContain(':initial-event-name="activeTab.objectBrowser?.eventName"');
|
||||
expect(contentAreaSource).toContain(':initial-event-create-request-id="activeTab.objectBrowser?.eventCreateRequestId"');
|
||||
});
|
||||
});
|
||||
@@ -1504,15 +1504,19 @@ async function confirmDeleteSavedSqlFile() {
|
||||
releaseActiveNodeReference([node.id]);
|
||||
}
|
||||
|
||||
async function openObjectBrowser(eventReadOnly = false, openEventEditor = false) {
|
||||
async function openObjectBrowser(eventReadOnly = false, openEventEditor: boolean | "create" = false) {
|
||||
const node = activeNode.value;
|
||||
if (!node.connectionId) return;
|
||||
try {
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
connectionStore.activeConnectionId = node.connectionId;
|
||||
|
||||
const eventName = openEventEditor === true && node.type === "event" ? node.objectName || node.label : undefined;
|
||||
const eventCreateRequestId = openEventEditor === "create" && node.type === "group-events" ? ++mysqlEventCreateRequestSeq : undefined;
|
||||
const objectFilter = node.type === "event" || node.type === "group-events" ? "events" : undefined;
|
||||
|
||||
if (hasTreeNodeDatabaseContext(node)) {
|
||||
queryStore.openObjectBrowser(node.connectionId, node.database, node.schema, node.catalog, openEventEditor && node.type === "event" ? node.objectName || node.label : undefined, eventReadOnly, node.type === "event" || node.type === "group-events" ? "events" : undefined);
|
||||
queryStore.openObjectBrowser(node.connectionId, node.database, node.schema, node.catalog, eventName, eventReadOnly, objectFilter, eventCreateRequestId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1521,7 +1525,7 @@ async function openObjectBrowser(eventReadOnly = false, openEventEditor = false)
|
||||
const options = await getDatabaseOptions(node.connectionId);
|
||||
const database = resolveDefaultDatabase(connection, options);
|
||||
if (database) {
|
||||
queryStore.openObjectBrowser(node.connectionId, database, undefined, undefined, openEventEditor && node.type === "event" ? node.objectName || node.label : undefined, eventReadOnly, node.type === "event" || node.type === "group-events" ? "events" : undefined);
|
||||
queryStore.openObjectBrowser(node.connectionId, database, undefined, undefined, eventName, eventReadOnly, objectFilter, eventCreateRequestId);
|
||||
} else {
|
||||
await toggle();
|
||||
}
|
||||
@@ -1531,6 +1535,14 @@ async function openObjectBrowser(eventReadOnly = false, openEventEditor = false)
|
||||
}
|
||||
}
|
||||
|
||||
// 每次"新建事件"菜单点击都会分配一个单调递增的请求号,保证同一个
|
||||
// ObjectBrowser tab 被复用时也能重新进入 MySQL Event 的 CREATE 编辑器。
|
||||
let mysqlEventCreateRequestSeq = 0;
|
||||
|
||||
function openMysqlEventCreateEditor() {
|
||||
void openObjectBrowser(false, "create");
|
||||
}
|
||||
|
||||
async function openDatabaseBrowser() {
|
||||
const node = activeNode.value;
|
||||
if (node.type !== "connection" || !node.connectionId) return;
|
||||
@@ -5705,7 +5717,7 @@ function buildObjectGroupSidebarMenu(context: SidebarMenuFactoryContext): boolea
|
||||
items.push({ label: t("contextMenu.createView"), action: createView, icon: Plus });
|
||||
}
|
||||
if (node.type === "group-events" && node.connectionId && node.database) {
|
||||
items.push({ label: t("contextMenu.createEvent"), action: openObjectBrowser, icon: Plus });
|
||||
items.push({ label: t("contextMenu.createEvent"), action: openMysqlEventCreateEditor, icon: Plus });
|
||||
}
|
||||
if (mysqlObjectTemplate) {
|
||||
items.push({ label: t(mysqlObjectTemplate.titleKey), action: createMysqlObjectTemplate, icon: Plus });
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveInitialEventEditorRequest } from "./eventEditorRequest";
|
||||
|
||||
const base = { openedRequestKey: "" };
|
||||
|
||||
describe("resolveInitialEventEditorRequest", () => {
|
||||
describe("create request (New Event)", () => {
|
||||
it("opens CREATE mode without requiring an existing EVENT row", () => {
|
||||
const decision = resolveInitialEventEditorRequest({ ...base, eventCreateRequestId: 7, hasEventRow: false });
|
||||
expect(decision).toEqual({ type: "create", requestKey: "create:7" });
|
||||
});
|
||||
|
||||
it("dedupes only the same request id", () => {
|
||||
expect(resolveInitialEventEditorRequest({ ...base, eventCreateRequestId: 3, openedRequestKey: "create:2" })).toEqual({ type: "create", requestKey: "create:3" });
|
||||
expect(resolveInitialEventEditorRequest({ openedRequestKey: "create:3", eventCreateRequestId: 3 })).toEqual({ type: "ignore" });
|
||||
});
|
||||
|
||||
it("re-enters CREATE mode when the request id increments on a reused tab", () => {
|
||||
const first = resolveInitialEventEditorRequest({ openedRequestKey: "", eventCreateRequestId: 1 });
|
||||
expect(first).toEqual({ type: "create", requestKey: "create:1" });
|
||||
// second click on the SAME tab with a fresh request id must open again
|
||||
const second = resolveInitialEventEditorRequest({ openedRequestKey: first.type === "create" ? first.requestKey : "", eventCreateRequestId: 2 });
|
||||
expect(second).toEqual({ type: "create", requestKey: "create:2" });
|
||||
});
|
||||
|
||||
it("defers while the object list is still loading", () => {
|
||||
expect(resolveInitialEventEditorRequest({ ...base, eventCreateRequestId: 1, loadingObjects: true })).toEqual({ type: "ignore" });
|
||||
expect(resolveInitialEventEditorRequest({ ...base, eventCreateRequestId: 1, loadingObjects: false })).toEqual({ type: "create", requestKey: "create:1" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("edit request (existing event)", () => {
|
||||
it("opens the existing EVENT editor only when the row is present", () => {
|
||||
expect(resolveInitialEventEditorRequest({ ...base, eventName: "foo_event", eventOpenRequestId: 2, hasEventRow: true })).toEqual({ type: "edit", requestKey: "2:foo_event" });
|
||||
expect(resolveInitialEventEditorRequest({ ...base, eventName: "foo_event", eventOpenRequestId: 2, hasEventRow: false })).toEqual({ type: "ignore" });
|
||||
});
|
||||
|
||||
it("remains ALTER-mode safe: same name + new request id reopens", () => {
|
||||
const first = resolveInitialEventEditorRequest({ openedRequestKey: "", eventName: "foo_event", eventOpenRequestId: 1, hasEventRow: true });
|
||||
expect(first).toEqual({ type: "edit", requestKey: "1:foo_event" });
|
||||
const second = resolveInitialEventEditorRequest({ openedRequestKey: first.type === "edit" ? first.requestKey : "", eventName: "foo_event", eventOpenRequestId: 2, hasEventRow: true });
|
||||
expect(second).toEqual({ type: "edit", requestKey: "2:foo_event" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("plain Event list open", () => {
|
||||
it("does not open any editor when there is no event intent", () => {
|
||||
expect(resolveInitialEventEditorRequest({ ...base, openedRequestKey: "" })).toEqual({ type: "ignore" });
|
||||
expect(resolveInitialEventEditorRequest({ ...base, openedRequestKey: "", initialObjectFilterIntent: undefined } as any)).toEqual({ type: "ignore" });
|
||||
});
|
||||
|
||||
it("does not open CREATE mode for stale blank names", () => {
|
||||
expect(resolveInitialEventEditorRequest({ ...base, eventName: " " })).toEqual({ type: "ignore" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("create priority over stale edit target", () => {
|
||||
it("honors a fresh create request even when an edit target lingers", () => {
|
||||
const decision = resolveInitialEventEditorRequest({ ...base, eventCreateRequestId: 5, eventName: "old_event", eventOpenRequestId: 9, hasEventRow: true });
|
||||
expect(decision).toEqual({ type: "create", requestKey: "create:5" });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Resolve which MySQL Event editor request an ObjectBrowser tab should apply.
|
||||
*
|
||||
* ObjectBrowser receives event-open intent through three independent props:
|
||||
*
|
||||
* - `eventCreateRequestId`: explicit "create a NEW event" request. It does not
|
||||
* depend on the EVENT row existing in the object list, so it can open the
|
||||
* MySqlEventEditor in CREATE mode even before the list is loaded.
|
||||
* - `eventName` + `eventOpenRequestId`: "open/edit THIS existing event". It
|
||||
* requires the EVENT row to be present in the loaded object list.
|
||||
*
|
||||
* The create request takes priority over an edit request so a stale edit target
|
||||
* can never swallow a fresh "New Event" action. Callers (queryStore) keep the
|
||||
* two states mutually exclusive, but this resolver stays defensive anyway.
|
||||
*/
|
||||
export type InitialEventEditorDecision = { type: "create"; requestKey: string } | { type: "edit"; requestKey: string } | { type: "ignore" };
|
||||
|
||||
export interface InitialEventEditorRequestInput {
|
||||
/** Monotonic "New Event" request id (undefined = no create intent). */
|
||||
eventCreateRequestId?: number;
|
||||
/** Existing event name to edit (undefined = no edit intent). */
|
||||
eventName?: string;
|
||||
/** Request id that pairs with eventName for repeat open-intent. */
|
||||
eventOpenRequestId?: number;
|
||||
/** Request key already honored by this ObjectBrowser instance (dedupe). */
|
||||
openedRequestKey: string;
|
||||
/** Whether the requested event already exists in the loaded object list. */
|
||||
hasEventRow?: boolean;
|
||||
/** Object list is still loading; defer honoring any request. */
|
||||
loadingObjects?: boolean;
|
||||
}
|
||||
|
||||
export function resolveInitialEventEditorRequest(input: InitialEventEditorRequestInput): InitialEventEditorDecision {
|
||||
const { eventCreateRequestId, eventName, eventOpenRequestId, openedRequestKey, hasEventRow = false, loadingObjects = false } = input;
|
||||
|
||||
// A "New Event" request opens the CREATE editor without needing an existing
|
||||
// EVENT row. It is deduped by its own monotonic request id so the SAME tab
|
||||
// can be asked to re-enter create mode on every click.
|
||||
if (eventCreateRequestId !== undefined) {
|
||||
const requestKey = `create:${eventCreateRequestId}`;
|
||||
if (loadingObjects || openedRequestKey === requestKey) return { type: "ignore" };
|
||||
return { type: "create", requestKey };
|
||||
}
|
||||
|
||||
const name = eventName?.trim();
|
||||
if (!name) return { type: "ignore" };
|
||||
const requestKey = `${eventOpenRequestId ?? 0}:${name}`;
|
||||
if (loadingObjects || openedRequestKey === requestKey || !hasEventRow) return { type: "ignore" };
|
||||
return { type: "edit", requestKey };
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
function installLocalStorage() {
|
||||
const data = new Map<string, string>();
|
||||
vi.stubGlobal("localStorage", {
|
||||
getItem: vi.fn((key: string) => data.get(key) ?? null),
|
||||
setItem: vi.fn((key: string, value: string) => data.set(key, value)),
|
||||
removeItem: vi.fn((key: string) => data.delete(key)),
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// queryStore 是巨型模块,每个用例 vi.resetModules() 后动态 import 需要数秒,
|
||||
// 显式放宽超时(与 queryStore.database-open.spec.ts 的做法一致)。
|
||||
const QUERY_STORE_TEST_TIMEOUT = 30_000;
|
||||
|
||||
describe("queryStore MySQL Event create navigation", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.unstubAllGlobals();
|
||||
installLocalStorage();
|
||||
setActivePinia(createPinia());
|
||||
}, QUERY_STORE_TEST_TIMEOUT);
|
||||
|
||||
it(
|
||||
"records an explicit CREATE request when opening a brand-new ObjectBrowser for New Event",
|
||||
async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
|
||||
const tabId = store.openObjectBrowser("mysql-1", "shop", undefined, undefined, undefined, false, "events", 1);
|
||||
|
||||
expect(store.tabs).toHaveLength(1);
|
||||
expect(store.tabs.find((tab) => tab.id === tabId)?.objectBrowser).toMatchObject({
|
||||
eventName: undefined,
|
||||
eventReadOnly: false,
|
||||
eventOpenRequestId: undefined,
|
||||
eventCreateRequestId: 1,
|
||||
initialObjectFilter: "events",
|
||||
});
|
||||
},
|
||||
QUERY_STORE_TEST_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
"reuses the existing ObjectBrowser tab and re-triggers CREATE on every New Event click",
|
||||
async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
|
||||
const firstTabId = store.openObjectBrowser("mysql-1", "shop", undefined, undefined, undefined, false, "events", 1);
|
||||
const tab = store.tabs.find((item) => item.id === firstTabId)!;
|
||||
|
||||
// Second click on the SAME tab: request id must increment so the tab can
|
||||
// re-enter CREATE mode instead of swallowing the action.
|
||||
const secondTabId = store.openObjectBrowser("mysql-1", "shop", undefined, undefined, undefined, false, "events", 2);
|
||||
|
||||
expect(secondTabId).toBe(firstTabId);
|
||||
expect(tab.objectBrowser?.eventCreateRequestId).toBe(2);
|
||||
expect(tab.objectBrowser?.eventName).toBeUndefined();
|
||||
},
|
||||
QUERY_STORE_TEST_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
"opens an existing event in edit mode without any create state",
|
||||
async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
|
||||
const tabId = store.openObjectBrowser("mysql-1", "shop", undefined, undefined, "foo_event", false, "events");
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
expect(tab.objectBrowser?.eventName).toBe("foo_event");
|
||||
expect(tab.objectBrowser?.eventOpenRequestId).toBe(1);
|
||||
expect(tab.objectBrowser?.eventCreateRequestId).toBeUndefined();
|
||||
expect(tab.objectBrowser?.initialObjectFilter).toBe("events");
|
||||
|
||||
// Re-opening the same existing event on the reused tab increments edit request id
|
||||
store.openObjectBrowser("mysql-1", "shop", undefined, undefined, "foo_event", true, "events");
|
||||
expect(tab.objectBrowser?.eventOpenRequestId).toBe(2);
|
||||
expect(tab.objectBrowser?.eventReadOnly).toBe(true);
|
||||
expect(tab.objectBrowser?.eventCreateRequestId).toBeUndefined();
|
||||
},
|
||||
QUERY_STORE_TEST_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
"keeps create and edit requests mutually exclusive on a reused tab",
|
||||
async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
|
||||
const tabId = store.openObjectBrowser("mysql-1", "shop", undefined, undefined, undefined, false, "events", 1);
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
|
||||
// Edit request after a create request clears the stale create state
|
||||
store.openObjectBrowser("mysql-1", "shop", undefined, undefined, "foo_event", false, "events");
|
||||
expect(tab.objectBrowser?.eventName).toBe("foo_event");
|
||||
expect(tab.objectBrowser?.eventCreateRequestId).toBeUndefined();
|
||||
|
||||
// A later create request clears the stale edit state
|
||||
store.openObjectBrowser("mysql-1", "shop", undefined, undefined, undefined, false, "events", 3);
|
||||
expect(tab.objectBrowser?.eventCreateRequestId).toBe(3);
|
||||
expect(tab.objectBrowser?.eventName).toBeUndefined();
|
||||
expect(tab.objectBrowser?.eventOpenRequestId).toBeUndefined();
|
||||
},
|
||||
QUERY_STORE_TEST_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
"plain Event list open carries no create or edit intent",
|
||||
async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
|
||||
const tabId = store.openObjectBrowser("mysql-1", "shop", undefined, undefined, undefined, false, "events");
|
||||
|
||||
expect(store.tabs.find((tab) => tab.id === tabId)?.objectBrowser).toMatchObject({
|
||||
eventName: undefined,
|
||||
eventCreateRequestId: undefined,
|
||||
eventOpenRequestId: undefined,
|
||||
initialObjectFilter: "events",
|
||||
});
|
||||
},
|
||||
QUERY_STORE_TEST_TIMEOUT,
|
||||
);
|
||||
});
|
||||
@@ -1942,15 +1942,27 @@ export const useQueryStore = defineStore("query", () => {
|
||||
return id;
|
||||
}
|
||||
|
||||
function openObjectBrowser(connectionId: string, database: string, schema?: string, catalog?: string, eventName?: string, eventReadOnly = false, initialObjectFilter?: "tables" | "events") {
|
||||
function openObjectBrowser(connectionId: string, database: string, schema?: string, catalog?: string, eventName?: string, eventReadOnly = false, initialObjectFilter?: "tables" | "events", eventCreateRequestId?: number) {
|
||||
const title = catalog ? `${catalog}.${database} objects` : schema ? `${schema} objects` : `${database} objects`;
|
||||
const existing = tabs.value.find((tab) => tab.mode === "objects" && tab.connectionId === connectionId && tab.database === database && (tab.objectBrowser?.catalog || "") === (catalog || "") && (tab.objectBrowser?.schema || "") === (schema || ""));
|
||||
if (existing) {
|
||||
if (eventName) {
|
||||
if (eventCreateRequestId !== undefined) {
|
||||
// 新建事件:显式 CREATE 请求优先,并清掉可能残留的"编辑已有事件"状态,
|
||||
// 保证同一 tab 被复用时每次点击都能重新进入 CREATE 编辑器(请求号单调递增)。
|
||||
existing.objectBrowser = {
|
||||
...existing.objectBrowser,
|
||||
eventName: undefined,
|
||||
eventReadOnly: false,
|
||||
eventOpenRequestId: undefined,
|
||||
eventCreateRequestId,
|
||||
initialObjectFilter: initialObjectFilter ?? "events",
|
||||
};
|
||||
} else if (eventName) {
|
||||
existing.objectBrowser = {
|
||||
...existing.objectBrowser,
|
||||
eventName,
|
||||
eventReadOnly,
|
||||
eventCreateRequestId: undefined,
|
||||
initialObjectFilter: initialObjectFilter ?? (eventName ? "events" : existing.objectBrowser?.initialObjectFilter),
|
||||
eventOpenRequestId: (existing.objectBrowser?.eventOpenRequestId ?? 0) + 1,
|
||||
};
|
||||
@@ -1975,10 +1987,11 @@ export const useQueryStore = defineStore("query", () => {
|
||||
catalog,
|
||||
schema,
|
||||
objectType: "tables",
|
||||
eventName,
|
||||
eventReadOnly,
|
||||
initialObjectFilter: initialObjectFilter ?? (eventName ? "events" : undefined),
|
||||
eventName: eventCreateRequestId !== undefined ? undefined : eventName,
|
||||
eventReadOnly: eventCreateRequestId !== undefined ? false : eventReadOnly,
|
||||
initialObjectFilter: initialObjectFilter ?? (eventName || eventCreateRequestId !== undefined ? "events" : undefined),
|
||||
eventOpenRequestId: eventName ? 1 : undefined,
|
||||
eventCreateRequestId,
|
||||
},
|
||||
};
|
||||
tabs.value.push(tab);
|
||||
|
||||
@@ -1218,6 +1218,8 @@ export interface QueryTab {
|
||||
eventName?: string;
|
||||
eventReadOnly?: boolean;
|
||||
eventOpenRequestId?: number;
|
||||
/** 显式的"新建事件"请求:单调递增,用于让已复用 tab 也能重复进入 CREATE 编辑器 */
|
||||
eventCreateRequestId?: number;
|
||||
initialObjectFilter?: "tables" | "events";
|
||||
viewport?: ObjectBrowserViewport;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user