feat: support emptying multiple selected tables
* Feature: support emptying multiple selected tables * ci: retrigger checks * Fix batch empty safety and ClickHouse feedback --------- Co-authored-by: zipg <4047349+zipg@users.noreply.github.com>
This commit is contained in:
1 parent
903ab26c9b
commit
7b2f969356
10 files changed
+204
-3
No files matched your search
@@ -146,6 +146,7 @@ import { connectionPasteTargetGroupId, selectedConnectionClipboardTargets, selec
|
||||
import { supportsDatabaseUserAdmin } from "@/lib/database/databaseUserAdmin";
|
||||
import { canCloseSidebarDatabaseConnection, isSidebarDatabaseOpened } from "@/lib/sidebar/sidebarDatabaseOpenState";
|
||||
import { sidebarTreeContextKey } from "@/lib/sidebar/sidebarTreeContext";
|
||||
import { batchTableEmptyFeedback, runBatchTableEmpty } from "@/lib/sidebar/batchTableEmpty";
|
||||
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
|
||||
import ProcedureExecutionDialog from "@/components/objects/ProcedureExecutionDialog.vue";
|
||||
import InstallExtensionDialog from "@/components/objects/InstallExtensionDialog.vue";
|
||||
@@ -1747,6 +1748,7 @@ async function duplicateConnection() {
|
||||
const showDropTableConfirm = ref(false);
|
||||
const showDropTableChildObjectConfirm = ref(false);
|
||||
const showBatchDropConfirm = ref(false);
|
||||
const showBatchEmptyConfirm = ref(false);
|
||||
const showBatchTruncateConfirm = ref(false);
|
||||
const showStructurePreviewDialog = ref(false);
|
||||
const showStructureDocCopyDialog = ref(false);
|
||||
@@ -1772,6 +1774,8 @@ const truncateTableCascade = ref(false);
|
||||
const dropObjectPreviewSql = ref("");
|
||||
const dropTableChildObjectPreviewSql = ref("");
|
||||
const batchDropPreviewSql = ref("");
|
||||
const batchEmptyPreviewSql = ref("");
|
||||
const batchEmptyTargets = ref<TreeNode[]>([]);
|
||||
const batchTruncatePreviewSql = ref("");
|
||||
const batchTruncateCascade = ref(false);
|
||||
const dropDatabasePreviewSql = ref("");
|
||||
@@ -2077,6 +2081,10 @@ function selectedBatchTruncateTargets(): TreeNode[] {
|
||||
return targets.every((node) => supportsTableTruncate(databaseTypeForNode(node))) ? targets : [];
|
||||
}
|
||||
|
||||
function selectedBatchEmptyTargets(): TreeNode[] {
|
||||
return selectedBatchTableTargets();
|
||||
}
|
||||
|
||||
function selectedBatchMongoIndexTargets(): TreeNode[] {
|
||||
const targets = selectedBatchDropTargets();
|
||||
return targets.length > 1 && targets.every((node) => canDropMongoIndexNode(node)) ? targets : [];
|
||||
@@ -2118,6 +2126,22 @@ function batchTruncateMenuLabel(): string {
|
||||
return t("contextMenu.batchTruncate", { count: selectedBatchTruncateTargets().length });
|
||||
}
|
||||
|
||||
function batchEmptyMenuLabel(): string {
|
||||
return t("contextMenu.batchEmpty", { count: selectedBatchEmptyTargets().length });
|
||||
}
|
||||
|
||||
function batchEmptyConfirmTitle(): string {
|
||||
return t("contextMenu.confirmBatchEmptyTitle", { count: batchEmptyTargets.value.length });
|
||||
}
|
||||
|
||||
function batchEmptyConfirmMessage(): string {
|
||||
return t("contextMenu.confirmBatchEmptyMessage", { count: batchEmptyTargets.value.length });
|
||||
}
|
||||
|
||||
function batchEmptyConfirmLabel(): string {
|
||||
return t("contextMenu.batchEmpty", { count: batchEmptyTargets.value.length });
|
||||
}
|
||||
|
||||
function batchTruncateConfirmTitle(): string {
|
||||
return t("contextMenu.confirmBatchTruncateTitle", { count: selectedBatchTruncateTargets().length });
|
||||
}
|
||||
@@ -2155,6 +2179,15 @@ async function truncateSqlForTreeNode(node: TreeNode, options?: { cascade?: bool
|
||||
});
|
||||
}
|
||||
|
||||
async function emptySqlForTreeNode(node: TreeNode): Promise<string | null> {
|
||||
if (node.type !== "table" || !node.connectionId || !node.database) return null;
|
||||
return buildEmptyTableSql({
|
||||
databaseType: databaseTypeForNode(node),
|
||||
schema: node.schema,
|
||||
tableName: node.label,
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshBatchDropPreviewSql() {
|
||||
const targets = selectedBatchDropTargets();
|
||||
const mongoIndexTargets = selectedBatchMongoIndexTargets();
|
||||
@@ -2182,6 +2215,15 @@ async function refreshBatchTruncatePreviewSql() {
|
||||
batchTruncatePreviewSql.value = statements.join("\n");
|
||||
}
|
||||
|
||||
async function refreshBatchEmptyPreviewSql(targets: TreeNode[]) {
|
||||
const statements: string[] = [];
|
||||
for (const target of targets) {
|
||||
const sql = await emptySqlForTreeNode(target);
|
||||
if (sql) statements.push(sql);
|
||||
}
|
||||
batchEmptyPreviewSql.value = statements.join("\n");
|
||||
}
|
||||
|
||||
function requestBatchDrop() {
|
||||
if (!selectedBatchDropTargets().length) return;
|
||||
batchDropCascade.value = false;
|
||||
@@ -2196,6 +2238,22 @@ function requestBatchTruncate() {
|
||||
showBatchTruncateConfirm.value = true;
|
||||
}
|
||||
|
||||
function requestBatchEmpty() {
|
||||
const targets = selectedBatchEmptyTargets();
|
||||
if (!targets.length) return;
|
||||
batchEmptyTargets.value = targets.slice();
|
||||
batchEmptyPreviewSql.value = "";
|
||||
void refreshBatchEmptyPreviewSql(batchEmptyTargets.value)
|
||||
.then(() => {
|
||||
if (!batchEmptyPreviewSql.value.trim()) throw new Error("Empty table SQL preview is unavailable");
|
||||
showBatchEmptyConfirm.value = true;
|
||||
})
|
||||
.catch((e: any) => {
|
||||
batchEmptyTargets.value = [];
|
||||
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
||||
});
|
||||
}
|
||||
|
||||
function requestDropSelectedNodes(): boolean {
|
||||
const selected = selectedTreeNodesInVisibleOrder();
|
||||
if (selected.length > 1 && selected.some((node) => node.id === props.node.id)) {
|
||||
@@ -2424,6 +2482,34 @@ async function confirmBatchTruncate() {
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmBatchEmpty() {
|
||||
const targets = batchEmptyTargets.value.slice();
|
||||
if (!targets.length) return;
|
||||
const asynchronousMutation = targets.every((target) => databaseTypeForNode(target) === "clickhouse");
|
||||
const result = await runBatchTableEmpty(targets, async (target) => {
|
||||
if (!target.connectionId || !target.database) throw new Error("Missing table connection context");
|
||||
await connectionStore.ensureConnected(target.connectionId);
|
||||
const sql = await emptySqlForTreeNode(target);
|
||||
if (!sql) throw new Error("Empty table SQL is unavailable");
|
||||
await api.executeQuery(target.connectionId, target.database, sql, target.schema);
|
||||
});
|
||||
for (const failure of result.failed) {
|
||||
console.error(`Failed to empty table "${failure.target.label}":`, failure.error);
|
||||
}
|
||||
const feedback = batchTableEmptyFeedback(result, asynchronousMutation);
|
||||
if (feedback === "success") {
|
||||
toast(t("contextMenu.batchEmptySuccess", { count: result.succeeded.length }), 3000);
|
||||
} else if (feedback === "submitted") {
|
||||
toast(t("contextMenu.batchEmptySubmitted", { count: result.succeeded.length }), 3000);
|
||||
} else if (feedback === "submitted-partial") {
|
||||
toast(t("contextMenu.batchEmptySubmittedPartial", { success: result.succeeded.length, failed: result.failed.length }), 5000);
|
||||
} else {
|
||||
toast(t("contextMenu.batchEmptyPartialFail", { success: result.succeeded.length, failed: result.failed.length }), 5000);
|
||||
}
|
||||
batchEmptyTargets.value = [];
|
||||
showBatchEmptyConfirm.value = false;
|
||||
}
|
||||
|
||||
const isTableNotView = computed(() => props.node.type === "table" && !isSqlServerLinkedNode(props.node));
|
||||
|
||||
const supportsTruncate = computed(() => {
|
||||
@@ -2617,7 +2703,8 @@ async function confirmEmptyTable() {
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
const sql = emptyTablePreviewSql.value || (await buildEmptyTableSql(tableAdminSqlOptions()));
|
||||
await api.executeQuery(node.connectionId, node.database, sql, node.schema);
|
||||
toast(t("contextMenu.emptyTableSuccess", { name: node.label }), 3000);
|
||||
const messageKey = currentDatabaseType() === "clickhouse" ? "contextMenu.emptyTableSubmitted" : "contextMenu.emptyTableSuccess";
|
||||
toast(t(messageKey, { name: node.label }), 3000);
|
||||
} catch (e: any) {
|
||||
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
@@ -4505,11 +4592,14 @@ function treeItemMenuItems(): ContextMenuItem[] {
|
||||
const node = props.node;
|
||||
const items: ContextMenuItem[] = [];
|
||||
const batchDropCount = selectedBatchDropTargets().length;
|
||||
const batchEmptyCount = selectedBatchEmptyTargets().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);
|
||||
const emptyMenuLabel = (singleLabel: string) => (batchEmptyCount > 1 ? batchEmptyMenuLabel() : singleLabel);
|
||||
const emptyMenuAction = (singleAction: () => void) => (batchEmptyCount > 1 ? requestBatchEmpty : singleAction);
|
||||
|
||||
// 1. Pin toggle
|
||||
if (canPin.value) {
|
||||
@@ -4934,8 +5024,8 @@ function treeItemMenuItems(): ContextMenuItem[] {
|
||||
});
|
||||
}
|
||||
destructiveActions.push({
|
||||
label: t("contextMenu.emptyTable"),
|
||||
action: emptyTable,
|
||||
label: emptyMenuLabel(t("contextMenu.emptyTable")),
|
||||
action: emptyMenuAction(emptyTable),
|
||||
icon: Eraser,
|
||||
variant: "destructive" as const,
|
||||
});
|
||||
@@ -5388,6 +5478,8 @@ function treeItemMenuItems(): ContextMenuItem[] {
|
||||
|
||||
<DangerConfirmDialog v-model:open="showEmptyTableConfirm" :title="t('contextMenu.confirmEmptyTableTitle')" :message="t('contextMenu.confirmEmptyTableMessage', { name: node.label })" :sql="emptyTablePreviewSql" :confirm-label="t('contextMenu.emptyTable')" @confirm="confirmEmptyTable" />
|
||||
|
||||
<DangerConfirmDialog v-model:open="showBatchEmptyConfirm" :title="batchEmptyConfirmTitle()" :message="batchEmptyConfirmMessage()" :sql="batchEmptyPreviewSql" :confirm-label="batchEmptyConfirmLabel()" @confirm="confirmBatchEmpty" />
|
||||
|
||||
<DangerConfirmDialog
|
||||
v-model:open="showTruncateTableConfirm"
|
||||
:title="t('contextMenu.confirmTruncateTableTitle')"
|
||||
|
||||
Reference in new issue
Block a user