feat(schema-diff): named config selector and mapping UX
This commit is contained in:
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">
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "利用可能なターゲットテーブルがありません",
|
||||
|
||||
@@ -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}개에 같은 이름의 대상이 없습니다",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "暂无目标表",
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user