mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 05:20:02 +00:00
feat(structure): refresh the open table structure with F5
This commit is contained in:
@@ -5201,6 +5201,10 @@ function isShiftEnterShortcut(event: KeyboardEvent): boolean {
|
||||
return !event.isComposing && event.key === "Enter" && event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey;
|
||||
}
|
||||
|
||||
function isPlainStructureRefreshShortcut(event: KeyboardEvent): boolean {
|
||||
return !event.isComposing && !event.repeat && event.key === "F5" && !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey;
|
||||
}
|
||||
|
||||
function isPlainModDeleteShortcut(event: KeyboardEvent): boolean {
|
||||
if (isPlainModShortcut(event, "delete")) return true;
|
||||
if (!event.metaKey || event.ctrlKey || !isPlainModShortcut(event, "backspace")) return false;
|
||||
@@ -5211,6 +5215,22 @@ function isPlainModDeleteShortcut(event: KeyboardEvent): boolean {
|
||||
}
|
||||
|
||||
function onStructureEditorKeydown(event: KeyboardEvent) {
|
||||
// Desktop blocks F5 before this bubble listener and marks it defaultPrevented.
|
||||
// Handle the shortcut first so that guard still refreshes the open table (#11850).
|
||||
if (isPlainStructureRefreshShortcut(event)) {
|
||||
// Only react when the keypress originates inside this editor: the listener is
|
||||
// window-level and every activated structure editor registers one, so without
|
||||
// this check a plain F5 typed into another editor group (where F5 is the global
|
||||
// refresh-data shortcut) would silently discard THIS editor's unsaved drafts.
|
||||
const target = event.target;
|
||||
if (!(target instanceof Element) || !rootRef.value?.contains(target)) return;
|
||||
if (target.closest('[role="dialog"], [role="alertdialog"]')) return;
|
||||
if (isCreateMode.value) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!loading.value && !saving.value && !ddlLoading.value) void reloadStructureFromDatabase();
|
||||
return;
|
||||
}
|
||||
if (event.defaultPrevented) return;
|
||||
if (activeTab.value === "columns" && !eventTargetUsesNativeClipboard(event)) {
|
||||
if (isPlainClipboardShortcut(event, "a")) {
|
||||
@@ -5629,7 +5649,7 @@ watch(
|
||||
<Database :class="[structureIconClass, 'text-muted-foreground']" />
|
||||
<span class="min-w-0 flex-1 truncate font-medium">{{ targetLabel || t("editor.noDatabase") }}</span>
|
||||
<Badge variant="outline">{{ connection?.driver_label || databaseType }}</Badge>
|
||||
<Button v-if="!isCreateMode" variant="ghost" size="sm" :class="structureToolbarButtonClass" :disabled="loading || saving || ddlLoading" @click="reloadStructureFromDatabase">
|
||||
<Button v-if="!isCreateMode" variant="ghost" size="sm" :class="structureToolbarButtonClass" :disabled="loading || saving || ddlLoading" :title="t('structureEditor.refresh') + ' (F5)'" aria-keyshortcuts="F5" data-refresh-structure-shortcut @click="reloadStructureFromDatabase">
|
||||
<RefreshCw :class="structureIconClass" />
|
||||
{{ t("structureEditor.refresh") }}
|
||||
</Button>
|
||||
|
||||
Vendored
+111
@@ -388,4 +388,115 @@ describe("TableStructureEditor DDL tab lifecycle", () => {
|
||||
expect(commentCalls).toHaveLength(2);
|
||||
expect(commentCalls.at(-1)?.[3]).toEqual({ force: true });
|
||||
});
|
||||
|
||||
it("refreshes the open table on F5 after the desktop reload guard", async () => {
|
||||
let tableComment = "Old comment";
|
||||
mocks.loadObjectMetadataFacet.mockImplementation(async (_request, facet: string) => ({
|
||||
value: facet === "columns" ? [{ name: "id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true }] : facet === "comment" ? tableComment : facet === "owner" ? "app_user" : [],
|
||||
cacheStatus: "remote",
|
||||
}));
|
||||
const pendingDdl = deferred<{ ddl: string; cacheStatus: "remote" }>();
|
||||
mocks.loadObjectDdl.mockImplementationOnce(() => pendingDdl.promise);
|
||||
|
||||
const root = document.createElement("div");
|
||||
document.body.append(root);
|
||||
app = createApp(TableStructureEditor, {
|
||||
connectionId: mocks.connection.id,
|
||||
database: "test",
|
||||
schema: "public",
|
||||
tableName: "users",
|
||||
initialTab: "ddl",
|
||||
});
|
||||
app.mount(root);
|
||||
|
||||
const refreshButton = () => buttonWithText(root, "structureEditor.refresh");
|
||||
await vi.waitFor(() => expect(refreshButton().disabled).toBe(true));
|
||||
// F5 originates inside the editor (bubbles up to the window-level listener).
|
||||
refreshButton().dispatchEvent(new KeyboardEvent("keydown", { key: "F5", bubbles: true, cancelable: true }));
|
||||
expect(mocks.loadObjectDdl).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.invalidateObjectDdl).not.toHaveBeenCalled();
|
||||
|
||||
pendingDdl.resolve({ ddl: DDL, cacheStatus: "remote" });
|
||||
const commentInput = () => root.querySelector<HTMLInputElement>('input[placeholder="structureEditor.tableCommentPlaceholder"]');
|
||||
await vi.waitFor(() => expect(refreshButton().disabled).toBe(false));
|
||||
await vi.waitFor(() => expect(commentInput()?.value).toBe("Old comment"));
|
||||
expect(refreshButton().getAttribute("aria-keyshortcuts")).toBe("F5");
|
||||
expect(refreshButton().getAttribute("title")).toBe("structureEditor.refresh (F5)");
|
||||
|
||||
const dialog = document.createElement("div");
|
||||
dialog.setAttribute("role", "dialog");
|
||||
const dialogInput = document.createElement("input");
|
||||
dialog.append(dialogInput);
|
||||
document.body.append(dialog);
|
||||
dialogInput.dispatchEvent(new KeyboardEvent("keydown", { key: "F5", bubbles: true, cancelable: true }));
|
||||
await nextTick();
|
||||
expect(mocks.invalidateObjectDdl).not.toHaveBeenCalled();
|
||||
dialog.remove();
|
||||
|
||||
const blockBrowserReload = (event: KeyboardEvent) => {
|
||||
if (event.key === "F5") event.preventDefault();
|
||||
};
|
||||
window.addEventListener("keydown", blockBrowserReload, true);
|
||||
try {
|
||||
window.dispatchEvent(new KeyboardEvent("keydown", { key: "F5", bubbles: true, cancelable: true, ctrlKey: true }));
|
||||
await nextTick();
|
||||
expect(mocks.invalidateObjectDdl).not.toHaveBeenCalled();
|
||||
|
||||
tableComment = "Updated comment";
|
||||
mocks.loadObjectDdl.mockResolvedValue({ ddl: "CREATE TABLE users (id bigint, name text);", cacheStatus: "remote" });
|
||||
refreshButton().dispatchEvent(new KeyboardEvent("keydown", { key: "F5", bubbles: true, cancelable: true }));
|
||||
await vi.waitFor(() => expect(commentInput()?.value).toBe("Updated comment"));
|
||||
expect(mocks.invalidateObjectDdl).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
window.removeEventListener("keydown", blockBrowserReload, true);
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores F5 dispatched from outside the editor root", async () => {
|
||||
// The keydown listener is window-level and shared by every activated structure
|
||||
// editor; an F5 typed in another editor group (e.g. the global refresh-data
|
||||
// shortcut) must not discard this editor's drafts.
|
||||
mocks.loadObjectDdl.mockResolvedValue({ ddl: DDL, cacheStatus: "remote" });
|
||||
const root = document.createElement("div");
|
||||
document.body.append(root);
|
||||
app = createApp(TableStructureEditor, {
|
||||
connectionId: mocks.connection.id,
|
||||
database: "test",
|
||||
schema: "public",
|
||||
tableName: "users",
|
||||
initialTab: "ddl",
|
||||
});
|
||||
app.mount(root);
|
||||
|
||||
await vi.waitFor(() => expect(mocks.loadObjectDdl).toHaveBeenCalledTimes(1));
|
||||
const outside = document.createElement("input");
|
||||
document.body.append(outside);
|
||||
try {
|
||||
outside.dispatchEvent(new KeyboardEvent("keydown", { key: "F5", bubbles: true, cancelable: true }));
|
||||
window.dispatchEvent(new KeyboardEvent("keydown", { key: "F5", bubbles: true, cancelable: true }));
|
||||
await nextTick();
|
||||
expect(mocks.invalidateObjectDdl).not.toHaveBeenCalled();
|
||||
expect(mocks.loadObjectDdl).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
outside.remove();
|
||||
}
|
||||
});
|
||||
|
||||
it("leaves a create-table draft alone on F5", async () => {
|
||||
const root = document.createElement("div");
|
||||
document.body.append(root);
|
||||
app = createApp(TableStructureEditor, {
|
||||
connectionId: mocks.connection.id,
|
||||
database: "test",
|
||||
schema: "public",
|
||||
tableName: "",
|
||||
});
|
||||
app.mount(root);
|
||||
|
||||
await nextTick();
|
||||
expect(root.textContent).not.toContain("structureEditor.refresh");
|
||||
window.dispatchEvent(new KeyboardEvent("keydown", { key: "F5", bubbles: true, cancelable: true }));
|
||||
await nextTick();
|
||||
expect(mocks.invalidateObjectDdl).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,7 +120,7 @@ description: 可视化查看和修改字段、索引、外键与触发器,并
|
||||
- 编辑草稿基于打开时读取的元数据,不会自动合并其他客户端的结构变更
|
||||
- 执行失败后,数据库可能已经成功执行前面的部分语句
|
||||
- SQLite 重建和多语句 DDL 的原子性取决于数据库和驱动
|
||||
- 刷新会重新读取结构,未保存草稿可能需要先确认放弃
|
||||
- F5 或工具栏「刷新结构」会重新读取结构,未保存草稿可能需要先确认放弃
|
||||
- 只读连接会阻止保存;生产连接每次写入都需要明确确认
|
||||
|
||||
## 使用建议
|
||||
|
||||
@@ -120,7 +120,7 @@ Do not assume that every DDL feature is editable because a database can connect.
|
||||
- Drafts are based on metadata read when the editor opened and do not merge external schema changes automatically
|
||||
- If execution fails, earlier statements may already have succeeded
|
||||
- Atomicity of SQLite rebuilds and multi-statement DDL depends on database and driver behavior
|
||||
- Refresh reloads structure and may require discarding an unsaved draft
|
||||
- F5 or the toolbar Refresh Structure action reloads structure and may require discarding an unsaved draft
|
||||
- Read-only connections block save, and production writes require explicit confirmation every time
|
||||
|
||||
## Recommendations
|
||||
|
||||
Reference in New Issue
Block a user