diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index d77657d170..a2c5961985 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -19,6 +19,7 @@ import { applyParsedConnectionUrl, parseConnectionUrl } from "@/lib/connectionUr import type { ConnectionDeepLinkDraft } from "@/lib/connectionDeepLink"; import { connectionUrlPlaceholder as getUrlPlaceholder } from "@/lib/connectionPresentation"; import { mongodbAuthFailureHint, mongoUrlParam, setMongoUrlParam } from "@/lib/mongoConnectionOptions"; +import { copyToClipboard } from "@/lib/clipboard"; import { showAgentDriverInstallHint, type AgentDriverInstallState } from "@/lib/agentDriverInstallHint"; import { ArrowLeft, @@ -688,8 +689,12 @@ function applyConnectionUrl() { async function copyTestResult() { if (!testResultMessage.value) return; - await navigator.clipboard.writeText(testResultMessage.value); - toast(t("grid.copied")); + try { + await copyToClipboard(testResultMessage.value); + toast(t("grid.copied")); + } catch (e: any) { + toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000); + } } function resetForm() { diff --git a/apps/desktop/src/components/diff/DataCompareDialog.vue b/apps/desktop/src/components/diff/DataCompareDialog.vue index b9e021ffa3..14f36ce06d 100644 --- a/apps/desktop/src/components/diff/DataCompareDialog.vue +++ b/apps/desktop/src/components/diff/DataCompareDialog.vue @@ -8,6 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { useConnectionStore } from "@/stores/connectionStore"; import { useToast } from "@/composables/useToast"; import { isSchemaAware } from "@/lib/databaseCapabilities"; +import { copyToClipboard } from "@/lib/clipboard"; import type { DataCompareResult } from "@/lib/dataCompare"; import * as api from "@/lib/api"; import DatabaseIcon from "@/components/icons/DatabaseIcon.vue"; @@ -288,8 +289,12 @@ async function startCompare() { } async function copySql() { - await navigator.clipboard.writeText(syncSql.value); - toast(t("grid.copied")); + try { + await copyToClipboard(syncSql.value); + toast(t("grid.copied")); + } catch (e: any) { + toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000); + } } async function executeSql() { diff --git a/apps/desktop/src/components/diff/SchemaDiffDialog.vue b/apps/desktop/src/components/diff/SchemaDiffDialog.vue index 00d919358f..c527b990f4 100644 --- a/apps/desktop/src/components/diff/SchemaDiffDialog.vue +++ b/apps/desktop/src/components/diff/SchemaDiffDialog.vue @@ -10,6 +10,7 @@ import { useConnectionStore } from "@/stores/connectionStore"; import DatabaseIcon from "@/components/icons/DatabaseIcon.vue"; import * as api from "@/lib/api"; import { isSchemaAware } from "@/lib/databaseCapabilities"; +import { copyToClipboard } from "@/lib/clipboard"; import type { TableDiff, TableSchemaDetail } from "@/lib/schemaDiff"; import type { TableInfo } from "@/types/database"; import { sqlMetadataRefreshTarget } from "@/lib/sqlMetadataRefresh"; @@ -286,9 +287,13 @@ async function executeSql() { } } -function copySql() { - navigator.clipboard.writeText(syncSql.value); - toast(t("grid.copied")); +async function copySql() { + try { + await copyToClipboard(syncSql.value); + toast(t("grid.copied")); + } catch (e: any) { + toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000); + } } function diffBadgeVariant(type: string) { diff --git a/apps/desktop/src/components/editor/AiAssistant.vue b/apps/desktop/src/components/editor/AiAssistant.vue index 3c73eb8e8f..f89c4ab1e5 100644 --- a/apps/desktop/src/components/editor/AiAssistant.vue +++ b/apps/desktop/src/components/editor/AiAssistant.vue @@ -68,6 +68,7 @@ import type { ConnectionConfig, QueryTab, TableInfo } from "@/types/database"; import { useDatabaseOptions } from "@/composables/useDatabaseOptions"; import { resolveDefaultDatabase } from "@/lib/defaultDatabase"; import { isSchemaAware } from "@/lib/databaseCapabilities"; +import { copyToClipboard } from "@/lib/clipboard"; import { formatAiTableMention, parseAiTableMentions, type AiTableMention } from "@/lib/aiTableMentions"; import { isAiPromptImeCompositionEvent, shouldSubmitAiPromptOnKeydown } from "@/lib/aiPromptKeyboard"; @@ -561,11 +562,15 @@ function executeSql(code: string) { const copiedIndex = ref(""); async function copyCode(code: string, key: string) { - await navigator.clipboard.writeText(code); - copiedIndex.value = key; - setTimeout(() => { - if (copiedIndex.value === key) copiedIndex.value = ""; - }, 2000); + try { + await copyToClipboard(code); + copiedIndex.value = key; + setTimeout(() => { + if (copiedIndex.value === key) copiedIndex.value = ""; + }, 2000); + } catch (e: any) { + toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000); + } } function clearMessages() { diff --git a/apps/desktop/src/components/editor/ColumnInfoPanel.vue b/apps/desktop/src/components/editor/ColumnInfoPanel.vue index 12cf4975f5..248f396cea 100644 --- a/apps/desktop/src/components/editor/ColumnInfoPanel.vue +++ b/apps/desktop/src/components/editor/ColumnInfoPanel.vue @@ -3,6 +3,8 @@ import { X, Copy } from "lucide-vue-next"; import { useI18n } from "vue-i18n"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; +import { useToast } from "@/composables/useToast"; +import { copyToClipboard } from "@/lib/clipboard"; export interface ColumnInfo { name: string; @@ -26,9 +28,15 @@ const emit = defineEmits<{ }>(); const { t } = useI18n(); +const { toast } = useToast(); -function copyText(text: string) { - navigator.clipboard.writeText(text); +async function copyText(text: string) { + try { + await copyToClipboard(text); + toast(t("grid.copied")); + } catch (e: any) { + toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000); + } } diff --git a/apps/desktop/src/components/editor/QueryHistory.vue b/apps/desktop/src/components/editor/QueryHistory.vue index d2273040b0..ff042d2b52 100644 --- a/apps/desktop/src/components/editor/QueryHistory.vue +++ b/apps/desktop/src/components/editor/QueryHistory.vue @@ -13,6 +13,7 @@ import { resolveHistoryActivityKind } from "@/lib/historyActivityKind"; import { canRollbackHistoryEntry } from "@/lib/historyAiAnalysis"; import { HISTORY_ROW_HEIGHT, HISTORY_SCROLL_BUFFER, shouldVirtualizeHistory } from "@/lib/historyVirtualList"; import type { HistoryEntry } from "@/lib/api"; +import { copyToClipboard } from "@/lib/clipboard"; import * as api from "@/lib/api"; const { t } = useI18n(); @@ -58,8 +59,12 @@ function restore(entry: HistoryEntry) { } async function copyText(text: string) { - await navigator.clipboard.writeText(text); - toast(t("grid.copied")); + try { + await copyToClipboard(text); + toast(t("grid.copied")); + } catch (e: any) { + toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000); + } } function confirmDeleteEntry(id: string) { diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index f8a5aee8ec..fd8e86a637 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -150,6 +150,13 @@ const { t } = useI18n(); const settingsStore = useSettingsStore(); const { toast } = useToast(); +interface PreparedCopyValue { + key: string; + text: string; + loading: boolean; + ready: boolean; +} + const props = defineProps<{ result: QueryResult; sql?: string; @@ -1892,6 +1899,82 @@ const activeValueEditorActions = computed(() => { }); }); +const detailSqlConditionCopy = ref({ + key: "", + text: "", + loading: false, + ready: false, +}); + +const detailSqlConditionKey = computed(() => { + const detail = activeCellDetail.value; + if (!detail) return ""; + return JSON.stringify({ + databaseType: props.databaseType ?? null, + column: detail.column, + value: detail.value, + type: detail.type, + schema: props.tableMeta?.schema ?? null, + tableName: props.tableMeta?.tableName ?? null, + }); +}); + +function canCopyPreparedDetailSqlCondition(): boolean { + return detailSqlConditionCopy.value.ready && detailSqlConditionCopy.value.key === detailSqlConditionKey.value; +} + +async function prefetchDetailSqlCondition() { + const detail = activeCellDetail.value; + const key = detailSqlConditionKey.value; + if (!detail || !key) { + detailSqlConditionCopy.value = { + key: "", + text: "", + loading: false, + ready: false, + }; + return; + } + const current = detailSqlConditionCopy.value; + if ((current.loading || current.ready) && current.key === key) return; + + detailSqlConditionCopy.value = { + key, + text: "", + loading: true, + ready: false, + }; + + try { + const condition = await buildDataGridContextFilterCondition({ + databaseType: props.databaseType, + columnName: detail.column, + columnInfo: props.tableMeta?.columns.find((column) => column.name === detail.column), + mode: "equals", + value: detail.value, + }); + if (detailSqlConditionCopy.value.key !== key) return; + detailSqlConditionCopy.value = { + key, + text: condition ?? "", + loading: false, + ready: !!condition, + }; + } catch { + if (detailSqlConditionCopy.value.key !== key) return; + detailSqlConditionCopy.value = { + key, + text: "", + loading: false, + ready: false, + }; + } +} + +watch(activeCellDetail, () => { + void prefetchDetailSqlCondition(); +}); + const detailEditValue = ref(""); const isEditingDetail = ref(false); const detailTemporalEditorKind = computed(() => { @@ -2204,6 +2287,10 @@ const { copyRow, copyRowAsInsert, copyRowAsInsertWithoutPrimaryKeys, + prefetchRowAsInsertStatement, + canCopyPreparedInsert, + prefetchRowAsUpdateStatement, + canCopyPreparedUpdate, copyRowAsUpdate, canCopyRowAsInsertWithoutPrimaryKeys, canCopyRowAsUpdate, @@ -2458,16 +2545,8 @@ function copyDetailColumnName() { } async function copyDetailSqlCondition() { - const detail = activeCellDetail.value; - if (!detail) return; - const condition = await buildDataGridContextFilterCondition({ - databaseType: props.databaseType, - columnName: detail.column, - columnInfo: props.tableMeta?.columns.find((column) => column.name === detail.column), - mode: "equals", - value: detail.value, - }); - if (condition) copyText(condition); + if (!canCopyPreparedDetailSqlCondition()) return; + copyText(detailSqlConditionCopy.value.text); } const TRANSPOSE_RECORD_DEFAULT_WIDTH = 168; @@ -2677,12 +2756,14 @@ watch( function onCellContext(rowId: number, rowIndex: number, colIdx: number, visibleColIdx: number) { contextCell.value = { rowId, rowIndex, col: colIdx }; if (hasRowSelection.value && isRowSelected(rowId)) { + void prefetchCopyStatements(); return; } clearRowSelection(); if (!cellIsSelected(rowIndex, visibleColIdx)) { selectSingleCell(rowIndex, visibleColIdx); } + void prefetchCopyStatements(); } function onRowContext(rowId: number, rowIndex: number) { @@ -2692,6 +2773,17 @@ function onRowContext(rowId: number, rowIndex: number) { selectedRowIds.value = new Set([rowId]); selection.lastClickedRowIndex.value = rowIndex; } + void prefetchCopyStatements(); +} + +async function prefetchCopyStatements() { + await prefetchRowAsInsertStatement(false); + if (canCopyRowAsInsertWithoutPrimaryKeys.value) { + await prefetchRowAsInsertStatement(true); + } + if (canCopyRowAsUpdate.value) { + await prefetchRowAsUpdateStatement(); + } } const sqlOneLiner = computed(() => props.sql?.replace(/\s+/g, " ").trim() || ""); @@ -2865,8 +2957,7 @@ if (showTableInfo.value && props.tableMeta && props.connectionId) { } function copyDdl() { - navigator.clipboard.writeText(ddlContent.value); - toast(t("grid.copied")); + copyText(ddlContent.value); } function toggleDdlWrap() { @@ -4487,7 +4578,13 @@ defineExpose({ - @@ -4598,17 +4695,21 @@ defineExpose({ {{ isMultiRow ? t("grid.copyRows", { count: multiRowCount }) : t("grid.copyRow") }} - + {{ isMultiRow ? t("grid.copyRowsInsert", { count: multiRowCount }) : t("grid.copyRowInsert") }} - + {{ isMultiRow ? t("grid.copyRowsInsertWithoutPrimaryKeys", { count: multiRowCount }) : t("grid.copyRowInsertWithoutPrimaryKeys") }} - + {{ isMultiRow ? t("grid.copyRowsUpdate", { count: multiRowCount }) : t("grid.copyRowUpdate") }} {{ t("grid.copyAll") }} diff --git a/apps/desktop/src/components/lineage/FieldLineageDialog.vue b/apps/desktop/src/components/lineage/FieldLineageDialog.vue index 6cdee68340..87bd4b0d7e 100644 --- a/apps/desktop/src/components/lineage/FieldLineageDialog.vue +++ b/apps/desktop/src/components/lineage/FieldLineageDialog.vue @@ -32,6 +32,7 @@ import { type FieldLineageTable, type FieldLineageView, } from "@/lib/fieldLineage"; +import { copyToClipboard } from "@/lib/clipboard"; const props = defineProps<{ open: boolean; @@ -291,14 +292,18 @@ function itemDescription(item: FieldLineageItem) { return t("lineage.description.sameName"); } -function copyItem(item: FieldLineageItem) { +async function copyItem(item: FieldLineageItem) { const text = itemPrimaryLabel(item); - navigator.clipboard.writeText(text); - copiedId.value = item.id; - toast(t("lineage.copied")); - setTimeout(() => { - if (copiedId.value === item.id) copiedId.value = ""; - }, 1400); + try { + await copyToClipboard(text); + copiedId.value = item.id; + toast(t("lineage.copied")); + setTimeout(() => { + if (copiedId.value === item.id) copiedId.value = ""; + }, 1400); + } catch (e: any) { + toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000); + } } function openItemTarget(item: FieldLineageItem) { diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue index 94e99cbe3f..fd2cd51309 100644 --- a/apps/desktop/src/components/objects/ObjectBrowser.vue +++ b/apps/desktop/src/components/objects/ObjectBrowser.vue @@ -70,6 +70,7 @@ import { import { buildRenameObjectSql, supportsObjectRename } from "@/lib/objectRenameSql"; import { buildViewDdl } from "@/lib/viewDdl"; import { isTauriRuntime } from "@/lib/tauriRuntime"; +import { copyToClipboard } from "@/lib/clipboard"; import { formatSqlInsert } from "@/lib/exportFormats"; import { fetchTableDataForExport } from "@/lib/tableDataExport"; import { useConnectionStore } from "@/stores/connectionStore"; @@ -746,15 +747,23 @@ async function confirmEmptyTable() { emptyTarget.value = null; } -function copyName(row: ObjectBrowserRow) { - navigator.clipboard.writeText(row.name); - toast(t("connection.copied"), 2000); +async function copyName(row: ObjectBrowserRow) { + try { + await copyToClipboard(row.name); + toast(t("connection.copied"), 2000); + } catch (e: any) { + toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000); + } } -function copySource() { +async function copySource() { if (!sourceContent.value) return; - navigator.clipboard.writeText(sourceContent.value); - toast(t("grid.copied")); + try { + await copyToClipboard(sourceContent.value); + toast(t("grid.copied")); + } catch (e: any) { + toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000); + } } function editSource() { diff --git a/apps/desktop/src/components/redis/RedisValueViewer.vue b/apps/desktop/src/components/redis/RedisValueViewer.vue index ddd0844f9e..7f7058abe1 100644 --- a/apps/desktop/src/components/redis/RedisValueViewer.vue +++ b/apps/desktop/src/components/redis/RedisValueViewer.vue @@ -15,6 +15,7 @@ import type { RedisKeyInfo, RedisValue } from "@/lib/api"; import { useToast } from "@/composables/useToast"; import { useTheme } from "@/composables/useTheme"; import { createRedisShikiJsonHighlighter, type RedisJsonHighlighter } from "@/lib/redisJsonHighlighter"; +import { copyToClipboard } from "@/lib/clipboard"; import { canEditRedisMemberDetail, clampRedisMemberDetailSheetWidth, @@ -275,20 +276,28 @@ function requestDeleteKey() { showDeleteConfirm.value = true; } -function copyValue() { +async function copyValue() { if (!data.value) return; const text = typeof data.value.value === "string" ? data.value.value : JSON.stringify(data.value.value, null, 2); - navigator.clipboard.writeText(text); - toast(t("redis.copied"), 2000); + try { + await copyToClipboard(text); + toast(t("redis.copied"), 2000); + } catch (e: any) { + toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000); + } } -function copyText(text: string) { - navigator.clipboard.writeText(text); - toast(t("redis.copied"), 2000); +async function copyText(text: string) { + try { + await copyToClipboard(text); + toast(t("redis.copied"), 2000); + } catch (e: any) { + toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000); + } } function copyMember(value: unknown) { - copyText(formatRedisMemberDetail(value).text); + void copyText(formatRedisMemberDetail(value).text); } function selectMember(title: string, value: unknown, context: RedisMemberContext) { diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index aacf99cecd..2ec47ad127 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -115,6 +115,7 @@ import { hasTreeNodeDatabaseContext } from "@/lib/treeNodeContext"; import { sidebarDisplayTableName } from "@/lib/sidebarTableNameDisplay"; import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue"; import { isTauriRuntime } from "@/lib/tauriRuntime"; +import { copyToClipboard } from "@/lib/clipboard"; import DatabaseIcon from "@/components/icons/DatabaseIcon.vue"; import ConnectionErrorIndicator from "@/components/connection/ConnectionErrorIndicator.vue"; import VisibleDatabasesDialog from "@/components/sidebar/VisibleDatabasesDialog.vue"; @@ -631,9 +632,13 @@ async function confirmDelete() { } } -function copyName() { - navigator.clipboard.writeText(props.node.label); - toast(t("connection.copied"), 2000); +async function copyName() { + try { + await copyToClipboard(props.node.label); + toast(t("connection.copied"), 2000); + } catch (e: any) { + toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000); + } } async function duplicateConnection() { diff --git a/apps/desktop/src/composables/useDataGridExport.ts b/apps/desktop/src/composables/useDataGridExport.ts index f7410cf592..cfd683a1ac 100644 --- a/apps/desktop/src/composables/useDataGridExport.ts +++ b/apps/desktop/src/composables/useDataGridExport.ts @@ -1,4 +1,4 @@ -import { computed, type ComputedRef, type Ref } from "vue"; +import { computed, ref, type ComputedRef, type Ref } from "vue"; import { useI18n } from "vue-i18n"; import { isTauriRuntime } from "@/lib/tauriRuntime"; import * as api from "@/lib/api"; @@ -46,10 +46,35 @@ export interface UseDataGridExportOptions { hasRowSelection: ComputedRef; } +interface CopyStatementCache { + key: string; + text: string; + loading: boolean; + ready: boolean; +} + export function useDataGridExport(options: UseDataGridExportOptions) { const { t } = useI18n(); const { toast } = useToast(); const exportGuard: ActionActivationGuard = {}; + const copyRowInsertCache = ref({ + key: "", + text: "", + loading: false, + ready: false, + }); + const copyRowInsertWithoutPrimaryKeysCache = ref({ + key: "", + text: "", + loading: false, + ready: false, + }); + const copyRowUpdateCache = ref({ + key: "", + text: "", + loading: false, + ready: false, + }); const { columns, @@ -99,6 +124,181 @@ export function useDataGridExport(options: UseDataGridExportOptions) { return targetedRows().filter((item) => !item.isNew && !item.isDeleted); } + function updateCopyKey(): string { + const rows = updateEligibleRows().map((item) => item.id); + return JSON.stringify({ + databaseType: databaseType.value ?? null, + schema: tableMeta.value?.schema ?? null, + tableName: tableMeta.value?.tableName ?? null, + primaryKeys: tableMeta.value?.primaryKeys ?? [], + columns: columns.value, + sourceColumns: sourceColumns.value ?? null, + rows, + }); + } + + function insertCopyKey(excludePrimaryKeys: boolean): string { + const rows = insertEligibleRows().map((item) => item.id); + return JSON.stringify({ + databaseType: databaseType.value ?? null, + schema: tableMeta.value?.schema ?? null, + tableName: tableMeta.value?.tableName ?? null, + columns: columns.value, + sourceColumns: sourceColumns.value ?? null, + excludePrimaryKeys, + rows, + }); + } + + function insertCopyCache(excludePrimaryKeys: boolean): CopyStatementCache { + return excludePrimaryKeys ? copyRowInsertWithoutPrimaryKeysCache.value : copyRowInsertCache.value; + } + + function setInsertCopyCache(excludePrimaryKeys: boolean, cache: CopyStatementCache) { + if (excludePrimaryKeys) { + copyRowInsertWithoutPrimaryKeysCache.value = cache; + } else { + copyRowInsertCache.value = cache; + } + } + + function setUpdateCopyCache(cache: CopyStatementCache) { + copyRowUpdateCache.value = cache; + } + + async function prefetchRowAsInsertStatement(excludePrimaryKeys: boolean) { + const rows = insertEligibleRows(); + if (!rows.length) { + setInsertCopyCache(excludePrimaryKeys, { + key: "", + text: "", + loading: false, + ready: false, + }); + return; + } + const key = insertCopyKey(excludePrimaryKeys); + const current = insertCopyCache(excludePrimaryKeys); + if ((current.loading || current.ready) && current.key === key) return; + + setInsertCopyCache(excludePrimaryKeys, { + key, + text: "", + loading: true, + ready: false, + }); + + try { + const statement = await buildDataGridCopyInsertStatement({ + databaseType: databaseType.value, + tableMeta: tableMeta.value, + columns: columns.value, + sourceColumns: sourceColumns.value, + rows: rows.map((item) => item.data), + excludePrimaryKeys, + }); + const latest = insertCopyCache(excludePrimaryKeys); + if (latest.key !== key) return; + setInsertCopyCache(excludePrimaryKeys, { + key, + text: statement ?? "", + loading: false, + ready: !!statement, + }); + } catch { + const latest = insertCopyCache(excludePrimaryKeys); + if (latest.key !== key) return; + setInsertCopyCache(excludePrimaryKeys, { + key, + text: "", + loading: false, + ready: false, + }); + } + } + + function canCopyPreparedInsert(excludePrimaryKeys: boolean): boolean { + const cache = insertCopyCache(excludePrimaryKeys); + return cache.ready && cache.key === insertCopyKey(excludePrimaryKeys); + } + + function copyPreparedRowAsInsert(excludePrimaryKeys: boolean): boolean { + if (!canCopyPreparedInsert(excludePrimaryKeys)) return false; + void copyText(insertCopyCache(excludePrimaryKeys).text); + return true; + } + + async function prefetchRowAsUpdateStatement() { + if (!tableMeta.value?.primaryKeys.length) { + setUpdateCopyCache({ + key: "", + text: "", + loading: false, + ready: false, + }); + return; + } + const rows = updateEligibleRows(); + if (!rows.length) { + setUpdateCopyCache({ + key: "", + text: "", + loading: false, + ready: false, + }); + return; + } + const key = updateCopyKey(); + const current = copyRowUpdateCache.value; + if ((current.loading || current.ready) && current.key === key) return; + + setUpdateCopyCache({ + key, + text: "", + loading: true, + ready: false, + }); + + try { + const statements = await buildDataGridCopyUpdateStatements({ + databaseType: databaseType.value, + tableMeta: tableMeta.value, + columns: columns.value, + sourceColumns: sourceColumns.value, + rows: rows.map((item) => item.data), + }); + const latest = copyRowUpdateCache.value; + if (latest.key !== key) return; + const text = statements.join("\n"); + setUpdateCopyCache({ + key, + text, + loading: false, + ready: statements.length > 0, + }); + } catch { + const latest = copyRowUpdateCache.value; + if (latest.key !== key) return; + setUpdateCopyCache({ + key, + text: "", + loading: false, + ready: false, + }); + } + } + + function canCopyPreparedUpdate(): boolean { + const cache = copyRowUpdateCache.value; + return cache.ready && cache.key === updateCopyKey(); + } + + function copyPreparedRowAsUpdate(): boolean { + if (!canCopyPreparedUpdate()) return false; + void copyText(copyRowUpdateCache.value.text); + return true; + } + // --- Selection copy functions --- async function copySelectionTsv() { if (!hasCellSelection.value) return; @@ -168,38 +368,16 @@ export function useDataGridExport(options: UseDataGridExportOptions) { return targetedRows(); } - async function copyRowAsInsertStatement(excludePrimaryKeys: boolean) { - const statement = await buildDataGridCopyInsertStatement({ - databaseType: databaseType.value, - tableMeta: tableMeta.value, - columns: columns.value, - sourceColumns: sourceColumns.value, - rows: insertEligibleRows().map((item) => item.data), - excludePrimaryKeys, - }); - if (!statement) return; - await copyText(statement); - } - async function copyRowAsInsert() { - await copyRowAsInsertStatement(false); + copyPreparedRowAsInsert(false); } async function copyRowAsInsertWithoutPrimaryKeys() { - await copyRowAsInsertStatement(true); + copyPreparedRowAsInsert(true); } async function copyRowAsUpdate() { - if (!tableMeta.value?.primaryKeys.length) return; - const statements = await buildDataGridCopyUpdateStatements({ - databaseType: databaseType.value, - tableMeta: tableMeta.value, - columns: columns.value, - sourceColumns: sourceColumns.value, - rows: updateEligibleRows().map((item) => item.data), - }); - if (!statements.length) return; - await copyText(statements.join("\n")); + copyPreparedRowAsUpdate(); } const canCopyRowAsUpdate = computed(() => { @@ -353,6 +531,12 @@ export function useDataGridExport(options: UseDataGridExportOptions) { copyRow, copyRowAsInsert, copyRowAsInsertWithoutPrimaryKeys, + prefetchRowAsInsertStatement, + canCopyPreparedInsert, + copyPreparedRowAsInsert, + prefetchRowAsUpdateStatement, + canCopyPreparedUpdate, + copyPreparedRowAsUpdate, copyRowAsUpdate, canCopyRowAsInsertWithoutPrimaryKeys, canCopyRowAsUpdate,