feat(postgres): support batch table cascade actions

This commit is contained in:
t8y2 committed 2026-07-07 11:57:44 +08:00
1 parent a893f0e478
commit 1db11ea145
11 files changed
+353 -19

No files matched your search

@@ -52,7 +52,7 @@ import { isSchemaAware } from "@/lib/database/databaseCapabilities";
import { supportsSchemaDiagram, supportsTableImport, supportsTableStructureEditing, supportsTableTruncate } from "@/lib/database/databaseFeatureSupport";
import { codeMirrorSqlDialect, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { buildTableSelectSql } from "@/lib/table/tableSelectSql";
import { buildDropObjectSql, buildDropTableSql, buildDuplicateTableStructureSql, buildCopyTableDataSql, buildEmptyTableSql, buildTruncateTableSql, supportsDropTableCascade, type TableAdminSqlOptions } from "@/lib/database/dbAdminSql";
import { buildDropObjectSql, buildDropTableSql, buildDuplicateTableStructureSql, buildCopyTableDataSql, buildEmptyTableSql, buildTruncateTableSql, supportsDropTableCascade, supportsTruncateTableCascade, type TableAdminSqlOptions } from "@/lib/database/dbAdminSql";
import { useToast } from "@/composables/useToast";
import { buildExecutableObjectSourceStatements, buildRoutineRenameObjectSourceStatements, executeObjectSourceSave, supportsSourceBackedRoutineRename } from "@/lib/table/objectSourceEditor";
import { buildRenameObjectSql, supportsObjectRename } from "@/lib/table/objectRenameSql";
@@ -134,6 +134,7 @@ const showDropConfirm = ref(false);
const dropTarget = ref<ObjectBrowserRow | null>(null);
const dropPreviewSql = ref("");
const dropTableCascade = ref(false);
const batchDropCascade = ref(false);
const showRenameDialog = ref(false);
const renameTarget = ref<ObjectBrowserRow | null>(null);
const renameInput = ref("");
@@ -142,6 +143,7 @@ const renamePreviewSqlText = ref("");
const showTruncateConfirm = ref(false);
const truncateTarget = ref<ObjectBrowserRow | null>(null);
const truncatePreviewSql = ref("");
const truncateTableCascade = ref(false);
const showEmptyConfirm = ref(false);
const emptyTarget = ref<ObjectBrowserRow | null>(null);
const emptyPreviewSql = ref("");
@@ -156,6 +158,9 @@ const selectedTableIds = ref<Set<string>>(new Set());
const expandedPartitionParentIds = ref<Set<string>>(new Set());
const showBatchDropConfirm = ref(false);
const batchDropPreviewSql = ref("");
const showBatchTruncateConfirm = ref(false);
const batchTruncatePreviewSql = ref("");
const batchTruncateCascade = ref(false);
// Paste table dialog state
const showPasteDialog = ref(false);
const pasteTableMode = ref<PasteTableMode>("structure-and-data");
@@ -179,6 +184,7 @@ const { addTask: addExportTask } = useExportTracker();
const needsSchema = computed(() => isSchemaAware(props.connection.db_type) && !connectionUsesDatabaseObjectTreeMode(props.connection));
const canDropTargetCascade = computed(() => dropTarget.value?.type === "TABLE" && supportsDropTableCascade(effectiveDatabaseType.value));
const canTruncateTargetCascade = computed(() => !!truncateTarget.value && supportsTruncateTableCascade(effectiveDatabaseType.value));
const tableCount = computed(() => rows.value.filter((row) => row.type === "TABLE").length);
const viewCount = computed(() => rows.value.filter((row) => row.type === "VIEW").length);
const materializedViewCount = computed(() => rows.value.filter((row) => row.type === "MATERIALIZED_VIEW").length);
@@ -249,6 +255,8 @@ const selectedTableRows = computed(() => {
return selectableRows.value.filter((row) => ids.has(row.id));
});
const selectedTableCount = computed(() => selectedTableRows.value.length);
const canBatchDropCascade = computed(() => selectedTableCount.value > 0 && supportsDropTableCascade(effectiveDatabaseType.value));
const canBatchTruncateCascade = computed(() => selectedTableCount.value > 0 && supportsTruncateTableCascade(effectiveDatabaseType.value));
const allVisibleTablesSelected = computed(() => visibleSelectableRows.value.length > 0 && visibleSelectableRows.value.every((row) => selectedTableIds.value.has(row.id)));
function iconFor(row: ObjectBrowserRow) {
@@ -798,8 +806,9 @@ async function fetchSortedTableRowsForDrop(): Promise<ObjectBrowserRow[]> {
async function refreshBatchDropPreviewSql() {
const statements: string[] = [];
const sortedRows = await fetchSortedTableRowsForDrop();
const useCascade = canBatchDropCascade.value && batchDropCascade.value;
for (const row of sortedRows) {
const sql = await buildDropTableSql(tableAdminSqlOptions(row)).catch(() => "");
const sql = await buildDropTableSql(tableAdminSqlOptions(row, { cascade: useCascade })).catch(() => "");
if (sql) statements.push(sql);
}
batchDropPreviewSql.value = statements.join("\n");
@@ -807,6 +816,7 @@ async function refreshBatchDropPreviewSql() {
function requestBatchDropTables() {
if (selectedTableCount.value === 0) return;
batchDropCascade.value = false;
batchDropPreviewSql.value = "";
void refreshBatchDropPreviewSql();
showBatchDropConfirm.value = true;
@@ -816,8 +826,9 @@ async function confirmBatchDropTables() {
const targets = await fetchSortedTableRowsForDrop();
if (targets.length === 0) return;
try {
const useCascade = canBatchDropCascade.value && batchDropCascade.value;
for (const row of targets) {
const sql = await buildDropTableSql(tableAdminSqlOptions(row));
const sql = await buildDropTableSql(tableAdminSqlOptions(row, { cascade: useCascade }));
await api.executeQuery(props.connection.id, props.database, sql);
closeDroppedTableObjectTabsForRow(row);
}
@@ -830,6 +841,43 @@ async function confirmBatchDropTables() {
}
}
async function refreshBatchTruncatePreviewSql() {
const statements: string[] = [];
const useCascade = canBatchTruncateCascade.value && batchTruncateCascade.value;
for (const row of selectedTableRows.value) {
const sql = await buildTruncateTableSql(tableAdminSqlOptions(row, { cascade: useCascade })).catch(() => "");
if (sql) statements.push(sql);
}
batchTruncatePreviewSql.value = statements.join("\n");
}
function requestBatchTruncateTables() {
if (selectedTableCount.value === 0 || !supportsTruncateTable.value) return;
batchTruncateCascade.value = false;
batchTruncatePreviewSql.value = "";
void refreshBatchTruncatePreviewSql();
showBatchTruncateConfirm.value = true;
}
async function confirmBatchTruncateTables() {
const targets = [...selectedTableRows.value];
if (targets.length === 0) return;
try {
const useCascade = canBatchTruncateCascade.value && batchTruncateCascade.value;
for (const row of targets) {
const sql = await buildTruncateTableSql(tableAdminSqlOptions(row, { cascade: useCascade }));
await api.executeQuery(props.connection.id, props.database, sql);
}
toast(t("objects.batchTruncateSuccess", { count: targets.length }));
clearTableSelection();
showBatchTruncateConfirm.value = false;
await reload();
await connectionStore.refreshObjectListTreeNode(props.connection.id, props.database, selectedSchema.value);
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
}
}
async function exportStructure(row: ObjectBrowserRow) {
try {
const schema = row.schema || selectedSchema.value || props.database;
@@ -1136,11 +1184,12 @@ function tableAdminSqlOptions(row: ObjectBrowserRow, options?: { cascade?: boole
async function refreshTruncatePreviewSql(row: ObjectBrowserRow) {
truncatePreviewSql.value = "";
truncatePreviewSql.value = await buildTruncateTableSql(tableAdminSqlOptions(row)).catch(() => "");
truncatePreviewSql.value = await buildTruncateTableSql(tableAdminSqlOptions(row, { cascade: canTruncateTargetCascade.value && truncateTableCascade.value })).catch(() => "");
}
function requestTruncateTable(row: ObjectBrowserRow) {
truncateTarget.value = row;
truncateTableCascade.value = false;
void refreshTruncatePreviewSql(row);
showTruncateConfirm.value = true;
}
@@ -1149,7 +1198,7 @@ async function confirmTruncateTable() {
const row = truncateTarget.value;
if (!row) return;
try {
const sql = truncatePreviewSql.value || (await buildTruncateTableSql(tableAdminSqlOptions(row)));
const sql = truncatePreviewSql.value || (await buildTruncateTableSql(tableAdminSqlOptions(row, { cascade: canTruncateTargetCascade.value && truncateTableCascade.value })));
await api.executeQuery(props.connection.id, props.database, sql);
toast(t("contextMenu.truncateTableSuccess", { name: row.name }));
} catch (e: any) {
@@ -1604,7 +1653,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
{{ t("objects.pasteTableSelected") }}
</Button>
</div>
<div v-if="selectedTableCount > 0" class="flex h-9 shrink-0 items-center gap-2 border-b bg-muted/30 px-3 text-xs">
<div v-if="selectedTableCount > 0" class="flex h-9 shrink-0 items-center gap-2 overflow-x-auto border-b bg-muted/30 px-3 text-xs">
<div class="min-w-0 flex-1 truncate text-muted-foreground">
{{ t("objects.selectedTables", { count: selectedTableCount }) }}
</div>
@@ -1616,6 +1665,10 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
<Clipboard class="mr-1.5 h-3.5 w-3.5" />
{{ t("objects.copyTableSelected") }}
</Button>
<Button v-if="supportsTruncateTable" variant="ghost" size="sm" class="h-7 px-2 text-xs text-destructive" @click="requestBatchTruncateTables">
<Scissors class="mr-1.5 h-3.5 w-3.5" />
{{ t("objects.truncateSelected") }}
</Button>
<Button variant="ghost" size="sm" class="h-7 px-2 text-xs text-destructive" @click="requestBatchDropTables">
<Trash2 class="mr-1.5 h-3.5 w-3.5" />
{{ t("objects.dropSelected") }}
@@ -1848,7 +1901,36 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
</template>
</DangerConfirmDialog>
<DangerConfirmDialog v-model:open="showBatchDropConfirm" :title="t('objects.confirmBatchDropTitle')" :message="t('objects.confirmBatchDropMessage', { count: selectedTableCount })" :sql="batchDropPreviewSql" :confirm-label="t('objects.dropSelected')" @confirm="confirmBatchDropTables" />
<DangerConfirmDialog v-model:open="showBatchDropConfirm" :title="t('objects.confirmBatchDropTitle')" :message="t('objects.confirmBatchDropMessage', { count: selectedTableCount })" :sql="batchDropPreviewSql" :confirm-label="t('objects.dropSelected')" @confirm="confirmBatchDropTables">
<template v-if="canBatchDropCascade" #options>
<label class="mb-3 flex items-start gap-2 rounded-md border bg-muted/20 px-3 py-2 text-sm">
<input v-model="batchDropCascade" type="checkbox" class="mt-0.5 h-3.5 w-3.5 shrink-0 accent-primary" @change="refreshBatchDropPreviewSql()" />
<span class="grid gap-0.5">
<span class="font-medium text-foreground">{{ t("contextMenu.dropTableCascade") }}</span>
<span class="text-xs leading-5 text-muted-foreground">{{ t("contextMenu.dropTableCascadeHint") }}</span>
</span>
</label>
</template>
</DangerConfirmDialog>
<DangerConfirmDialog
v-model:open="showBatchTruncateConfirm"
:title="t('objects.confirmBatchTruncateTitle')"
:message="t('objects.confirmBatchTruncateMessage', { count: selectedTableCount })"
:sql="batchTruncatePreviewSql"
:confirm-label="t('objects.truncateSelected')"
@confirm="confirmBatchTruncateTables"
>
<template v-if="canBatchTruncateCascade" #options>
<label class="mb-3 flex items-start gap-2 rounded-md border bg-muted/20 px-3 py-2 text-sm">
<input v-model="batchTruncateCascade" type="checkbox" class="mt-0.5 h-3.5 w-3.5 shrink-0 accent-primary" @change="refreshBatchTruncatePreviewSql()" />
<span class="grid gap-0.5">
<span class="font-medium text-foreground">{{ t("contextMenu.truncateTableCascade") }}</span>
<span class="text-xs leading-5 text-muted-foreground">{{ t("contextMenu.truncateTableCascadeHint") }}</span>
</span>
</label>
</template>
</DangerConfirmDialog>
<Dialog v-model:open="showRenameDialog">
<DialogContent class="sm:max-w-[420px]">
@@ -1876,7 +1958,17 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
:sql="truncatePreviewSql"
:confirm-label="t('contextMenu.truncateTable')"
@confirm="confirmTruncateTable"
/>
>
<template v-if="canTruncateTargetCascade" #options>
<label class="mb-3 flex items-start gap-2 rounded-md border bg-muted/20 px-3 py-2 text-sm">
<input v-model="truncateTableCascade" type="checkbox" class="mt-0.5 h-3.5 w-3.5 shrink-0 accent-primary" @change="truncateTarget && refreshTruncatePreviewSql(truncateTarget)" />
<span class="grid gap-0.5">
<span class="font-medium text-foreground">{{ t("contextMenu.truncateTableCascade") }}</span>
<span class="text-xs leading-5 text-muted-foreground">{{ t("contextMenu.truncateTableCascadeHint") }}</span>
</span>
</label>
</template>
</DangerConfirmDialog>
<DangerConfirmDialog v-model:open="showEmptyConfirm" :title="t('contextMenu.confirmEmptyTableTitle')" :message="t('contextMenu.confirmEmptyTableMessage', { name: emptyTarget?.name ?? '' })" :sql="emptyPreviewSql" :confirm-label="t('contextMenu.emptyTable')" @confirm="confirmEmptyTable" />
@@ -119,6 +119,7 @@ import {
buildEmptyTableSql,
buildTruncateTableSql,
supportsDropTableCascade,
supportsTruncateTableCascade,
supportsSchemaComment,
type DropTableChildObjectSqlOptions,
type DropObjectSqlOptions,
@@ -1704,6 +1705,7 @@ async function duplicateConnection() {
const showDropTableConfirm = ref(false);
const showDropTableChildObjectConfirm = ref(false);
const showBatchDropConfirm = ref(false);
const showBatchTruncateConfirm = ref(false);
const showStructurePreviewDialog = ref(false);
const showStructureDocCopyDialog = ref(false);
const structurePreviewSql = ref("");
@@ -1721,11 +1723,15 @@ const renameObjectError = ref("");
const renameObjectPreviewSql = ref("");
const dropTablePreviewSql = ref("");
const dropTableCascade = ref(false);
const batchDropCascade = ref(false);
const emptyTablePreviewSql = ref("");
const truncateTablePreviewSql = ref("");
const truncateTableCascade = ref(false);
const dropObjectPreviewSql = ref("");
const dropTableChildObjectPreviewSql = ref("");
const batchDropPreviewSql = ref("");
const batchTruncatePreviewSql = ref("");
const batchTruncateCascade = ref(false);
const dropDatabasePreviewSql = ref("");
const dropSchemaPreviewSql = ref("");
const showDuplicateDialog = ref(false);
@@ -2019,6 +2025,16 @@ function selectedBatchDropTargets(): TreeNode[] {
return selected;
}
function selectedBatchTableTargets(): TreeNode[] {
const targets = selectedBatchDropTargets();
return targets.length > 1 && targets.every((node) => node.type === "table") ? targets : [];
}
function selectedBatchTruncateTargets(): TreeNode[] {
const targets = selectedBatchTableTargets();
return targets.every((node) => supportsTableTruncate(databaseTypeForNode(node))) ? targets : [];
}
function selectedBatchMongoIndexTargets(): TreeNode[] {
const targets = selectedBatchDropTargets();
return targets.length > 1 && targets.every((node) => canDropMongoIndexNode(node)) ? targets : [];
@@ -2056,12 +2072,25 @@ function batchDropConfirmMessage(): string {
return t("contextMenu.confirmBatchDropMessage", { count: targets.length });
}
async function dropSqlForTreeNode(node: TreeNode): Promise<string | null> {
function batchTruncateMenuLabel(): string {
return t("contextMenu.batchTruncate", { count: selectedBatchTruncateTargets().length });
}
function batchTruncateConfirmTitle(): string {
return t("contextMenu.confirmBatchTruncateTitle", { count: selectedBatchTruncateTargets().length });
}
function batchTruncateConfirmMessage(): string {
return t("contextMenu.confirmBatchTruncateMessage", { count: selectedBatchTruncateTargets().length });
}
async function dropSqlForTreeNode(node: TreeNode, options?: { cascade?: boolean }): Promise<string | null> {
if (node.type === "table" && node.connectionId && node.database) {
return buildDropTableSql({
databaseType: databaseTypeForNode(node),
schema: node.schema,
tableName: node.label,
cascade: options?.cascade && supportsDropTableCascade(databaseTypeForNode(node)),
});
}
const objectOptions = dropObjectSqlOptionsForNode(node);
@@ -2074,6 +2103,16 @@ async function dropSqlForTreeNode(node: TreeNode): Promise<string | null> {
return null;
}
async function truncateSqlForTreeNode(node: TreeNode, options?: { cascade?: boolean }): Promise<string | null> {
if (node.type !== "table" || !node.connectionId || !node.database || !supportsTableTruncate(databaseTypeForNode(node))) return null;
return buildTruncateTableSql({
databaseType: databaseTypeForNode(node),
schema: node.schema,
tableName: node.label,
cascade: options?.cascade && supportsTruncateTableCascade(databaseTypeForNode(node)),
});
}
async function refreshBatchDropPreviewSql() {
const targets = selectedBatchDropTargets();
const mongoIndexTargets = selectedBatchMongoIndexTargets();
@@ -2082,19 +2121,39 @@ async function refreshBatchDropPreviewSql() {
return;
}
const statements: string[] = [];
const useCascade = canBatchDropCascade.value && batchDropCascade.value;
for (const target of targets) {
const sql = await dropSqlForTreeNode(target);
const sql = await dropSqlForTreeNode(target, { cascade: useCascade });
if (sql) statements.push(sql);
}
batchDropPreviewSql.value = statements.join("\n");
}
async function refreshBatchTruncatePreviewSql() {
const targets = selectedBatchTruncateTargets();
const statements: string[] = [];
const useCascade = canBatchTruncateCascade.value && batchTruncateCascade.value;
for (const target of targets) {
const sql = await truncateSqlForTreeNode(target, { cascade: useCascade });
if (sql) statements.push(sql);
}
batchTruncatePreviewSql.value = statements.join("\n");
}
function requestBatchDrop() {
if (!selectedBatchDropTargets().length) return;
batchDropCascade.value = false;
void refreshBatchDropPreviewSql();
showBatchDropConfirm.value = true;
}
function requestBatchTruncate() {
if (!selectedBatchTruncateTargets().length) return;
batchTruncateCascade.value = false;
void refreshBatchTruncatePreviewSql();
showBatchTruncateConfirm.value = true;
}
function requestDropSelectedNodes(): boolean {
const selected = selectedTreeNodesInVisibleOrder();
if (selected.length > 1 && selected.some((node) => node.id === props.node.id)) {
@@ -2287,10 +2346,11 @@ async function confirmBatchDrop() {
showBatchDropConfirm.value = false;
return;
}
const useCascade = canBatchDropCascade.value && batchDropCascade.value;
for (const target of targets) {
if (!target.connectionId || !target.database) continue;
await connectionStore.ensureConnected(target.connectionId);
const sql = await dropSqlForTreeNode(target);
const sql = await dropSqlForTreeNode(target, { cascade: useCascade });
if (!sql) continue;
await api.executeQuery(target.connectionId, target.database, sql, target.schema);
closeDroppedTableObjectTabsForNode(target);
@@ -2303,6 +2363,25 @@ async function confirmBatchDrop() {
}
}
async function confirmBatchTruncate() {
const targets = selectedBatchTruncateTargets();
if (!targets.length) return;
try {
const useCascade = canBatchTruncateCascade.value && batchTruncateCascade.value;
for (const target of targets) {
if (!target.connectionId || !target.database) continue;
await connectionStore.ensureConnected(target.connectionId);
const sql = await truncateSqlForTreeNode(target, { cascade: useCascade });
if (!sql) continue;
await api.executeQuery(target.connectionId, target.database, sql, target.schema);
}
toast(t("contextMenu.batchTruncateSuccess", { count: targets.length }), 3000);
showBatchTruncateConfirm.value = false;
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
}
}
const isTableNotView = computed(() => props.node.type === "table" && !isSqlServerLinkedNode(props.node));
const supportsTruncate = computed(() => {
@@ -2410,6 +2489,15 @@ const canEditSchemaComment = computed(() => {
});
const canDropTableCascade = computed(() => props.node.type === "table" && supportsDropTableCascade(currentDatabaseType()));
const canTruncateTableCascade = computed(() => props.node.type === "table" && supportsTruncateTableCascade(currentDatabaseType()));
const canBatchDropCascade = computed(() => {
const targets = selectedBatchTableTargets();
return targets.length > 1 && targets.every((node) => supportsDropTableCascade(databaseTypeForNode(node)));
});
const canBatchTruncateCascade = computed(() => {
const targets = selectedBatchTruncateTargets();
return targets.length > 1 && targets.every((node) => supportsTruncateTableCascade(databaseTypeForNode(node)));
});
function tableAdminSqlOptions(options?: { cascade?: boolean }): TableAdminSqlOptions {
const result: TableAdminSqlOptions = {
@@ -2425,6 +2513,10 @@ function dropTableSqlOptions(): TableAdminSqlOptions {
return tableAdminSqlOptions({ cascade: canDropTableCascade.value && dropTableCascade.value });
}
function truncateTableSqlOptions(): TableAdminSqlOptions {
return tableAdminSqlOptions({ cascade: canTruncateTableCascade.value && truncateTableCascade.value });
}
async function refreshDropTablePreviewSql() {
dropTablePreviewSql.value = "";
dropTablePreviewSql.value = await buildDropTableSql(dropTableSqlOptions()).catch(() => "");
@@ -2437,7 +2529,7 @@ async function refreshEmptyTablePreviewSql() {
async function refreshTruncateTablePreviewSql() {
truncateTablePreviewSql.value = "";
truncateTablePreviewSql.value = await buildTruncateTableSql(tableAdminSqlOptions()).catch(() => "");
truncateTablePreviewSql.value = await buildTruncateTableSql(truncateTableSqlOptions()).catch(() => "");
}
function dropTable() {
@@ -2485,6 +2577,7 @@ async function confirmEmptyTable() {
}
function truncateTable() {
truncateTableCascade.value = false;
void refreshTruncateTablePreviewSql();
showTruncateTableConfirm.value = true;
}
@@ -2494,7 +2587,7 @@ async function confirmTruncateTable() {
if (!node.connectionId || !node.database) return;
try {
await connectionStore.ensureConnected(node.connectionId);
const sql = truncateTablePreviewSql.value || (await buildTruncateTableSql(tableAdminSqlOptions()));
const sql = truncateTablePreviewSql.value || (await buildTruncateTableSql(truncateTableSqlOptions()));
await api.executeQuery(node.connectionId, node.database, sql, node.schema);
toast(t("contextMenu.truncateTableSuccess", { name: node.label }), 3000);
} catch (e: any) {
@@ -4328,8 +4421,11 @@ function treeItemMenuItems(): ContextMenuItem[] {
const node = props.node;
const items: ContextMenuItem[] = [];
const batchDropCount = selectedBatchDropTargets().length;
const batchTruncateCount = selectedBatchTruncateTargets().length;
const deleteMenuLabel = (singleLabel: string) => (batchDropCount > 1 ? batchDropMenuLabel() : singleLabel);
const deleteMenuAction = (singleAction: () => void) => (batchDropCount > 1 ? requestBatchDrop : singleAction);
const truncateMenuLabel = (singleLabel: string) => (batchTruncateCount > 1 ? batchTruncateMenuLabel() : singleLabel);
const truncateMenuAction = (singleAction: () => void) => (batchTruncateCount > 1 ? requestBatchTruncate : singleAction);
// 1. Pin toggle
if (canPin.value) {
@@ -4739,8 +4835,8 @@ function treeItemMenuItems(): ContextMenuItem[] {
items.push({ label: t("contextMenu.copyTable"), action: copyTableToClipboard, icon: Copy });
if (supportsTruncate.value) {
destructiveActions.push({
label: t("contextMenu.truncateTable"),
action: truncateTable,
label: truncateMenuLabel(t("contextMenu.truncateTable")),
action: truncateMenuAction(truncateTable),
icon: Scissors,
variant: "destructive" as const,
});
@@ -5207,13 +5303,45 @@ function treeItemMenuItems(): ContextMenuItem[] {
:sql="truncateTablePreviewSql"
:confirm-label="t('contextMenu.truncateTable')"
@confirm="confirmTruncateTable"
/>
>
<template v-if="canTruncateTableCascade" #options>
<label class="mb-3 flex items-start gap-2 rounded-md border bg-muted/20 px-3 py-2 text-sm">
<input v-model="truncateTableCascade" type="checkbox" class="mt-0.5 h-3.5 w-3.5 shrink-0 accent-primary" @change="refreshTruncateTablePreviewSql()" />
<span class="grid gap-0.5">
<span class="font-medium text-foreground">{{ t("contextMenu.truncateTableCascade") }}</span>
<span class="text-xs leading-5 text-muted-foreground">{{ t("contextMenu.truncateTableCascadeHint") }}</span>
</span>
</label>
</template>
</DangerConfirmDialog>
<DangerConfirmDialog v-model:open="showDropObjectConfirm" :title="dropObjectConfirmTitle()" :message="dropObjectConfirmMessage()" :sql="dropObjectPreviewSql" :confirm-label="dropObjectMenuLabel()" @confirm="confirmDropObject" />
<DangerConfirmDialog v-model:open="showDropTableChildObjectConfirm" :title="dropTableChildObjectConfirmTitle()" :message="dropTableChildObjectConfirmMessage()" :sql="dropTableChildObjectPreviewSql" :confirm-label="dropTableChildObjectMenuLabel()" @confirm="confirmDropTableChildObject" />
<DangerConfirmDialog v-model:open="showBatchDropConfirm" :title="batchDropConfirmTitle()" :message="batchDropConfirmMessage()" :sql="batchDropPreviewSql" :confirm-label="batchDropMenuLabel()" @confirm="confirmBatchDrop" />
<DangerConfirmDialog v-model:open="showBatchDropConfirm" :title="batchDropConfirmTitle()" :message="batchDropConfirmMessage()" :sql="batchDropPreviewSql" :confirm-label="batchDropMenuLabel()" @confirm="confirmBatchDrop">
<template v-if="canBatchDropCascade" #options>
<label class="mb-3 flex items-start gap-2 rounded-md border bg-muted/20 px-3 py-2 text-sm">
<input v-model="batchDropCascade" type="checkbox" class="mt-0.5 h-3.5 w-3.5 shrink-0 accent-primary" @change="refreshBatchDropPreviewSql()" />
<span class="grid gap-0.5">
<span class="font-medium text-foreground">{{ t("contextMenu.dropTableCascade") }}</span>
<span class="text-xs leading-5 text-muted-foreground">{{ t("contextMenu.dropTableCascadeHint") }}</span>
</span>
</label>
</template>
</DangerConfirmDialog>
<DangerConfirmDialog v-model:open="showBatchTruncateConfirm" :title="batchTruncateConfirmTitle()" :message="batchTruncateConfirmMessage()" :sql="batchTruncatePreviewSql" :confirm-label="batchTruncateMenuLabel()" @confirm="confirmBatchTruncate">
<template v-if="canBatchTruncateCascade" #options>
<label class="mb-3 flex items-start gap-2 rounded-md border bg-muted/20 px-3 py-2 text-sm">
<input v-model="batchTruncateCascade" type="checkbox" class="mt-0.5 h-3.5 w-3.5 shrink-0 accent-primary" @change="refreshBatchTruncatePreviewSql()" />
<span class="grid gap-0.5">
<span class="font-medium text-foreground">{{ t("contextMenu.truncateTableCascade") }}</span>
<span class="text-xs leading-5 text-muted-foreground">{{ t("contextMenu.truncateTableCascadeHint") }}</span>
</span>
</label>
</template>
</DangerConfirmDialog>
<ProcedureExecutionDialog
v-if="node.type === 'procedure' && node.connectionId && node.database"