fix(app): harden clipboard copy flows
This commit is contained in:
parent
ccfc3cf92c
commit
2fea0c38c5
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<PreparedCopyValue>({
|
||||
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({
|
|||
<Button variant="ghost" size="sm" class="h-7 justify-start text-xs" @click="copyDetailColumnName">
|
||||
<Copy class="w-3 h-3 mr-2" /> {{ t("grid.copyColumnName") }}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" class="h-7 justify-start text-xs" @click="copyDetailSqlCondition">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 justify-start text-xs"
|
||||
:disabled="!canCopyPreparedDetailSqlCondition()"
|
||||
@click="copyDetailSqlCondition"
|
||||
>
|
||||
<Code2 class="w-3 h-3 mr-2" /> {{ t("grid.copySqlCondition") }}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -4598,17 +4695,21 @@ defineExpose({
|
|||
<ContextMenuItem @click="copyRow">
|
||||
{{ isMultiRow ? t("grid.copyRows", { count: multiRowCount }) : t("grid.copyRow") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem @click="copyRowAsInsert">
|
||||
<ContextMenuItem :disabled="!canCopyPreparedInsert(false)" @click="copyRowAsInsert">
|
||||
{{ isMultiRow ? t("grid.copyRowsInsert", { count: multiRowCount }) : t("grid.copyRowInsert") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem v-if="canCopyRowAsInsertWithoutPrimaryKeys" @click="copyRowAsInsertWithoutPrimaryKeys">
|
||||
<ContextMenuItem
|
||||
v-if="canCopyRowAsInsertWithoutPrimaryKeys"
|
||||
:disabled="!canCopyPreparedInsert(true)"
|
||||
@click="copyRowAsInsertWithoutPrimaryKeys"
|
||||
>
|
||||
{{
|
||||
isMultiRow
|
||||
? t("grid.copyRowsInsertWithoutPrimaryKeys", { count: multiRowCount })
|
||||
: t("grid.copyRowInsertWithoutPrimaryKeys")
|
||||
}}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem v-if="canCopyRowAsUpdate" @click="copyRowAsUpdate">
|
||||
<ContextMenuItem v-if="canCopyRowAsUpdate" :disabled="!canCopyPreparedUpdate()" @click="copyRowAsUpdate">
|
||||
{{ isMultiRow ? t("grid.copyRowsUpdate", { count: multiRowCount }) : t("grid.copyRowUpdate") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem @click="copyAll">{{ t("grid.copyAll") }}</ContextMenuItem>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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<boolean>;
|
||||
}
|
||||
|
||||
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<CopyStatementCache>({
|
||||
key: "",
|
||||
text: "",
|
||||
loading: false,
|
||||
ready: false,
|
||||
});
|
||||
const copyRowInsertWithoutPrimaryKeysCache = ref<CopyStatementCache>({
|
||||
key: "",
|
||||
text: "",
|
||||
loading: false,
|
||||
ready: false,
|
||||
});
|
||||
const copyRowUpdateCache = ref<CopyStatementCache>({
|
||||
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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue