feat(desktop): drop tree schema objects

This commit is contained in:
t8y2 committed 2026-06-02 00:33:58 +08:00
1 parent 17504be027
commit ac7a5b7576
15 files changed
+466 -13

No files matched your search

+205 -13
View File
@@ -102,15 +102,19 @@ import {
buildDropObjectSql,
buildDropSchemaSql,
buildDropTableSql,
buildDropTableChildObjectSql,
buildDuplicateTableStructureSql,
buildEmptyTableSql,
buildTruncateTableSql,
type DropTableChildObjectSqlOptions,
type DropObjectSqlOptions,
type TableChildObjectType,
type TableAdminSqlOptions,
} from "@/lib/dbAdminSql";
import { buildRenameObjectSql, supportsObjectRename, type RenameableObjectType } from "@/lib/objectRenameSql";
import { buildRoutineRenameObjectSourceStatements, supportsSourceBackedRoutineRename } from "@/lib/objectSourceEditor";
import { buildViewDdl } from "@/lib/viewDdl";
import { getTableStructureCapabilities } from "@/lib/tableStructureCapabilities";
import { hexToRgba } from "@/lib/color";
import { focusSidebarRenameInput } from "@/lib/sidebarRenameFocus";
import { hasTreeNodeDatabaseContext } from "@/lib/treeNodeContext";
@@ -450,6 +454,12 @@ function isEditableShortcutTarget(target: EventTarget | null): boolean {
function onKeydown(event: KeyboardEvent) {
if (!isSelected.value || isEditableShortcutTarget(event.target)) return;
if (!event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey && isDeleteTreeNodeShortcut(event)) {
if (!requestDropSelectedNode()) return;
event.preventDefault();
event.stopPropagation();
return;
}
const action = sidebarSelectionCopyAction(event);
if (action !== "copy-name") return;
event.preventDefault();
@@ -457,6 +467,10 @@ function onKeydown(event: KeyboardEvent) {
copyName();
}
function isDeleteTreeNodeShortcut(event: KeyboardEvent): boolean {
return event.key === "Delete" || event.key === "Backspace";
}
function onDoubleClick() {
const action = treeNodeRowDoubleClickAction(
props.node.type,
@@ -704,6 +718,7 @@ async function duplicateConnection() {
// --- Table Management Operations ---
const showDropTableConfirm = ref(false);
const showDropTableChildObjectConfirm = ref(false);
const showEmptyTableConfirm = ref(false);
const showTruncateTableConfirm = ref(false);
const showRenameObjectDialog = ref(false);
@@ -714,6 +729,7 @@ const dropTablePreviewSql = ref("");
const emptyTablePreviewSql = ref("");
const truncateTablePreviewSql = ref("");
const dropObjectPreviewSql = ref("");
const dropTableChildObjectPreviewSql = ref("");
const dropDatabasePreviewSql = ref("");
const dropSchemaPreviewSql = ref("");
const showDuplicateDialog = ref(false);
@@ -735,21 +751,114 @@ const showProcedureExecutionConfirm = ref(false);
function dropObjectSqlOptions(): DropObjectSqlOptions | null {
const node = props.node;
if (node.type !== "procedure" && node.type !== "function") return null;
if (node.type !== "view" && node.type !== "procedure" && node.type !== "function") return null;
return {
databaseType: currentDatabaseType(),
objectType: node.type === "procedure" ? "PROCEDURE" : "FUNCTION",
objectType: node.type === "view" ? "VIEW" : node.type === "procedure" ? "PROCEDURE" : "FUNCTION",
schema: node.schema,
name: node.label,
};
}
function tableChildDropObjectType(type: TreeNodeType): TableChildObjectType | null {
if (type === "column") return "COLUMN";
if (type === "index") return "INDEX";
if (type === "fkey") return "FOREIGN_KEY";
if (type === "trigger") return "TRIGGER";
return null;
}
function tableChildDropObjectName(node: TreeNode): string {
if (node.type === "column")
return node.meta && "name" in node.meta ? node.meta.name : node.label.replace(/\s+\(.+\)$/, "");
if (node.type === "index")
return node.meta && "name" in node.meta ? node.meta.name : node.label.replace(/\s+\(.+\)$/, "");
if (node.type === "fkey") return node.meta && "name" in node.meta ? node.meta.name : node.label;
if (node.type === "trigger")
return node.meta && "name" in node.meta ? node.meta.name : node.label.replace(/\s+\(.+\)$/, "");
return node.label;
}
function dropTableChildObjectSqlOptions(): DropTableChildObjectSqlOptions | null {
const node = props.node;
const objectType = tableChildDropObjectType(node.type);
if (!objectType || !node.tableName) return null;
const name = tableChildDropObjectName(node).trim();
if (!name) return null;
return {
databaseType: currentDatabaseType(),
objectType,
schema: node.schema,
tableName: node.tableName,
name,
};
}
const canDropTableChildObject = computed(() => {
const options = dropTableChildObjectSqlOptions();
if (!options) return false;
const capabilities = getTableStructureCapabilities(currentDatabaseType());
if (options.objectType === "COLUMN") return capabilities.dropColumn;
if (options.objectType === "INDEX") return capabilities.dropIndex;
return true;
});
function dropObjectMenuLabel(): string {
if (props.node.type === "view") return t("contextMenu.dropView");
if (props.node.type === "procedure") return t("contextMenu.dropProcedure");
if (props.node.type === "function") return t("contextMenu.dropFunction");
return t("contextMenu.dropObject");
}
function dropObjectConfirmTitle(): string {
if (props.node.type === "view") return t("contextMenu.confirmDropViewTitle");
if (props.node.type === "procedure") return t("contextMenu.confirmDropProcedureTitle");
if (props.node.type === "function") return t("contextMenu.confirmDropFunctionTitle");
return t("contextMenu.confirmDropObjectTitle");
}
function dropObjectConfirmMessage(): string {
if (props.node.type === "view") return t("contextMenu.confirmDropViewMessage", { name: props.node.label });
if (props.node.type === "procedure") return t("contextMenu.confirmDropProcedureMessage", { name: props.node.label });
if (props.node.type === "function") return t("contextMenu.confirmDropFunctionMessage", { name: props.node.label });
return t("contextMenu.confirmDropObjectMessage", { name: props.node.label });
}
function dropTableChildObjectMenuLabel(): string {
if (props.node.type === "column") return t("contextMenu.dropColumn");
if (props.node.type === "index") return t("contextMenu.dropIndex");
if (props.node.type === "fkey") return t("contextMenu.dropForeignKey");
if (props.node.type === "trigger") return t("contextMenu.dropTrigger");
return t("contextMenu.dropObject");
}
function dropTableChildObjectConfirmTitle(): string {
if (props.node.type === "column") return t("contextMenu.confirmDropColumnTitle");
if (props.node.type === "index") return t("contextMenu.confirmDropIndexTitle");
if (props.node.type === "fkey") return t("contextMenu.confirmDropForeignKeyTitle");
if (props.node.type === "trigger") return t("contextMenu.confirmDropTriggerTitle");
return t("contextMenu.confirmDropObjectTitle");
}
function dropTableChildObjectConfirmMessage(): string {
return t("contextMenu.confirmDropTableChildObjectMessage", {
name: tableChildDropObjectName(props.node),
table: props.node.tableName || "",
});
}
async function refreshDropObjectPreviewSql() {
const options = dropObjectSqlOptions();
dropObjectPreviewSql.value = "";
dropObjectPreviewSql.value = options ? await buildDropObjectSql(options).catch(() => "") : "";
}
async function refreshDropTableChildObjectPreviewSql() {
const options = dropTableChildObjectSqlOptions();
dropTableChildObjectPreviewSql.value = "";
dropTableChildObjectPreviewSql.value = options ? await buildDropTableChildObjectSql(options).catch(() => "") : "";
}
function viewObjectSource() {
const node = props.node;
if (!node.connectionId || !node.database) return;
@@ -828,6 +937,28 @@ function requestDropObject() {
showDropObjectConfirm.value = true;
}
function requestDropTableChildObject() {
if (!canDropTableChildObject.value) return;
void refreshDropTableChildObjectPreviewSql();
showDropTableChildObjectConfirm.value = true;
}
function requestDropSelectedNode(): boolean {
if (props.node.type === "table") {
dropTable();
return true;
}
if (props.node.type === "view" || props.node.type === "procedure" || props.node.type === "function") {
requestDropObject();
return true;
}
if (canDropTableChildObject.value) {
requestDropTableChildObject();
return true;
}
return false;
}
function nodeRenameObjectType(): RenameableObjectType | null {
if (props.node.type === "table") return "TABLE";
if (props.node.type === "view") return "VIEW";
@@ -944,9 +1075,34 @@ async function confirmDropObject() {
await connectionStore.ensureConnected(node.connectionId);
const sql = dropObjectPreviewSql.value || (await buildDropObjectSql(options));
await api.executeQuery(node.connectionId, node.database, sql, node.schema);
const msgKey = node.type === "procedure" ? "contextMenu.dropProcedureSuccess" : "contextMenu.dropFunctionSuccess";
const msgKey =
node.type === "view"
? "contextMenu.dropViewSuccess"
: node.type === "procedure"
? "contextMenu.dropProcedureSuccess"
: "contextMenu.dropFunctionSuccess";
toast(t(msgKey, { name: node.label }), 3000);
await refreshTableList(node);
if (node.type === "view") {
connectionStore.removeTreeNode(node.id);
} else {
await refreshTableList(node);
}
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
}
}
async function confirmDropTableChildObject() {
const node = props.node;
if (!node.connectionId || !node.database) return;
const options = dropTableChildObjectSqlOptions();
if (!options) return;
try {
await connectionStore.ensureConnected(node.connectionId);
const sql = dropTableChildObjectPreviewSql.value || (await buildDropTableChildObjectSql(options));
await api.executeQuery(node.connectionId, node.database, sql, node.schema);
toast(t("contextMenu.dropTableChildObjectSuccess", { name: options.name }), 3000);
connectionStore.removeTreeNode(node.id);
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
}
@@ -2190,6 +2346,14 @@ function treeItemMenuItems(): ContextMenuItem[] {
if (canRenameObject.value) {
items.push({ label: t("contextMenu.renameObject"), action: openRenameObjectDialog, icon: Pencil });
}
if (node.type === "view") {
items.push({
label: t("contextMenu.dropView"),
action: requestDropObject,
icon: Trash2,
variant: "destructive" as const,
});
}
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
if (canOpenDiagram.value) {
items.push({ label: t("diagram.open"), action: openDiagram, icon: Network });
@@ -2236,9 +2400,34 @@ function treeItemMenuItems(): ContextMenuItem[] {
// 7. Column
if (node.type === "column") {
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy });
if (canOpenFieldLineage.value) {
items.push({ label: "", separator: true });
items.push({ label: t("lineage.open"), action: openFieldLineage, icon: Network });
}
if (canDropTableChildObject.value) {
items.push({ label: "", separator: true });
items.push({
label: dropTableChildObjectMenuLabel(),
action: requestDropTableChildObject,
icon: Trash2,
variant: "destructive" as const,
});
}
return items;
}
if (node.type === "index" || node.type === "fkey" || node.type === "trigger") {
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy });
if (canDropTableChildObject.value) {
items.push({ label: "", separator: true });
items.push({
label: dropTableChildObjectMenuLabel(),
action: requestDropTableChildObject,
icon: Trash2,
variant: "destructive" as const,
});
}
return items;
}
@@ -2611,19 +2800,22 @@ function treeItemMenuItems(): ContextMenuItem[] {
<DangerConfirmDialog
v-model:open="showDropObjectConfirm"
:title="
node.type === 'procedure' ? t('contextMenu.confirmDropProcedureTitle') : t('contextMenu.confirmDropFunctionTitle')
"
:message="
node.type === 'procedure'
? t('contextMenu.confirmDropProcedureMessage', { name: node.label })
: t('contextMenu.confirmDropFunctionMessage', { name: node.label })
"
:title="dropObjectConfirmTitle()"
:message="dropObjectConfirmMessage()"
:sql="dropObjectPreviewSql"
:confirm-label="node.type === 'procedure' ? t('contextMenu.dropProcedure') : t('contextMenu.dropFunction')"
:confirm-label="dropObjectMenuLabel()"
@confirm="confirmDropObject"
/>
<DangerConfirmDialog
v-model:open="showDropTableChildObjectConfirm"
:title="dropTableChildObjectConfirmTitle()"
:message="dropTableChildObjectConfirmMessage()"
:sql="dropTableChildObjectPreviewSql"
:confirm-label="dropTableChildObjectMenuLabel()"
@confirm="confirmDropTableChildObject"
/>
<ProcedureExecutionDialog
v-if="node.type === 'procedure' && node.connectionId && node.database"
v-model:open="showProcedureExecutionConfirm"
+13
View File
@@ -938,7 +938,12 @@ export default {
editView: "Edit View",
viewSource: "View Source",
viewDdl: "View DDL",
dropObject: "Drop Object",
dropView: "Drop View",
dropColumn: "Drop Column",
dropIndex: "Drop Index",
dropForeignKey: "Drop Foreign Key",
dropTrigger: "Drop Trigger",
executeProcedure: "Execute Procedure",
confirmExecuteProcedureTitle: "Execute Procedure",
confirmExecuteProcedureMessage: 'Execute procedure "{name}"? You can fill or adjust parameter values first.',
@@ -965,6 +970,13 @@ export default {
dropFunction: "Drop Function",
confirmDropViewTitle: "Drop View",
confirmDropViewMessage: 'Are you sure you want to drop view "{name}"?',
confirmDropObjectTitle: "Drop Object",
confirmDropObjectMessage: 'Are you sure you want to drop "{name}"?',
confirmDropColumnTitle: "Drop Column",
confirmDropIndexTitle: "Drop Index",
confirmDropForeignKeyTitle: "Drop Foreign Key",
confirmDropTriggerTitle: "Drop Trigger",
confirmDropTableChildObjectMessage: 'Are you sure you want to drop "{name}" from "{table}"?',
confirmDropProcedureTitle: "Drop Procedure",
confirmDropProcedureMessage: 'Are you sure you want to drop procedure "{name}"?',
confirmDropFunctionTitle: "Drop Function",
@@ -972,6 +984,7 @@ export default {
dropViewSuccess: 'View "{name}" dropped',
dropProcedureSuccess: 'Procedure "{name}" dropped',
dropFunctionSuccess: 'Function "{name}" dropped',
dropTableChildObjectSuccess: '"{name}" dropped',
emptyTableSuccess: 'All data deleted from "{name}"',
truncateTableSuccess: 'Table "{name}" truncated',
duplicateStructureSuccess: 'Table structure duplicated as "{name}"',
+13
View File
@@ -830,7 +830,12 @@ export default {
editView: "Editar vista",
viewSource: "Ver código fuente",
viewDdl: "Ver DDL",
dropObject: "Eliminar objeto",
dropView: "Eliminar vista",
dropColumn: "Eliminar columna",
dropIndex: "Eliminar índice",
dropForeignKey: "Eliminar clave foránea",
dropTrigger: "Eliminar disparador",
executeProcedure: "Ejecutar procedimiento",
confirmExecuteProcedureTitle: "Ejecutar procedimiento",
confirmExecuteProcedureMessage:
@@ -858,6 +863,13 @@ export default {
dropFunction: "Eliminar función",
confirmDropViewTitle: "Eliminar vista",
confirmDropViewMessage: '¿Seguro que deseas eliminar la vista "{name}"?',
confirmDropObjectTitle: "Eliminar objeto",
confirmDropObjectMessage: '¿Seguro que deseas eliminar "{name}"?',
confirmDropColumnTitle: "Eliminar columna",
confirmDropIndexTitle: "Eliminar índice",
confirmDropForeignKeyTitle: "Eliminar clave foránea",
confirmDropTriggerTitle: "Eliminar disparador",
confirmDropTableChildObjectMessage: '¿Seguro que deseas eliminar "{name}" de "{table}"?',
confirmDropProcedureTitle: "Eliminar procedimiento",
confirmDropProcedureMessage: '¿Seguro que deseas eliminar el procedimiento "{name}"?',
confirmDropFunctionTitle: "Eliminar función",
@@ -865,6 +877,7 @@ export default {
dropViewSuccess: 'Vista "{name}" eliminada',
dropProcedureSuccess: 'Procedimiento "{name}" eliminado',
dropFunctionSuccess: 'Función "{name}" eliminada',
dropTableChildObjectSuccess: '"{name}" eliminado',
emptyTableSuccess: 'Todos los datos eliminados de "{name}"',
truncateTableSuccess: 'Tabla "{name}" truncada',
duplicateStructureSuccess: 'Estructura de tabla duplicada como "{name}"',
+13
View File
@@ -918,7 +918,12 @@ export default {
editView: "编辑视图",
viewSource: "查看源码",
viewDdl: "查看 DDL",
dropObject: "删除对象",
dropView: "删除视图",
dropColumn: "删除字段",
dropIndex: "删除索引",
dropForeignKey: "删除外键",
dropTrigger: "删除触发器",
executeProcedure: "执行过程",
confirmExecuteProcedureTitle: "执行存储过程",
confirmExecuteProcedureMessage: "确认执行存储过程「{name}」?可先补充或调整参数值。",
@@ -943,6 +948,13 @@ export default {
dropFunction: "删除函数",
confirmDropViewTitle: "删除视图",
confirmDropViewMessage: "确定要删除视图「{name}」吗?",
confirmDropObjectTitle: "删除对象",
confirmDropObjectMessage: "确定要删除「{name}」吗?",
confirmDropColumnTitle: "删除字段",
confirmDropIndexTitle: "删除索引",
confirmDropForeignKeyTitle: "删除外键",
confirmDropTriggerTitle: "删除触发器",
confirmDropTableChildObjectMessage: "确定要从「{table}」删除「{name}」吗?",
confirmDropProcedureTitle: "删除存储过程",
confirmDropProcedureMessage: "确定要删除存储过程「{name}」吗?",
confirmDropFunctionTitle: "删除函数",
@@ -950,6 +962,7 @@ export default {
dropViewSuccess: "视图「{name}」已删除",
dropProcedureSuccess: "存储过程「{name}」已删除",
dropFunctionSuccess: "函数「{name}」已删除",
dropTableChildObjectSuccess: "「{name}」已删除",
emptyTableSuccess: "已清空「{name}」的所有数据",
truncateTableSuccess: "表「{name}」已截断",
duplicateStructureSuccess: "已复制表结构为「{name}」",
+13
View File
@@ -918,7 +918,12 @@ export default {
editView: "編輯檢視",
viewSource: "檢視原始碼",
viewDdl: "檢視 DDL",
dropObject: "刪除物件",
dropView: "刪除檢視",
dropColumn: "刪除欄位",
dropIndex: "刪除索引",
dropForeignKey: "刪除外鍵",
dropTrigger: "刪除觸發器",
executeProcedure: "執行預存程序",
confirmExecuteProcedureTitle: "執行預存程序",
confirmExecuteProcedureMessage: "確認執行預存程序「{name}」?可先補充或調整參數值。",
@@ -943,6 +948,13 @@ export default {
dropFunction: "刪除函式",
confirmDropViewTitle: "刪除檢視",
confirmDropViewMessage: "確定要刪除檢視「{name}」嗎?",
confirmDropObjectTitle: "刪除物件",
confirmDropObjectMessage: "確定要刪除「{name}」嗎?",
confirmDropColumnTitle: "刪除欄位",
confirmDropIndexTitle: "刪除索引",
confirmDropForeignKeyTitle: "刪除外鍵",
confirmDropTriggerTitle: "刪除觸發器",
confirmDropTableChildObjectMessage: "確定要從「{table}」刪除「{name}」嗎?",
confirmDropProcedureTitle: "刪除預存程序",
confirmDropProcedureMessage: "確定要刪除預存程序「{name}」嗎?",
confirmDropFunctionTitle: "刪除函式",
@@ -950,6 +962,7 @@ export default {
dropViewSuccess: "檢視「{name}」已刪除",
dropProcedureSuccess: "預存程序「{name}」已刪除",
dropFunctionSuccess: "函式「{name}」已刪除",
dropTableChildObjectSuccess: "「{name}」已刪除",
emptyTableSuccess: "已清空「{name}」的所有資料",
truncateTableSuccess: "資料表「{name}」已截斷",
duplicateStructureSuccess: "已複製資料表結構為「{name}」",
+1
View File
@@ -106,6 +106,7 @@ export const buildCreateDatabaseSql = forward("buildCreateDatabaseSql");
export const buildDuckDbAttachDatabaseSql = forward("buildDuckDbAttachDatabaseSql");
export const buildDropObjectSql = forward("buildDropObjectSql");
export const buildDropTableSql = forward("buildDropTableSql");
export const buildDropTableChildObjectSql = forward("buildDropTableChildObjectSql");
export const buildEmptyTableSql = forward("buildEmptyTableSql");
export const buildTruncateTableSql = forward("buildTruncateTableSql");
export const buildDropDatabaseSql = forward("buildDropDatabaseSql");
+14
View File
@@ -14,6 +14,16 @@ export interface TableAdminSqlOptions {
tableName: string;
}
export type TableChildObjectType = "COLUMN" | "INDEX" | "FOREIGN_KEY" | "TRIGGER";
export interface DropTableChildObjectSqlOptions {
databaseType?: DatabaseType;
objectType: TableChildObjectType;
schema?: string | null;
tableName: string;
name: string;
}
export interface DatabaseNameSqlOptions {
databaseType?: DatabaseType;
name: string;
@@ -39,6 +49,10 @@ export function buildDropTableSql(options: TableAdminSqlOptions): Promise<string
return api.buildDropTableSql(options);
}
export function buildDropTableChildObjectSql(options: DropTableChildObjectSqlOptions): Promise<string> {
return api.buildDropTableChildObjectSql(options);
}
export function buildEmptyTableSql(options: TableAdminSqlOptions): Promise<string> {
return api.buildEmptyTableSql(options);
}
+5
View File
@@ -81,6 +81,7 @@ import type { BuildRenameObjectSqlOptions } from "@/lib/objectRenameSql";
import type { CreateDatabaseSqlOptions } from "@/lib/createDatabaseSql";
import type {
DatabaseNameSqlOptions,
DropTableChildObjectSqlOptions,
DropObjectSqlOptions,
DuplicateTableStructureSqlOptions,
SchemaNameSqlOptions,
@@ -588,6 +589,10 @@ export async function buildDropTableSql(options: TableAdminSqlOptions): Promise<
return post("/api/query/build-drop-table-sql", { options });
}
export async function buildDropTableChildObjectSql(options: DropTableChildObjectSqlOptions): Promise<string> {
return post("/api/query/build-drop-table-child-object-sql", { options });
}
export async function buildEmptyTableSql(options: TableAdminSqlOptions): Promise<string> {
return post("/api/query/build-empty-table-sql", { options });
}
+5
View File
@@ -54,6 +54,7 @@ import type { BuildRenameObjectSqlOptions } from "@/lib/objectRenameSql";
import type { CreateDatabaseSqlOptions } from "@/lib/createDatabaseSql";
import type {
DatabaseNameSqlOptions,
DropTableChildObjectSqlOptions,
DropObjectSqlOptions,
DuplicateTableStructureSqlOptions,
SchemaNameSqlOptions,
@@ -580,6 +581,10 @@ export async function buildDropTableSql(options: TableAdminSqlOptions): Promise<
return invoke("build_drop_table_sql", { options });
}
export async function buildDropTableChildObjectSql(options: DropTableChildObjectSqlOptions): Promise<string> {
return invoke("build_drop_table_child_object_sql", { options });
}
export async function buildEmptyTableSql(options: TableAdminSqlOptions): Promise<string> {
return invoke("build_empty_table_sql", { options });
}
@@ -1170,6 +1170,7 @@ export const useConnectionStore = defineStore("connection", () => {
connectionId,
database,
schema,
tableName: table,
meta: idx,
})),
);
@@ -1210,6 +1211,7 @@ export const useConnectionStore = defineStore("connection", () => {
connectionId,
database,
schema,
tableName: table,
meta: fk,
})),
);
@@ -1244,6 +1246,7 @@ export const useConnectionStore = defineStore("connection", () => {
connectionId,
database,
schema,
tableName: table,
meta: tr,
})),
);