diff --git a/apps/desktop/src/components/diff/SchemaDiffConfigStep.vue b/apps/desktop/src/components/diff/SchemaDiffConfigStep.vue index e027de45fd..df1565c0ef 100644 --- a/apps/desktop/src/components/diff/SchemaDiffConfigStep.vue +++ b/apps/desktop/src/components/diff/SchemaDiffConfigStep.vue @@ -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 {{ t("diff.sourceTables") }} {{ t("diff.targetTable") }} {{ t("diff.status") }} + {{ t("common.actions") }} @@ -881,9 +896,14 @@ async function fetchDbVersion(connectionId: string, database: string, schema: st content-class="w-[var(--reka-popover-trigger-width)]" /> - + {{ t(`diff.tableMatchStatus.${match.kind}`) }} + + + @@ -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')" /> @@ -946,6 +967,7 @@ async function fetchDbVersion(connectionId: string, database: string, schema: st {{ t("diff.sourceRoutines") }} {{ t("diff.targetRoutine") }} {{ t("diff.status") }} + {{ t("common.actions") }} @@ -955,9 +977,14 @@ async function fetchDbVersion(connectionId: string, database: string, schema: st {{ match.targetRoutine }} — - + {{ t(`diff.tableMatchStatus.${match.kind}`) }} + + + diff --git a/apps/desktop/src/components/diff/SchemaDiffDialog.vue b/apps/desktop/src/components/diff/SchemaDiffDialog.vue index 38605675e5..96d7cd5443 100644 --- a/apps/desktop/src/components/diff/SchemaDiffDialog.vue +++ b/apps/desktop/src/components/diff/SchemaDiffDialog.vue @@ -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(() => { + +
+
+
+

{{ t("diff.loadConfig") }}

+ +
+ +
+
+ (), { title: "", + searchPlaceholder: "", searchable: true, emptyText: "", disabled: false, @@ -64,7 +67,7 @@ function toggleSelectAll() { - +