feat(grid): 新增原始数据复制模式并设为默认

* ✨ feat(grid): 新增原始数据复制模式并设为默认

- 原样输出单元格内容,不对引号、制表符和换行进行 DSV 转义
- 支持原始数据复制后的网格粘贴还原
- 补充多语言复制选项文案及前后端提取器实现与测试

* ✨ feat(grid): 优化新增智能复制选项

- 单个单元格复制保留原始单元格文本,多单元格自动使用转义 TSV
- 默认复制设置由格式选择升级为复制行为选择,并支持预览
- 限制原始文本格式仅用于单单元格复制
- 更新多语言文案及相关测试
This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-08-05 23:58:01 +08:00
1 parent e749bdd42c
commit 930aa48c20
24 files changed
+333 -91

No files matched your search

@@ -1640,10 +1640,10 @@ defineExpose({ focusSearch });
</span>
</label>
<DataGridCopyFormatControl
:current-label="dataGridRef?.defaultCopyExtractorLabel ?? '-'"
:current-value="dataGridRef?.defaultCopyExtractor ?? ''"
:items="dataGridRef?.copyExtractorMenuItems ?? []"
@select="dataGridRef?.setDefaultCopyExtractor($event)"
:current-label="dataGridRef?.defaultCopyPreferenceLabel ?? '-'"
:current-value="dataGridRef?.defaultCopyPreference ?? ''"
:items="dataGridRef?.copyPreferenceMenuItems ?? []"
@select="dataGridRef?.setDefaultCopyPreference($event)"
@configure="openDataGridExtractorConfiguration"
/>
</PopoverContent>
+42 -32
View File
@@ -189,7 +189,7 @@ import { useDataGridExport, type MongoCopyUpdateTarget } from "@/composables/use
import { eventTargetAllowsNativeClipboard, isPlainClipboardShortcut, readTextFromClipboard } from "@/lib/common/clipboard";
import { claimDataGridPaste, claimDataGridSelectAll, clearDataGridClipboardCopy, parseDataGridClipboard, planDataGridPaste } from "@/lib/dataGrid/dataGridClipboard";
import { beginDataGridNativeSelectionBlock, finishDataGridNativeSelectionBlock } from "@/lib/dataGrid/dataGridNativeSelection";
import { DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS, DATA_GRID_COPY_EXTRACTOR_IDS, extractorUnavailableForDatabase, type DataGridCopyExtractorId } from "@/lib/dataGrid/dataGridCopyExtractor";
import { DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS, DATA_GRID_COPY_EXTRACTOR_IDS, DATA_GRID_DEFAULT_COPY_PREFERENCES, extractorUnavailableForDatabase, type DataGridCopyExtractorId, type DataGridCopyPreference } from "@/lib/dataGrid/dataGridCopyExtractor";
import { columnNamesForCopy } from "@/lib/dataGrid/dataGridColumnNameCopy";
import { DATA_GRID_ROW_NUM_WIDTH, dataGridRowNumberColumnWidth, resolveDataGridMaxRowNumber, useDataGridColumnResize } from "@/composables/useDataGridColumnResize";
import { createDataGridColumnStructureSignature } from "@/lib/dataGrid/dataGridColumnWidthState";
@@ -5497,7 +5497,8 @@ const {
canCopyRow,
copyAll,
copyWithExtractor,
previewWithExtractor,
copyWithPreference,
previewWithPreference,
canCopyWithExtractor,
exportCsv,
exportCurrentPageCsv,
@@ -5566,6 +5567,7 @@ const {
function copyExtractorLabel(extractor: DataGridCopyExtractorId): string {
const labels: Record<DataGridCopyExtractorId, string> = {
raw: t("grid.copyExtractorRaw"),
tsv: "TSV",
"tsv-with-headers": t("grid.copyExtractorWithHeaders", { format: "TSV" }),
csv: "CSV",
@@ -5587,28 +5589,34 @@ function copyExtractorLabel(extractor: DataGridCopyExtractorId): string {
return labels[extractor];
}
const selectedCopyExtractor = computed(() => settingsStore.editorSettings.dataGridCopyExtractor);
const defaultCopyExtractorLabel = computed(() => {
if (selectedCopyExtractor.value !== "sql-inserts") return copyExtractorLabel(selectedCopyExtractor.value);
function copyPreferenceLabel(preference: DataGridCopyPreference): string {
if (preference === "smart") return t("grid.copyExtractorSmart");
if (preference !== "sql-inserts") return copyExtractorLabel(preference);
return t(settingsStore.editorSettings.dataGridExtractorOptions.sql.excludePrimaryKeysFromInsert ? "grid.copyExtractorSqlInsertsWithoutPrimaryKeys" : "grid.copyExtractorSqlInsertsWithPrimaryKeys");
});
}
const selectedCopyPreference = computed(() => settingsStore.editorSettings.dataGridCopyExtractor);
const defaultCopyPreferenceLabel = computed(() => copyPreferenceLabel(selectedCopyPreference.value));
const extractorConfigOpen = ref(false);
const extractorMenuItems = computed(() =>
DATA_GRID_COPY_EXTRACTOR_IDS.map((extractor) => ({
value: extractor,
label: copyExtractorLabel(extractor),
disabled: extractorUnavailableForDatabase(extractor, props.databaseType) || (extractor === "sql-updates" && !props.tableMeta?.primaryKeys.length),
separatorBefore: DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS[extractor].separatorBefore,
})),
const copyPreferenceMenuItems = computed(() =>
DATA_GRID_DEFAULT_COPY_PREFERENCES.map((preference) => {
const extractor = preference === "smart" ? undefined : preference;
return {
value: preference,
label: copyPreferenceLabel(preference),
disabled: extractor ? extractorUnavailableForDatabase(extractor, props.databaseType) || (extractor === "sql-updates" && !props.tableMeta?.primaryKeys.length) : false,
separatorBefore: extractor ? DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS[extractor].separatorBefore : false,
};
}),
);
function openExtractorConfiguration() {
extractorConfigOpen.value = true;
}
function setDefaultCopyExtractor(value: string) {
const item = extractorMenuItems.value.find((candidate) => candidate.value === value);
function setDefaultCopyPreference(value: string) {
const item = copyPreferenceMenuItems.value.find((candidate) => candidate.value === value);
if (!item || item.disabled) return;
settingsStore.updateEditorSettings({ dataGridCopyExtractor: item.value });
settingsStore.updateEditorSettings({ dataGridCopyExtractor: item.value as DataGridCopyPreference });
}
function sqlInsertExtractorOptions(excludePrimaryKeysFromInsert: boolean) {
@@ -5619,8 +5627,8 @@ function sqlInsertExtractorOptions(excludePrimaryKeysFromInsert: boolean) {
};
}
function saveExtractorConfiguration(value: { extractor: DataGridCopyExtractorId; options: typeof settingsStore.editorSettings.dataGridExtractorOptions }) {
settingsStore.updateEditorSettings({ dataGridCopyExtractor: value.extractor, dataGridExtractorOptions: value.options });
function saveExtractorConfiguration(value: { preference: DataGridCopyPreference; options: typeof settingsStore.editorSettings.dataGridExtractorOptions }) {
settingsStore.updateEditorSettings({ dataGridCopyExtractor: value.preference, dataGridExtractorOptions: value.options });
}
const pageSizeMenuItems = computed(() =>
@@ -6575,7 +6583,7 @@ async function onGridKeydown(event: KeyboardEvent) {
if (clipboardShortcut(event, "c")) {
if (!hasCellSelection.value && !hasRowSelection.value) return;
event.preventDefault();
await copyWithExtractor(selectedCopyExtractor.value);
await copyWithPreference(selectedCopyPreference.value);
return;
}
if (clipboardShortcut(event, "a")) {
@@ -8187,10 +8195,10 @@ defineExpose({
exportSql,
exportXlsx,
exportTxt,
defaultCopyExtractor: selectedCopyExtractor,
defaultCopyExtractorLabel,
copyExtractorMenuItems: extractorMenuItems,
setDefaultCopyExtractor,
defaultCopyPreference: selectedCopyPreference,
defaultCopyPreferenceLabel,
copyPreferenceMenuItems,
setDefaultCopyPreference,
openExtractorConfiguration,
});
@@ -8228,11 +8236,12 @@ function buildExtractorContextItems(): ContextMenuItem[] {
const items: ContextMenuItem[] = [];
let separatorPending = false;
for (const extractor of DATA_GRID_COPY_EXTRACTOR_IDS) {
if (extractor === "raw" && !canCopyWithExtractor(extractor)) continue;
const descriptor = DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS[extractor];
if (descriptor.separatorBefore) {
separatorPending = items.length > 0;
}
const selected = extractor === selectedCopyExtractor.value;
const selected = extractor === selectedCopyPreference.value;
const extractorItems: ContextMenuItem[] = [];
if (extractor === "sql-inserts") {
for (const excludePrimaryKeysFromInsert of [false, true]) {
@@ -8265,13 +8274,14 @@ function buildExtractorContextItems(): ContextMenuItem[] {
return items;
}
function defaultExtractorContextItem(): ContextMenuItem {
const extractor = selectedCopyExtractor.value;
const extractorOptions = extractor === "sql-inserts" ? sqlInsertExtractorOptions(settingsStore.editorSettings.dataGridExtractorOptions.sql.excludePrimaryKeysFromInsert) : undefined;
function defaultCopyPreferenceContextItem(): ContextMenuItem {
const preference = selectedCopyPreference.value;
const extractorOptions = preference === "sql-inserts" ? sqlInsertExtractorOptions(settingsStore.editorSettings.dataGridExtractorOptions.sql.excludePrimaryKeysFromInsert) : undefined;
const disabled = preference === "smart" ? !canCopyWithExtractor("raw", extractorOptions) && !canCopyWithExtractor("tsv", extractorOptions) : !canCopyWithExtractor(preference, extractorOptions);
return {
label: defaultCopyExtractorLabel.value,
action: () => void copyWithExtractor(extractor, extractorOptions),
disabled: !canCopyWithExtractor(extractor, extractorOptions),
label: defaultCopyPreferenceLabel.value,
action: () => void copyWithPreference(preference, extractorOptions),
disabled,
checked: true,
};
}
@@ -8283,7 +8293,7 @@ function copySubmenu(): ContextMenuItem {
icon: Settings2,
action: openExtractorConfiguration,
},
defaultExtractorContextItem(),
defaultCopyPreferenceContextItem(),
{ label: "", separator: true },
];
if (contextColumn.value) {
@@ -10047,7 +10057,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
<DataGridBulkEditDialog v-if="bulkEditDialogMounted" v-model:open="bulkEditDialogOpen" v-model:value="bulkEditValue" :selected-cell-count="selectedCellCount" @apply="applyBulkEditValue" />
<DataGridInsertRowsDialog v-if="insertRowsDialogMounted" v-model:open="insertRowsDialogOpen" :can-place-at-selection="canPlaceInsertAtSelection" :initial-position="insertPosition" @insert="insertRows" />
<DataGridExtractorDialog v-model:open="extractorConfigOpen" :extractor="selectedCopyExtractor" :options="settingsStore.editorSettings.dataGridExtractorOptions" :items="extractorMenuItems" :preview="previewWithExtractor" @save="saveExtractorConfiguration" />
<DataGridExtractorDialog v-model:open="extractorConfigOpen" :preference="selectedCopyPreference" :options="settingsStore.editorSettings.dataGridExtractorOptions" :items="copyPreferenceMenuItems" :preview="previewWithPreference" @save="saveExtractorConfiguration" />
<DataGridCopyColumnNamesDialog v-if="copyColumnNamesDialogMounted" v-model:open="copyColumnNamesDialogOpen" :column-names="copyColumnNamesDialogColumns" :database-type="resolvedDatabaseType" :column-comments="columnCommentMap" @copy="copyText" />
<Dialog v-model:open="generateIncrementDialogOpen">
@@ -6,23 +6,32 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS, normalizeDataGridExtractorOptions, validateDataGridExtractorOptions, type DataGridCopyExtractorId, type DataGridExtractorOptions, type DataGridExtractPreview, type DataGridExtractWarningCode } from "@/lib/dataGrid/dataGridCopyExtractor";
import {
DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS,
normalizeDataGridExtractorOptions,
validateDataGridExtractorOptions,
type DataGridCopyExtractorId,
type DataGridCopyPreference,
type DataGridExtractorOptions,
type DataGridExtractPreview,
type DataGridExtractWarningCode,
} from "@/lib/dataGrid/dataGridCopyExtractor";
const props = defineProps<{
open: boolean;
extractor: DataGridCopyExtractorId;
preference: DataGridCopyPreference;
options: DataGridExtractorOptions;
items: Array<{ value: DataGridCopyExtractorId; label: string; disabled?: boolean }>;
preview: (extractor: DataGridCopyExtractorId, options: DataGridExtractorOptions) => Promise<DataGridExtractPreview>;
items: Array<{ value: DataGridCopyPreference; label: string; disabled?: boolean }>;
preview: (preference: DataGridCopyPreference, options: DataGridExtractorOptions) => Promise<DataGridExtractPreview>;
}>();
const emit = defineEmits<{
"update:open": [value: boolean];
save: [value: { extractor: DataGridCopyExtractorId; options: DataGridExtractorOptions }];
save: [value: { preference: DataGridCopyPreference; options: DataGridExtractorOptions }];
}>();
const { t } = useI18n();
const draftExtractor = ref<DataGridCopyExtractorId>(props.extractor);
const draftPreference = ref<DataGridCopyPreference>(props.preference);
const draftOptions = ref<DataGridExtractorOptions>(normalizeDataGridExtractorOptions(props.options));
const previewText = ref("");
const previewError = ref("");
@@ -32,11 +41,13 @@ const previewTruncatedRows = ref(0);
let previewSequence = 0;
let previewTimer: ReturnType<typeof setTimeout> | undefined;
const extractorCategory = computed(() => DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS[draftExtractor.value].category);
const draftExtractor = computed<DataGridCopyExtractorId | null>(() => (draftPreference.value === "smart" ? null : draftPreference.value));
const extractorCategory = computed(() => (draftExtractor.value ? DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS[draftExtractor.value].category : null));
const isDsv = computed(() => extractorCategory.value === "delimited");
const isSql = computed(() => draftExtractor.value === "sql-inserts" || draftExtractor.value === "sql-updates");
const isJson = computed(() => extractorCategory.value === "json");
const optionsError = computed(() => {
if (!draftExtractor.value) return "";
const code = validateDataGridExtractorOptions(draftExtractor.value, draftOptions.value);
if (!code) return "";
return t(`grid.copyExtractorValidation.${code}`);
@@ -70,7 +81,7 @@ async function refreshPreview() {
return;
}
try {
const result = await props.preview(draftExtractor.value, normalizeDataGridExtractorOptions(draftOptions.value));
const result = await props.preview(draftPreference.value, normalizeDataGridExtractorOptions(draftOptions.value));
if (sequence === previewSequence) {
previewText.value = result.text;
previewWarnings.value = (result.warnings ?? []).map((warning) => warningText(warning.code, result.omittedColumns));
@@ -105,14 +116,14 @@ watch(
previewLoading.value = false;
return;
}
draftExtractor.value = props.extractor;
draftPreference.value = props.preference;
draftOptions.value = normalizeDataGridExtractorOptions(props.options);
schedulePreview();
},
);
watch(
[draftExtractor, draftOptions],
[draftPreference, draftOptions],
() => {
if (props.open) schedulePreview();
},
@@ -126,7 +137,7 @@ onBeforeUnmount(() => {
function save() {
if (optionsError.value) return;
emit("save", { extractor: draftExtractor.value, options: normalizeDataGridExtractorOptions(draftOptions.value) });
emit("save", { preference: draftPreference.value, options: normalizeDataGridExtractorOptions(draftOptions.value) });
emit("update:open", false);
}
</script>
@@ -142,7 +153,7 @@ function save() {
<div class="space-y-3">
<div class="space-y-1.5">
<Label>{{ t("grid.copyExtractorDefaultFormat") }}</Label>
<Select v-model="draftExtractor">
<Select v-model="draftPreference">
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem v-for="item in items" :key="item.value" :value="item.value" :disabled="item.disabled">{{ item.label }}</SelectItem>
@@ -108,10 +108,10 @@ type DataGridHandle = DataGridColumnLayoutHandle & {
allNullColumnCount: number;
canToggleAllNullColumns: boolean;
toggleAllNullColumns: () => void;
defaultCopyExtractor: string;
defaultCopyExtractorLabel: string;
copyExtractorMenuItems: Array<{ value: string; label: string; disabled?: boolean; separatorBefore?: boolean }>;
setDefaultCopyExtractor: (value: string) => void;
defaultCopyPreference: string;
defaultCopyPreferenceLabel: string;
copyPreferenceMenuItems: Array<{ value: string; label: string; disabled?: boolean; separatorBefore?: boolean }>;
setDefaultCopyPreference: (value: string) => void;
openExtractorConfiguration: () => void;
showDdl: boolean;
toggleDdl: (tab?: TableInfoTab) => void;
@@ -1286,10 +1286,10 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
<Switch size="sm" :model-value="!!dataGridRef?.nullColumnsHidden" :disabled="!dataGridRef?.canToggleAllNullColumns" :aria-label="t('grid.hideNullColumns')" @update:model-value="dataGridRef?.toggleAllNullColumns()" />
</div>
<DataGridCopyFormatControl
:current-label="dataGridRef?.defaultCopyExtractorLabel ?? '-'"
:current-value="dataGridRef?.defaultCopyExtractor ?? ''"
:items="dataGridRef?.copyExtractorMenuItems ?? []"
@select="dataGridRef?.setDefaultCopyExtractor($event)"
:current-label="dataGridRef?.defaultCopyPreferenceLabel ?? '-'"
:current-value="dataGridRef?.defaultCopyPreference ?? ''"
:items="dataGridRef?.copyPreferenceMenuItems ?? []"
@select="dataGridRef?.setDefaultCopyPreference($event)"
@configure="openDataGridExtractorConfiguration"
/>
</PopoverContent>
@@ -1732,10 +1732,10 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
<Switch size="sm" :model-value="!!dataGridRef?.nullColumnsHidden" :disabled="!dataGridRef?.canToggleAllNullColumns" :aria-label="t('grid.hideNullColumns')" @update:model-value="dataGridRef?.toggleAllNullColumns()" />
</div>
<DataGridCopyFormatControl
:current-label="dataGridRef?.defaultCopyExtractorLabel ?? '-'"
:current-value="dataGridRef?.defaultCopyExtractor ?? ''"
:items="dataGridRef?.copyExtractorMenuItems ?? []"
@select="dataGridRef?.setDefaultCopyExtractor($event)"
:current-label="dataGridRef?.defaultCopyPreferenceLabel ?? '-'"
:current-value="dataGridRef?.defaultCopyPreference ?? ''"
:items="dataGridRef?.copyPreferenceMenuItems ?? []"
@select="dataGridRef?.setDefaultCopyPreference($event)"
@configure="openDataGridExtractorConfiguration"
/>
</PopoverContent>