feat(schema-diff): named config selector and mapping UX

This commit is contained in:
Mo-Moore authored and GitHub committed 2026-09-14 12:37:39 +08:00
1 parent 2808996526
commit 18e34bf33a
15 files changed
+282 -15

No files matched your search

@@ -28,7 +28,7 @@ import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import * as api from "@/lib/backend/api";
import { isSchemaAware } from "@/lib/database/databaseCapabilities";
import { fetchNamespaceOptionsForConnection } from "@/composables/useDatabaseOptions";
import { ArrowLeftRight, GitCompareArrows, Save, FolderOpen, Settings, X } from "@lucide/vue";
import { ArrowLeftRight, GitCompareArrows, Save, FolderOpen, Settings, Trash2, X } from "@lucide/vue";
import type { SchemaDiffConfig, SchemaDiffCompareOptions, FieldMappingEntry, SchemaDiffTableMapping, SchemaDiffRoutineMapping } from "@/types/schemaDiff";
import type { DatabaseType } from "@/types/database";
@@ -166,6 +166,16 @@ function handleUpdateSelectedTables(value: string[]) {
reconcileTableMappings(value);
}
function handleRemoveSelectedTable(sourceTable: string) {
handleUpdateSelectedTables(localSelectedTables.value.filter((table) => table !== sourceTable));
}
function matchStatusClass(kind: SchemaDiffTableMatch["kind"]): string {
if (kind === "unmatched") return "text-destructive";
if (kind === "manual") return "text-amber-600 dark:text-amber-400";
return "text-muted-foreground";
}
function clearUnavailableTableSelection() {
sourceTableList.value = [];
targetTableList.value = [];
@@ -307,6 +317,10 @@ function handleUpdateSelectedRoutines(value: string[]) {
reconcileRoutineMappings(value);
}
function handleRemoveSelectedRoutine(sourceRoutine: string) {
handleUpdateSelectedRoutines(localSelectedRoutines.value.filter((routine) => routine !== sourceRoutine));
}
function clearUnavailableRoutineSelection() {
sourceRoutineList.value = [];
targetRoutineList.value = [];
@@ -861,6 +875,7 @@ async function fetchDbVersion(connectionId: string, database: string, schema: st
<th class="px-2 py-1.5 text-left font-medium">{{ t("diff.sourceTables") }}</th>
<th class="px-2 py-1.5 text-left font-medium">{{ t("diff.targetTable") }}</th>
<th class="w-28 px-2 py-1.5 text-left font-medium">{{ t("diff.status") }}</th>
<th class="w-16 px-2 py-1.5 text-right font-medium">{{ t("common.actions") }}</th>
</tr>
</thead>
<tbody>
@@ -881,9 +896,14 @@ async function fetchDbVersion(connectionId: string, database: string, schema: st
content-class="w-[var(--reka-popover-trigger-width)]"
/>
</td>
<td class="px-2 py-1.5" :class="match.kind === 'unmatched' ? 'text-destructive' : match.kind === 'manual' ? 'text-amber-600 dark:text-amber-400' : 'text-muted-foreground'">
<td class="px-2 py-1.5" :class="matchStatusClass(match.kind)">
{{ t(`diff.tableMatchStatus.${match.kind}`) }}
</td>
<td class="px-2 py-1.5 text-right">
<Button variant="ghost" size="sm" class="h-7 w-7 p-0" :aria-label="t('common.delete')" @click="handleRemoveSelectedTable(match.sourceTable)">
<Trash2 class="h-3.5 w-3.5" />
</Button>
</td>
</tr>
</tbody>
</table>
@@ -926,6 +946,7 @@ async function fetchDbVersion(connectionId: string, database: string, schema: st
@update:model-value="handleUpdateSelectedRoutines"
:tables="sourceRoutineList"
:title="t('diff.sourceRoutines')"
:search-placeholder="t('diff.searchSourceRoutines')"
:empty-text="t('diff.noRoutines')"
/>
</template>
@@ -946,6 +967,7 @@ async function fetchDbVersion(connectionId: string, database: string, schema: st
<th class="px-2 py-1.5 text-left font-medium">{{ t("diff.sourceRoutines") }}</th>
<th class="px-2 py-1.5 text-left font-medium">{{ t("diff.targetRoutine") }}</th>
<th class="w-28 px-2 py-1.5 text-left font-medium">{{ t("diff.status") }}</th>
<th class="w-16 px-2 py-1.5 text-right font-medium">{{ t("common.actions") }}</th>
</tr>
</thead>
<tbody>
@@ -955,9 +977,14 @@ async function fetchDbVersion(connectionId: string, database: string, schema: st
<span v-if="match.targetRoutine">{{ match.targetRoutine }}</span>
<span v-else class="text-muted-foreground">—</span>
</td>
<td class="px-2 py-1.5" :class="match.kind === 'unmatched' ? 'text-destructive' : 'text-muted-foreground'">
<td class="px-2 py-1.5" :class="matchStatusClass(match.kind)">
{{ t(`diff.tableMatchStatus.${match.kind}`) }}
</td>
<td class="px-2 py-1.5 text-right">
<Button variant="ghost" size="sm" class="h-7 w-7 p-0" :aria-label="t('common.delete')" @click="handleRemoveSelectedRoutine(match.sourceRoutine)">
<Trash2 class="h-3.5 w-3.5" />
</Button>
</td>
</tr>
</tbody>
</table>
@@ -11,6 +11,7 @@ import { executeWithProductionSqlGuard } from "@/lib/database/productionExecutio
import { isSchemaAware } from "@/lib/database/databaseCapabilities";
import { useSchemaDiffConfig } from "@/composables/useSchemaDiffConfig";
import SchemaDiffConfigStep from "@/components/diff/SchemaDiffConfigStep.vue";
import SchemaDiffConfigSelector from "@/components/diff/SchemaDiffConfigSelector.vue";
import FieldMappingDialog from "@/components/diff/FieldMappingDialog.vue";
import SchemaDiffObjectTree from "@/components/diff/SchemaDiffObjectTree.vue";
import SchemaDiffRoutineList from "@/components/diff/SchemaDiffRoutineList.vue";
@@ -103,6 +104,7 @@ const ignoreComments = ref(false);
// Options panel
const showOptionsPanel = ref(false);
const showFieldMappingDialog = ref(false);
const showConfigSelector = ref(false);
const sourceDbType = computed(() => store.getConfig(sourceConnectionId.value)?.db_type ?? "");
const targetDbType = computed(() => store.getConfig(targetConnectionId.value)?.db_type ?? "");
@@ -254,6 +256,11 @@ function toggleMaximize() {
}
function handleDialogEscape(event: KeyboardEvent) {
if (showConfigSelector.value) {
event.preventDefault();
showConfigSelector.value = false;
return;
}
if (!showOptionsPanel.value) return;
event.preventDefault();
@@ -313,7 +320,7 @@ onBeforeUnmount(() => {
});
// Config management
const { configs, activeConfigId, activeConfig, recentConfigs, ensureDefaultConfig, updateActiveConfigConnection, updateActiveConfigOptions, saveToHistory, deleteFromHistory } = useSchemaDiffConfig();
const { configs, activeConfigId, activeConfig, recentConfigs, ensureDefaultConfig, createConfig, updateConfig, renameConfig, deleteConfig, duplicateConfig, importConfigs, updateActiveConfigConnection, updateActiveConfigOptions, saveToHistory, deleteFromHistory } = useSchemaDiffConfig();
const schemaDiffPanelOptions = computed(() => normalizeSchemaDiffCompareOptions(activeConfig.value?.options, getDbType()));
const selectedObject = computed(() => {
@@ -1075,6 +1082,13 @@ async function copyRoutineDiffSide(side: TextDiffSide) {
}
}
function handleLoadHistoryConfig(config: SchemaDiffConfig) {
applyConfigToForm(config);
if (config.options) {
updateActiveConfigOptions(normalizeSchemaDiffCompareOptions(config.options, getDbType()));
}
}
function applyConfigToForm(config: SchemaDiffConfig) {
runWithoutTargetIdentityReset(() => {
sourceConnectionId.value = config.sourceConnectionId;
sourceDatabase.value = config.sourceDatabase;
@@ -1083,18 +1097,85 @@ function handleLoadHistoryConfig(config: SchemaDiffConfig) {
targetDatabase.value = config.targetDatabase;
targetSchema.value = config.targetSchema;
});
if (config.options) {
updateActiveConfigOptions(normalizeSchemaDiffCompareOptions(config.options, getDbType()));
}
}
function snapshotCurrentConfig(name: string): SchemaDiffConfig {
ensureDefaultConfig(getDbType());
const base = activeConfig.value;
return {
id: base?.id ?? "",
name,
createdAt: base?.createdAt ?? Date.now(),
updatedAt: Date.now(),
sourceConnectionId: sourceConnectionId.value,
sourceDatabase: sourceDatabase.value,
sourceSchema: sourceSchema.value,
targetConnectionId: targetConnectionId.value,
targetDatabase: targetDatabase.value,
targetSchema: targetSchema.value,
options: { ...schemaDiffPanelOptions.value },
};
}
function handleLoadConfig() {
ensureDefaultConfig(getDbType());
showConfigSelector.value = true;
}
function handleSelectActiveConfig(id: string) {
if (!id || id === activeConfigId.value) return;
activeConfigId.value = id;
const config = configs.value.find((entry) => entry.id === id);
if (config) applyConfigToForm(config);
}
function handleSaveConfig() {
if (activeConfig.value) {
const name = window.prompt(t("diff.saveConfigPrompt"), activeConfig.value.name || t("diff.defaultConfigName"));
if (name === null) return; // User cancelled
const configToSave = { ...activeConfig.value, name: name.trim() || t("diff.defaultConfigName") };
saveToHistory(configToSave);
ensureDefaultConfig(getDbType());
if (!activeConfig.value) return;
const name = window.prompt(t("diff.saveConfigPrompt"), activeConfig.value.name || t("diff.defaultConfigName"));
if (name === null) return;
const trimmed = name.trim() || t("diff.defaultConfigName");
const snapshot = snapshotCurrentConfig(trimmed);
updateConfig(activeConfig.value.id, {
name: trimmed,
sourceConnectionId: snapshot.sourceConnectionId,
sourceDatabase: snapshot.sourceDatabase,
sourceSchema: snapshot.sourceSchema,
targetConnectionId: snapshot.targetConnectionId,
targetDatabase: snapshot.targetDatabase,
targetSchema: snapshot.targetSchema,
options: snapshot.options,
});
saveToHistory({ ...snapshot, id: activeConfig.value.id });
toast(t("diff.configSaved"), 2000);
}
function handleCreateNamedConfig(name: string) {
const snapshot = snapshotCurrentConfig(name);
createConfig(name, snapshot, getDbType());
}
function handleRenameNamedConfig(id: string, name: string) {
renameConfig(id, name);
}
function handleDeleteNamedConfig(id: string) {
deleteConfig(id);
if (activeConfig.value) applyConfigToForm(activeConfig.value);
}
function handleDuplicateNamedConfig(id: string) {
duplicateConfig(id);
if (activeConfig.value) applyConfigToForm(activeConfig.value);
}
function handleImportNamedConfigs(jsonText: string) {
try {
importConfigs(jsonText, "merge");
if (activeConfig.value) applyConfigToForm(activeConfig.value);
toast(t("diff.configSaved"), 2000);
} catch (error) {
toast(error instanceof Error ? error.message : String(error), 5000);
}
}
@@ -1237,6 +1318,7 @@ const targetConnectionInfo = computed(() => {
@swap="handleSwap"
@show-options="showOptionsPanel = true"
@save-config="handleSaveConfig"
@load-config="handleLoadConfig"
@load-history-config="handleLoadHistoryConfig"
@delete-history-config="handleDeleteHistoryConfig"
@update:field-mappings="handleFieldMappingsUpdate"
@@ -1566,6 +1648,26 @@ const targetConnectionInfo = computed(() => {
</div>
</div>
<!-- Config selector overlay (save/load named configs) -->
<div v-if="showConfigSelector" class="absolute inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm" @click.self="showConfigSelector = false">
<div class="max-h-[80vh] w-[720px] max-w-[calc(100vw-2rem)] overflow-auto rounded-lg border bg-card p-4 shadow-lg">
<div class="mb-4 flex items-center justify-between gap-2">
<h3 class="text-sm font-medium">{{ t("diff.loadConfig") }}</h3>
<Button variant="ghost" size="sm" :aria-label="t('common.close')" @click="showConfigSelector = false">✕</Button>
</div>
<SchemaDiffConfigSelector
:configs="configs"
:active-config-id="activeConfigId"
@update:active-config-id="handleSelectActiveConfig"
@create="handleCreateNamedConfig"
@rename="handleRenameNamedConfig"
@delete="handleDeleteNamedConfig"
@duplicate="handleDuplicateNamedConfig"
@import="handleImportNamedConfigs"
/>
</div>
</div>
<!-- Field Mapping Dialog Overlay -->
<FieldMappingDialog
:open="showFieldMappingDialog"
@@ -20,6 +20,8 @@ const props = withDefaults(
modelValue: string[];
/** Optional header label; falls back to `tableMultiSelect.tables`. */
title?: string;
/** Optional search input placeholder; falls back to `tableMultiSelect.search`. */
searchPlaceholder?: string;
/** Show the search box (only rendered when there are more than 5 tables). */
searchable?: boolean;
/** Text shown when `tables` is empty. */
@@ -28,6 +30,7 @@ const props = withDefaults(
}>(),
{
title: "",
searchPlaceholder: "",
searchable: true,
emptyText: "",
disabled: false,
@@ -64,7 +67,7 @@ function toggleSelectAll() {
</div>
</div>
<Input v-if="showSearch" v-model="search" class="h-7 text-xs" :placeholder="t('tableMultiSelect.search')" :disabled="disabled" />
<Input v-if="showSearch" v-model="search" class="h-7 text-xs" :placeholder="searchPlaceholder || t('tableMultiSelect.search')" :disabled="disabled" />
<div class="flex items-center gap-2">
<Button v-if="tables.length" variant="outline" size="sm" class="h-7 px-2 text-xs" :disabled="disabled" @click="toggleSelectAll">
+12
View File
@@ -6358,6 +6358,14 @@ export default withEnglishFallback({
syncSummary: "Uğurlu: {success}, uğursuz: {failed}",
configSaved: "Konfiqurasiya yadda saxlanıldı",
configDeleted: "Konfiqurasiya silindi",
comparisonObjects: "Müqayisə obyektləri",
searchObjects: "Obyekt adlarını axtar...",
selectAllObjects: "Hamısını seç",
deselectAllObjects: "Seçimi ləğv et",
objectPickerAll: "Hamısı",
objectGroupDisabled: "Bu obyekt növü müqayisəyə daxil deyil.",
noObjects: "Obyekt yoxdur",
noObjectMatches: "Uyğunluq yoxdur",
tableSelection: "Cədvəllər",
chooseTables: "Cədvəlləri seç...",
compareAllTables: "Bütün cədvəlləri müqayisə et",
@@ -6367,6 +6375,10 @@ export default withEnglishFallback({
matchedTables: "{matched}/{total} cədvəl uyğunlaşdırıldı",
missingTargetTables: "Eyni adlı hədəf cədvəllər yoxdur: {tables}",
targetTableMatching: "Hədəf cədvəllərin uyğunlaşdırılması",
sourceTable: "Mənbə cədvəl",
targetTable: "Hədəf cədvəl",
similarity: "Oxşarlıq",
searchSourceRoutines: "Obyektləri axtar...",
selectTargetTable: "Hədəf cədvəli seç",
searchTargetTable: "Hədəf cədvəlləri axtar...",
noTargetTables: "Mövcud hədəf cədvəl yoxdur",
+12
View File
@@ -6495,6 +6495,14 @@ export default {
syncSummary: "Succeeded {success}, failed {failed}",
configSaved: "Config saved",
configDeleted: "Config deleted",
comparisonObjects: "Compare objects",
searchObjects: "Search object names...",
selectAllObjects: "Select all",
deselectAllObjects: "Deselect all",
objectPickerAll: "All",
objectGroupDisabled: "This object type is not included in the comparison.",
noObjects: "No objects",
noObjectMatches: "No matches",
tableSelection: "Tables",
chooseTables: "Choose tables...",
compareAllTables: "Compare all tables",
@@ -6517,6 +6525,7 @@ export default {
targetRoutineMatching: "Target routine matching",
matchedRoutines: "{matched}/{total} routines matched",
selectTargetRoutine: "Select target routine",
searchSourceRoutines: "Search objects...",
searchTargetRoutine: "Search target routines...",
noTargetRoutines: "No target routines available",
unmatchedRoutineWarning: "{count} selected source routine(s) have no same-name target",
@@ -6538,6 +6547,9 @@ export default {
matchedTables: "{matched}/{total} tables matched",
missingTargetTables: "Missing same-name target tables: {tables}",
targetTableMatching: "Target table matching",
sourceTable: "Source table",
targetTable: "Target table",
similarity: "Similarity",
selectTargetTable: "Select target table",
searchTargetTable: "Search target tables...",
noTargetTables: "No target tables available",
+12
View File
@@ -6069,6 +6069,14 @@ export default withEnglishFallback({
syncSummary: "Exitosos {success}, fallidos {failed}",
configSaved: "Config guardada",
configDeleted: "Config eliminada",
comparisonObjects: "Objetos a comparar",
searchObjects: "Buscar nombres de objetos...",
selectAllObjects: "Seleccionar todo",
deselectAllObjects: "Deseleccionar todo",
objectPickerAll: "Todos",
objectGroupDisabled: "Este tipo de objeto no está incluido en la comparación.",
noObjects: "Sin objetos",
noObjectMatches: "Sin coincidencias",
tableSelection: "Tablas",
chooseTables: "Elegir tablas...",
compareAllTables: "Comparar todas las tablas",
@@ -6089,6 +6097,7 @@ export default withEnglishFallback({
targetRoutineMatching: "Emparejamiento de rutinas de destino",
matchedRoutines: "{matched}/{total} rutinas coinciden",
selectTargetRoutine: "Selecciona una rutina de destino",
searchSourceRoutines: "Buscar objetos...",
searchTargetRoutine: "Buscar rutinas de destino...",
noTargetRoutines: "No hay rutinas de destino disponibles",
unmatchedRoutineWarning: "{count} rutina(s) de origen seleccionada(s) no tienen destino con el mismo nombre",
@@ -6107,6 +6116,9 @@ export default withEnglishFallback({
matchedTables: "{matched}/{total} tablas coinciden",
missingTargetTables: "Faltan tablas de destino con el mismo nombre: {tables}",
targetTableMatching: "Emparejamiento de tablas de destino",
sourceTable: "Tabla de origen",
targetTable: "Tabla de destino",
similarity: "Similitud",
selectTargetTable: "Selecciona una tabla de destino",
searchTargetTable: "Buscar tablas de destino...",
noTargetTables: "No hay tablas de destino disponibles",
+12
View File
@@ -6069,6 +6069,14 @@ export default withEnglishFallback({
syncSummary: "Riuscite {success}, non riuscite {failed}",
configSaved: "Configurazione salvata",
configDeleted: "Configurazione eliminata",
comparisonObjects: "Oggetti da confrontare",
searchObjects: "Cerca nomi oggetti...",
selectAllObjects: "Seleziona tutto",
deselectAllObjects: "Deseleziona tutto",
objectPickerAll: "Tutti",
objectGroupDisabled: "Questo tipo di oggetto non è incluso nel confronto.",
noObjects: "Nessun oggetto",
noObjectMatches: "Nessuna corrispondenza",
tableSelection: "Tabelle",
chooseTables: "Scegli tabelle...",
compareAllTables: "Confronta tutte le tabelle",
@@ -6089,6 +6097,7 @@ export default withEnglishFallback({
targetRoutineMatching: "Abbinamento routine di destinazione",
matchedRoutines: "{matched}/{total} routine abbinate",
selectTargetRoutine: "Seleziona routine di destinazione",
searchSourceRoutines: "Cerca oggetti...",
searchTargetRoutine: "Cerca routine di destinazione...",
noTargetRoutines: "Nessuna routine di destinazione disponibile",
unmatchedRoutineWarning: "{count} routine di origine selezionate non hanno una destinazione con lo stesso nome",
@@ -6107,6 +6116,9 @@ export default withEnglishFallback({
matchedTables: "{matched}/{total} tabelle abbinate",
missingTargetTables: "Tabelle di destinazione omonime mancanti: {tables}",
targetTableMatching: "Abbinamento delle tabelle di destinazione",
sourceTable: "Tabella sorgente",
targetTable: "Tabella di destinazione",
similarity: "Somiglianza",
selectTargetTable: "Seleziona la tabella di destinazione",
searchTargetTable: "Cerca tabelle di destinazione...",
noTargetTables: "Nessuna tabella di destinazione disponibile",
+12
View File
@@ -6163,6 +6163,14 @@ export default withEnglishFallback({
serverVersion: "サーバーバージョン",
close: "閉じる",
configDeleted: "設定を削除しました",
comparisonObjects: "比較オブジェクト",
searchObjects: "オブジェクト名を検索...",
selectAllObjects: "すべて選択",
deselectAllObjects: "すべて解除",
objectPickerAll: "すべて",
objectGroupDisabled: "このオブジェクト種別は比較対象外です。",
noObjects: "オブジェクトなし",
noObjectMatches: "一致なし",
tableSelection: "テーブル",
chooseTables: "テーブルを選択...",
compareAllTables: "すべてのテーブルを比較",
@@ -6183,6 +6191,7 @@ export default withEnglishFallback({
targetRoutineMatching: "ターゲットのルーチン照合",
matchedRoutines: "{matched}/{total} 件のルーチンが一致",
selectTargetRoutine: "ターゲットのルーチンを選択",
searchSourceRoutines: "オブジェクトを検索...",
searchTargetRoutine: "ターゲットのルーチンを検索...",
noTargetRoutines: "利用可能なターゲットのルーチンがありません",
unmatchedRoutineWarning: "選択したソースのルーチンのうち {count} 件に同名のターゲットがありません",
@@ -6201,6 +6210,9 @@ export default withEnglishFallback({
matchedTables: "{matched}/{total}テーブルがマッチしました",
missingTargetTables: "同名のターゲットテーブルがありません: {tables}",
targetTableMatching: "ターゲットテーブルのマッピング",
sourceTable: "ソーステーブル",
targetTable: "ターゲットテーブル",
similarity: "類似度",
selectTargetTable: "ターゲットテーブルを選択",
searchTargetTable: "ターゲットテーブルを検索...",
noTargetTables: "利用可能なターゲットテーブルがありません",
+11
View File
@@ -5827,12 +5827,22 @@ export default withEnglishFallback({
syncSummary: "성공 {success}, 실패 {failed}",
configSaved: "설정을 저장했습니다",
configDeleted: "설정을 삭제했습니다",
comparisonObjects: "비교 객체",
searchObjects: "객체 이름 검색...",
selectAllObjects: "모두 선택",
deselectAllObjects: "선택 해제",
objectPickerAll: "전체",
objectGroupDisabled: "이 객체 유형은 비교에 포함되지 않습니다.",
noObjects: "객체 없음",
noObjectMatches: "일치 항목 없음",
tableSelection: "테이블",
chooseTables: "테이블 선택...",
compareAllTables: "모든 테이블 비교",
tableSelectionUnrestricted: "정규식 필터 후 모든 테이블을 비교합니다. 특정 테이블을 선택하면 비교 범위를 제한할 수 있습니다.",
sourceTables: "소스 테이블",
targetTable: "대상 테이블",
sourceTable: "소스 테이블",
similarity: "유사도",
autoMatchHint: "대상 테이블은 같은 이름으로 자동 매칭됩니다.",
matchedTables: "{matched}/{total}개 테이블 매칭됨",
missingTargetTables: "같은 이름의 대상 테이블 누락: {tables}",
@@ -5991,6 +6001,7 @@ export default withEnglishFallback({
targetRoutineMatching: "대상 루틴 매칭",
matchedRoutines: "{matched}/{total}개 루틴 매칭됨",
selectTargetRoutine: "대상 루틴 선택",
searchSourceRoutines: "객체 검색...",
searchTargetRoutine: "대상 루틴 검색...",
noTargetRoutines: "사용 가능한 대상 루틴이 없습니다",
unmatchedRoutineWarning: "선택한 소스 루틴 {count}개에 같은 이름의 대상이 없습니다",
+12
View File
@@ -6071,6 +6071,14 @@ export default withEnglishFallback({
selectRecentConfig: "Selecionar configuração recente",
configSaved: "Configuração salva",
configDeleted: "Configuração excluída",
comparisonObjects: "Objetos a comparar",
searchObjects: "Pesquisar nomes de objetos...",
selectAllObjects: "Selecionar tudo",
deselectAllObjects: "Desmarcar tudo",
objectPickerAll: "Todos",
objectGroupDisabled: "Este tipo de objeto não está incluído na comparação.",
noObjects: "Nenhum objeto",
noObjectMatches: "Nenhuma correspondência",
tableSelection: "Tabelas",
chooseTables: "Escolher tabelas...",
compareAllTables: "Comparar todas as tabelas",
@@ -6091,6 +6099,7 @@ export default withEnglishFallback({
targetRoutineMatching: "Correspondência de rotinas de destino",
matchedRoutines: "{matched}/{total} rotinas correspondidas",
selectTargetRoutine: "Selecionar rotina de destino",
searchSourceRoutines: "Pesquisar objetos...",
searchTargetRoutine: "Pesquisar rotinas de destino...",
noTargetRoutines: "Nenhuma rotina de destino disponível",
unmatchedRoutineWarning: "{count} rotina(s) de origem selecionada(s) não têm destino com o mesmo nome",
@@ -6109,6 +6118,9 @@ export default withEnglishFallback({
matchedTables: "{matched}/{total} tabelas correspondidas",
missingTargetTables: "Tabelas de destino com o mesmo nome ausentes: {tables}",
targetTableMatching: "Mapeamento das tabelas de destino",
sourceTable: "Tabela de origem",
targetTable: "Tabela de destino",
similarity: "Similaridade",
selectTargetTable: "Selecione a tabela de destino",
searchTargetTable: "Pesquisar tabelas de destino...",
noTargetTables: "Nenhuma tabela de destino disponível",
+12
View File
@@ -6240,6 +6240,14 @@ export default withEnglishFallback({
syncSummary: "{success} başarılı, {failed} başarısız",
configSaved: "Yapılandırma kaydedildi",
configDeleted: "Yapılandırma silindi",
comparisonObjects: "Karşılaştırılacak nesneler",
searchObjects: "Nesne adlarını ara...",
selectAllObjects: "Tümünü seç",
deselectAllObjects: "Seçimi kaldır",
objectPickerAll: "Tümü",
objectGroupDisabled: "Bu nesne türü karşılaştırmaya dahil değil.",
noObjects: "Nesne yok",
noObjectMatches: "Eşleşme yok",
tableSelection: "Tablolar",
chooseTables: "Tabloları seçin...",
compareAllTables: "Tüm tabloları karşılaştır",
@@ -6249,6 +6257,10 @@ export default withEnglishFallback({
matchedTables: "{matched}/{total} tablo eşleşti",
missingTargetTables: "Aynı adlı hedef tablolar eksik: {tables}",
targetTableMatching: "Hedef tablo eşleştirme",
sourceTable: "Kaynak tablo",
targetTable: "Hedef tablo",
similarity: "Benzerlik",
searchSourceRoutines: "Nesneleri ara...",
selectTargetTable: "Hedef tabloyu seçin",
searchTargetTable: "Hedef tabloları arayın...",
noTargetTables: "Kullanılabilir hedef tablo yok",
+12
View File
@@ -6553,6 +6553,14 @@ export default withEnglishFallback({
copied: "已复制到剪贴板",
configSaved: "配置已保存",
configDeleted: "配置已删除",
comparisonObjects: "对比对象",
searchObjects: "搜索对象名...",
selectAllObjects: "全选",
deselectAllObjects: "取消全选",
objectPickerAll: "全部",
objectGroupDisabled: "该对象类型未纳入本次对比。",
noObjects: "暂无对象",
noObjectMatches: "无匹配",
tableSelection: "表",
chooseTables: "指定表...",
compareAllTables: "比较所有表",
@@ -6575,6 +6583,7 @@ export default withEnglishFallback({
targetRoutineMatching: "目标对象匹配",
matchedRoutines: "已匹配 {matched}/{total} 个对象",
selectTargetRoutine: "请选择目标对象",
searchSourceRoutines: "搜索对象...",
searchTargetRoutine: "搜索目标对象...",
noTargetRoutines: "暂无目标对象",
unmatchedRoutineWarning: "还有 {count} 个源对象没有同名目标",
@@ -6596,6 +6605,9 @@ export default withEnglishFallback({
matchedTables: "已匹配 {matched}/{total} 张表",
missingTargetTables: "缺少同名的目标表:{tables}",
targetTableMatching: "目标表匹配",
sourceTable: "源表",
targetTable: "目标表",
similarity: "相似度",
selectTargetTable: "请选择目标表",
searchTargetTable: "搜索目标表...",
noTargetTables: "暂无目标表",
+12
View File
@@ -5405,6 +5405,14 @@ export default withEnglishFallback({
selectRecentConfig: "選擇最近配置",
configSaved: "配置已儲存",
configDeleted: "配置已刪除",
comparisonObjects: "比對物件",
searchObjects: "搜尋物件名稱...",
selectAllObjects: "全選",
deselectAllObjects: "取消全選",
objectPickerAll: "全部",
objectGroupDisabled: "此物件類型未納入本次比對。",
noObjects: "暫無物件",
noObjectMatches: "無符合項目",
tableSelection: "資料表",
chooseTables: "選擇資料表...",
compareAllTables: "比較所有資料表",
@@ -5425,6 +5433,7 @@ export default withEnglishFallback({
targetRoutineMatching: "目標例程對應",
matchedRoutines: "已匹配 {matched}/{total} 個例程",
selectTargetRoutine: "選擇目標例程",
searchSourceRoutines: "搜尋物件...",
searchTargetRoutine: "搜尋目標例程...",
noTargetRoutines: "沒有可用的目標例程",
unmatchedRoutineWarning: "有 {count} 個已選來源例程沒有同名目標",
@@ -5443,6 +5452,9 @@ export default withEnglishFallback({
matchedTables: "已比對 {matched}/{total} 張資料表",
missingTargetTables: "缺少同名的目標資料表:{tables}",
targetTableMatching: "目標資料表比對",
sourceTable: "來源資料表",
targetTable: "目標資料表",
similarity: "相似度",
selectTargetTable: "請選擇目標資料表",
searchTargetTable: "搜尋目標資料表...",
noTargetTables: "沒有可用的目標資料表",
@@ -46,6 +46,22 @@ describe("schema diff table mappings", () => {
expect(buildSchemaDiffTableMatches(["a"], ["a", "a_new"], [{ sourceTable: "a", targetTable: "a_new" }])).toEqual([{ sourceTable: "a", targetTable: "a_new", kind: "manual" }]);
});
it("treats reconciled same-name mappings as automatic", () => {
expect(
buildSchemaDiffTableMatches(
["a", "b"],
["a", "b"],
[
{ sourceTable: "a", targetTable: "a" },
{ sourceTable: "b", targetTable: "b" },
],
),
).toEqual([
{ sourceTable: "a", targetTable: "a", kind: "automatic" },
{ sourceTable: "b", targetTable: "b", kind: "automatic" },
]);
});
it("removes mappings for deselected sources and automatically matches new sources", () => {
const mappings = [
{ sourceTable: "a", targetTable: "a" },
@@ -90,13 +90,13 @@ export function reconcileSchemaDiffTableMappings(selectedTables: readonly string
export function buildSchemaDiffTableMatches(selectedTables: readonly string[], targetTables: readonly string[], mappings: readonly SchemaDiffTableMapping[], ignoreTableNameCase = false): SchemaDiffTableMatch[] {
const reconciled = reconcileSchemaDiffTableMappings(selectedTables, targetTables, mappings, ignoreTableNameCase);
const targetBySource = new Map(reconciled.map((mapping) => [mapping.sourceTable, mapping.targetTable]));
const explicitSources = new Set(firstMappingBySource(mappings).keys());
return selectedTables.map((sourceTable) => {
const targetTable = targetBySource.get(sourceTable);
let kind: SchemaDiffTableMatchKind;
if (!targetTable) kind = "unmatched";
else if (!explicitSources.has(sourceTable) && identifiersEqual(targetTable, sourceTable, ignoreTableNameCase)) kind = "automatic";
// Same-name (incl. ignore-case) stays automatic even after reconcile persists the pair in mappings.
else if (identifiersEqual(targetTable, sourceTable, ignoreTableNameCase)) kind = "automatic";
else kind = "manual";
return { sourceTable, targetTable, kind };
});