mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
@@ -87,7 +87,18 @@ import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { generateDatabaseExportId } from "@/lib/export/databaseExport";
|
||||
import { buildXlsxHeaderOverrides, hasXlsxHeaderComments, type XlsxHeaderMode } from "@/lib/export/xlsxHeader";
|
||||
import { copyToClipboard, eventTargetAllowsAppClipboardShortcut } from "@/lib/common/clipboard";
|
||||
import { defaultPasteTableMode, pasteTableModeCopiesData, supportsWholeRowTableDataCopy, tableClipboardMatchesTarget, tableClipboardMenuState, tableClipboardSourceContext, tableDataCopyColumnOptions, type PasteTableMode, type TableClipboardContext } from "@/lib/table/tableClipboard";
|
||||
import {
|
||||
defaultPasteTableMode,
|
||||
pasteTableModeCopiesData,
|
||||
supportsWholeRowTableDataCopy,
|
||||
tableClipboardMatchesTarget,
|
||||
tableClipboardMenuState,
|
||||
tableClipboardSourceContext,
|
||||
tableDataCopyColumnOptions,
|
||||
tablePasteFeedback,
|
||||
type PasteTableMode,
|
||||
type TableClipboardContext,
|
||||
} from "@/lib/table/tableClipboard";
|
||||
import { buildSingleDdlExportFileContent } from "@/lib/export/ddlExport";
|
||||
import { fetchTableDataForExport } from "@/lib/table/tableDataExport";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
@@ -2285,7 +2296,8 @@ async function confirmPasteTable() {
|
||||
const copyData = pasteTableModeCopiesData(mode) && pasteTableDataCopySupported.value;
|
||||
showPasteDialog.value = false;
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
let pasteFailCount = 0;
|
||||
let firstPasteError: unknown;
|
||||
let pasteCancelled = false;
|
||||
let hasMutatedTable = false;
|
||||
for (const entry of entries) {
|
||||
@@ -2327,7 +2339,8 @@ async function confirmPasteTable() {
|
||||
}
|
||||
successCount++;
|
||||
} catch (e: any) {
|
||||
failCount++;
|
||||
pasteFailCount++;
|
||||
firstPasteError ??= e;
|
||||
console.error(`Failed to paste table "${entry.sourceName}" -> "${targetName}":`, e);
|
||||
}
|
||||
}
|
||||
@@ -2343,13 +2356,14 @@ async function confirmPasteTable() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (failCount === 0) {
|
||||
const pasteFeedback = tablePasteFeedback(successCount, pasteFailCount, firstPasteError);
|
||||
if (pasteFeedback.failedCount === 0) {
|
||||
if (connectionStore.treeClipboard === clipboardAtPasteStart) {
|
||||
connectionStore.treeClipboard = null;
|
||||
}
|
||||
toast(t("contextMenu.batchPasteSuccess", { count: successCount }), 3000);
|
||||
} else {
|
||||
toast(t("contextMenu.batchPastePartialFail", { success: successCount, failed: failCount }), 5000);
|
||||
toast(`${t("contextMenu.batchPastePartialFail", { success: pasteFeedback.successCount, failed: pasteFeedback.failedCount })}\n${t("contextMenu.tableOperationFailed", { message: translateBackendError(t, pasteFeedback.firstError) })}`, 5000);
|
||||
}
|
||||
await reload();
|
||||
await connectionStore.refreshObjectListTreeNode(props.connection.id, props.database, selectedSchema.value);
|
||||
|
||||
@@ -22,7 +22,15 @@ describe("ObjectBrowser table clipboard context menu", () => {
|
||||
});
|
||||
|
||||
it("consumes only the clipboard used by a fully successful paste", () => {
|
||||
expect(objectBrowserSource).toMatch(/async function confirmPasteTable\(\)[\s\S]*?const clipboardAtPasteStart = connectionStore\.treeClipboard[\s\S]*?if \(failCount === 0\)[\s\S]*?connectionStore\.treeClipboard === clipboardAtPasteStart[\s\S]*?connectionStore\.treeClipboard = null/);
|
||||
expect(objectBrowserSource).toMatch(
|
||||
/async function confirmPasteTable\(\)[\s\S]*?const clipboardAtPasteStart = connectionStore\.treeClipboard[\s\S]*?const pasteFeedback = tablePasteFeedback\(successCount, pasteFailCount, firstPasteError\)[\s\S]*?if \(pasteFeedback\.failedCount === 0\)[\s\S]*?connectionStore\.treeClipboard === clipboardAtPasteStart[\s\S]*?connectionStore\.treeClipboard = null/,
|
||||
);
|
||||
});
|
||||
|
||||
it("shows the first paste failure reason without concatenating every error", () => {
|
||||
expect(objectBrowserSource).toMatch(/pasteFailCount\+\+/);
|
||||
expect(objectBrowserSource).toMatch(/firstPasteError \?\?= e/);
|
||||
expect(objectBrowserSource).toMatch(/batchPastePartialFail[\s\S]*?tableOperationFailed[\s\S]*?translateBackendError\(t, pasteFeedback\.firstError\)/);
|
||||
});
|
||||
|
||||
it("refreshes created tables and retains the clipboard when a later paste step is cancelled", () => {
|
||||
|
||||
@@ -149,7 +149,18 @@ import { formatSqlForDisplay, sqlFormatDialectForDbType } from "@/lib/sql/sqlFor
|
||||
import { getTableStructureCapabilities } from "@/lib/table/tableStructureCapabilities";
|
||||
import { connectionObjectTreeNodeSchema, connectionObjectTreeQuerySchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { hasTreeNodeDatabaseContext } from "@/lib/sidebar/treeNodeContext";
|
||||
import { defaultPasteTableMode, pasteTableModeCopiesData, supportsWholeRowTableDataCopy, tableClipboardMatchesTarget, tableClipboardMenuState, tableClipboardSourceContext, tableDataCopyColumnOptions, type TableClipboardContext, type TableClipboardTableContext } from "@/lib/table/tableClipboard";
|
||||
import {
|
||||
defaultPasteTableMode,
|
||||
pasteTableModeCopiesData,
|
||||
supportsWholeRowTableDataCopy,
|
||||
tableClipboardMatchesTarget,
|
||||
tableClipboardMenuState,
|
||||
tableClipboardSourceContext,
|
||||
tableDataCopyColumnOptions,
|
||||
tablePasteFeedback,
|
||||
type TableClipboardContext,
|
||||
type TableClipboardTableContext,
|
||||
} from "@/lib/table/tableClipboard";
|
||||
import { selectedSidebarBatchTargets, selectedTreeNodesInVisibleOrder as orderSelectedTreeNodes } from "@/lib/sidebar/sidebarTreeSelection";
|
||||
import { connectionPasteTargetGroupId, selectedConnectionClipboardTargets, selectedConnectionEditTarget, selectedConnectionMoveTargets } from "@/lib/sidebar/sidebarConnectionSelection";
|
||||
import { connectionSupportsDatabaseUserAdmin, resolveDatabaseUserAdminProviderForConnection, type DatabaseUserIdentity } from "@/lib/database/databaseUserAdmin";
|
||||
@@ -3831,6 +3842,7 @@ async function confirmPasteTable() {
|
||||
showPasteDialog.value = false;
|
||||
let successCount = 0;
|
||||
let pasteFailCount = 0;
|
||||
let firstPasteError: unknown;
|
||||
let refreshFailCount = 0;
|
||||
let refreshError: unknown;
|
||||
let pasteCancelled = false;
|
||||
@@ -3902,6 +3914,7 @@ async function confirmPasteTable() {
|
||||
successCount++;
|
||||
} catch (e: any) {
|
||||
pasteFailCount++;
|
||||
firstPasteError ??= e;
|
||||
console.error(`Failed to paste table "${entry.sourceName}" -> "${targetName}":`, e);
|
||||
}
|
||||
}
|
||||
@@ -3924,13 +3937,14 @@ async function confirmPasteTable() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (pasteFailCount === 0) {
|
||||
const pasteFeedback = tablePasteFeedback(successCount, pasteFailCount, firstPasteError);
|
||||
if (pasteFeedback.failedCount === 0) {
|
||||
if (connectionStore.treeClipboard === clipboardAtPasteStart) {
|
||||
connectionStore.treeClipboard = null;
|
||||
}
|
||||
toast(t("contextMenu.batchPasteSuccess", { count: successCount }), 3000);
|
||||
} else {
|
||||
toast(t("contextMenu.batchPastePartialFail", { success: successCount, failed: pasteFailCount }), 5000);
|
||||
toast(`${t("contextMenu.batchPastePartialFail", { success: pasteFeedback.successCount, failed: pasteFeedback.failedCount })}\n${t("contextMenu.tableOperationFailed", { message: translateBackendError(t, pasteFeedback.firstError) })}`, 5000);
|
||||
}
|
||||
if (refreshFailCount > 0) {
|
||||
const refreshMessage = refreshError instanceof Error ? refreshError.message : String(refreshError);
|
||||
|
||||
@@ -41,4 +41,10 @@ describe("cross-database table paste", () => {
|
||||
expect(runtimeSource).toMatch(/buildDuplicateTableStructurePlan\(\{[\s\S]*?connectionId: entry\.connectionId[\s\S]*?sourceName: entry\.sourceName[\s\S]*?tableComment: entry\.tableComment/);
|
||||
expect(runtimeSource).toContain("sourceColumns = plan.sourceColumns");
|
||||
});
|
||||
|
||||
it("shows the first paste failure reason without concatenating every error", () => {
|
||||
expect(runtimeSource).toMatch(/pasteFailCount\+\+/);
|
||||
expect(runtimeSource).toMatch(/firstPasteError \?\?= e/);
|
||||
expect(runtimeSource).toMatch(/batchPastePartialFail[\s\S]*?tableOperationFailed[\s\S]*?translateBackendError\(t, pasteFeedback\.firstError\)/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { tableClipboardMatchesTarget, tableClipboardMenuState, tableClipboardSourceContext } from "@/lib/table/tableClipboard";
|
||||
import { tableClipboardMatchesTarget, tableClipboardMenuState, tableClipboardSourceContext, tablePasteFeedback } from "@/lib/table/tableClipboard";
|
||||
|
||||
describe("table clipboard contexts", () => {
|
||||
const source = { connectionId: "source", database: "app", schema: "public" };
|
||||
@@ -23,4 +23,11 @@ describe("table clipboard contexts", () => {
|
||||
expect(tableClipboardMenuState([sourceTable], targetTable)).toBe("copy");
|
||||
expect(tableClipboardMenuState([sourceTable], targetTable, true)).toBe("copy-and-paste");
|
||||
});
|
||||
|
||||
it("keeps batch feedback bounded to the first paste error", () => {
|
||||
const firstError = { code: "permission-denied" };
|
||||
const feedback = tablePasteFeedback(0, 3, firstError);
|
||||
|
||||
expect(feedback).toEqual({ successCount: 0, failedCount: 3, firstError });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,21 @@ export interface TableDataCopyColumnOptions {
|
||||
sqlserverIdentityInsert: boolean;
|
||||
}
|
||||
|
||||
export interface TablePasteFeedback {
|
||||
successCount: number;
|
||||
failedCount: number;
|
||||
firstError: unknown;
|
||||
}
|
||||
|
||||
/** Keep batch paste feedback bounded while preserving the first actionable error. */
|
||||
export function tablePasteFeedback(successCount: number, failedCount: number, firstError: unknown): TablePasteFeedback {
|
||||
return {
|
||||
successCount,
|
||||
failedCount,
|
||||
firstError,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSchema(schema: string | null | undefined): string {
|
||||
return schema?.trim() ?? "";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user