From 2f2303e1d6fe077bc68ed09b3c93623e5f945d8c Mon Sep 17 00:00:00 2001 From: Melody Ruch <396921921@qq.com> Date: Sat, 15 Aug 2026 23:29:24 +0800 Subject: [PATCH] fix(mysql): keep view edit draft formatted --- .../components/objects/ObjectSourceDialog.vue | 6 ++-- .../ObjectSourceDialog.editDraft.spec.ts | 14 ++++++++++ .../table/objectSourceEditor.spec.ts | 28 ++++++++++++++++++- .../src/lib/table/objectSourceEditor.ts | 5 ++++ 4 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/src/components/objects/__tests__/ObjectSourceDialog.editDraft.spec.ts diff --git a/apps/desktop/src/components/objects/ObjectSourceDialog.vue b/apps/desktop/src/components/objects/ObjectSourceDialog.vue index c871deb6dc..b0964dfd3b 100644 --- a/apps/desktop/src/components/objects/ObjectSourceDialog.vue +++ b/apps/desktop/src/components/objects/ObjectSourceDialog.vue @@ -7,7 +7,7 @@ import { useConnectionStore } from "@/stores/connectionStore"; import { useSettingsStore } from "@/stores/settingsStore"; import { copyToClipboard } from "@/lib/common/clipboard"; import { formatSqlForDisplay, type SqlFormatDialect } from "@/lib/sql/sqlFormatter"; -import { buildEditableObjectSource, buildExecutableObjectSourceStatements, executeObjectSourceSave, formatObjectSourceSaveError } from "@/lib/table/objectSourceEditor"; +import { buildEditableObjectSource, buildExecutableObjectSourceStatements, executeObjectSourceSave, formatObjectSourceSaveError, resolveObjectSourceEditDraft } from "@/lib/table/objectSourceEditor"; import { loadObjectSourceWithRoutineFallback } from "@/lib/table/objectSourceLoad"; import { xuguRoutineMetadataFromDefinition, type XuguRoutineMetadata } from "@/lib/table/routineParameters"; import { executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard"; @@ -104,7 +104,7 @@ async function loadSource(nextEditing = props.initialEditing && canEdit.value) { const formatted = await formatSqlForDisplay(editable, props.formatDialect ?? props.dialect, settingsStore.editorSettings.sqlFormatter); editableText.value = editable; content.value = formatted; - draft.value = nextEditing && canEdit.value ? editable : ""; + draft.value = nextEditing && canEdit.value ? resolveObjectSourceEditDraft(props.databaseType, resolvedType, formatted, editable) : ""; editing.value = nextEditing && canEdit.value; if (nextEditing && !canEdit.value) { toast(t("objects.sourceReadOnly"), 3000); @@ -131,7 +131,7 @@ function editSource() { if (!canEdit.value) toast(t("objects.sourceReadOnly"), 3000); return; } - draft.value = editableText.value; + draft.value = resolveObjectSourceEditDraft(props.databaseType, resolvedObjectType.value, content.value, editableText.value); saveError.value = ""; editing.value = true; } diff --git a/apps/desktop/src/components/objects/__tests__/ObjectSourceDialog.editDraft.spec.ts b/apps/desktop/src/components/objects/__tests__/ObjectSourceDialog.editDraft.spec.ts new file mode 100644 index 0000000000..fa2295a17d --- /dev/null +++ b/apps/desktop/src/components/objects/__tests__/ObjectSourceDialog.editDraft.spec.ts @@ -0,0 +1,14 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const objectSourceDialogSource = readFileSync(new URL("../ObjectSourceDialog.vue", import.meta.url), "utf8"); + +describe("ObjectSourceDialog edit draft wiring (issue #5057)", () => { + it("seeds the initial edit draft from the pretty-printed source, not the raw single-line definition", () => { + expect(objectSourceDialogSource).toMatch(/draft\.value = nextEditing && canEdit\.value \? resolveObjectSourceEditDraft\(props\.databaseType, resolvedType, formatted, editable\) : "";/); + }); + + it("seeds the draft opened via the Edit button from the pretty-printed source, not the raw single-line definition", () => { + expect(objectSourceDialogSource).toMatch(/function editSource\(\) \{[\s\S]*?draft\.value = resolveObjectSourceEditDraft\(props\.databaseType, resolvedObjectType\.value, content\.value, editableText\.value\);/); + }); +}); diff --git a/apps/desktop/src/lib/__tests__/table/objectSourceEditor.spec.ts b/apps/desktop/src/lib/__tests__/table/objectSourceEditor.spec.ts index 31695a6d8d..957dfa1cc1 100644 --- a/apps/desktop/src/lib/__tests__/table/objectSourceEditor.spec.ts +++ b/apps/desktop/src/lib/__tests__/table/objectSourceEditor.spec.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import * as api from "@/lib/backend/api"; -import { executeObjectSourceSave, formatObjectSourceSaveError } from "@/lib/table/objectSourceEditor"; +import { executeObjectSourceSave, formatObjectSourceSaveError, resolveObjectSourceEditDraft } from "@/lib/table/objectSourceEditor"; +import { formatSqlForDisplay } from "@/lib/sql/sqlFormatter"; vi.mock("@/lib/backend/api", () => ({ executeInTransaction: vi.fn().mockResolvedValue({}), @@ -62,3 +63,28 @@ describe("formatObjectSourceSaveError", () => { expect(formatObjectSourceSaveError(formatted, "postgres", "VIEW", hint)).toBe(formatted); }); }); + +describe("resolveObjectSourceEditDraft", () => { + const mysqlSingleLineViewDefinition = "CREATE VIEW `v_active_users` AS select `users`.`id` AS `id`,`users`.`name` AS `name` from `users` where (`users`.`active` = 1)"; + + it("uses the pretty-printed draft for MySQL views (issue #5057)", async () => { + const formatted = await formatSqlForDisplay(mysqlSingleLineViewDefinition, "mysql"); + + expect(formatted).not.toBe(mysqlSingleLineViewDefinition); + expect(formatted.split("\n").length).toBeGreaterThan(1); + expect(resolveObjectSourceEditDraft("mysql", "VIEW", formatted, mysqlSingleLineViewDefinition)).toBe(formatted); + }); + + it("keeps raw editable source for other database and object types", () => { + const formatted = "CREATE VIEW v AS\nSELECT 1"; + + expect(resolveObjectSourceEditDraft("postgres", "VIEW", formatted, mysqlSingleLineViewDefinition)).toBe(mysqlSingleLineViewDefinition); + expect(resolveObjectSourceEditDraft("mysql", "FUNCTION", formatted, mysqlSingleLineViewDefinition)).toBe(mysqlSingleLineViewDefinition); + expect(resolveObjectSourceEditDraft(undefined, "VIEW", formatted, mysqlSingleLineViewDefinition)).toBe(mysqlSingleLineViewDefinition); + }); + + it("falls back to raw MySQL view source when formatting produced nothing usable", () => { + expect(resolveObjectSourceEditDraft("mysql", "VIEW", "", mysqlSingleLineViewDefinition)).toBe(mysqlSingleLineViewDefinition); + expect(resolveObjectSourceEditDraft("mysql", "VIEW", " ", mysqlSingleLineViewDefinition)).toBe(mysqlSingleLineViewDefinition); + }); +}); diff --git a/apps/desktop/src/lib/table/objectSourceEditor.ts b/apps/desktop/src/lib/table/objectSourceEditor.ts index a8ce19a3d3..6f936124bc 100644 --- a/apps/desktop/src/lib/table/objectSourceEditor.ts +++ b/apps/desktop/src/lib/table/objectSourceEditor.ts @@ -60,6 +60,11 @@ export async function buildExecutableObjectSourceSql(input: BuildEditableObjectS return api.buildExecutableObjectSourceSql(input); } +export function resolveObjectSourceEditDraft(databaseType: DatabaseType | undefined, objectType: ObjectSourceKind, formatted: string, editable: string): string { + if (databaseType === "mysql" && objectType === "VIEW" && formatted.trim()) return formatted; + return editable; +} + export function buildEditableObjectSource(input: BuildEditableObjectSourceSqlInput): Promise { return api.buildEditableObjectSource(input); }