feat: persist template selection and add update template button

The template dropdown now stays on the selected value after applying,
and a new update button lets users save config changes back to the
selected template.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Le-King-Fu 2026-02-15 13:16:55 +00:00
parent 981291f048
commit ac295d9048
6 changed files with 82 additions and 7 deletions

View file

@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Wand2, Check, Save, X } from "lucide-react"; import { Wand2, Check, Save, RefreshCw, X } from "lucide-react";
import type { import type {
ScannedSource, ScannedSource,
ScannedFile, ScannedFile,
@ -24,7 +24,9 @@ interface SourceConfigPanelProps {
onAutoDetect: () => void; onAutoDetect: () => void;
onSaveAsTemplate: (name: string) => void; onSaveAsTemplate: (name: string) => void;
onApplyTemplate: (id: number) => void; onApplyTemplate: (id: number) => void;
onUpdateTemplate: () => void;
onDeleteTemplate: (id: number) => void; onDeleteTemplate: (id: number) => void;
selectedTemplateId: number | null;
isLoading?: boolean; isLoading?: boolean;
} }
@ -41,7 +43,9 @@ export default function SourceConfigPanel({
onAutoDetect, onAutoDetect,
onSaveAsTemplate, onSaveAsTemplate,
onApplyTemplate, onApplyTemplate,
onUpdateTemplate,
onDeleteTemplate, onDeleteTemplate,
selectedTemplateId,
isLoading, isLoading,
}: SourceConfigPanelProps) { }: SourceConfigPanelProps) {
const { t } = useTranslation(); const { t } = useTranslation();
@ -79,7 +83,7 @@ export default function SourceConfigPanel({
{t("import.config.loadTemplate")} {t("import.config.loadTemplate")}
</label> </label>
<select <select
value="" value={selectedTemplateId ?? ""}
onChange={(e) => { onChange={(e) => {
if (e.target.value) onApplyTemplate(Number(e.target.value)); if (e.target.value) onApplyTemplate(Number(e.target.value));
}} }}
@ -96,6 +100,15 @@ export default function SourceConfigPanel({
</option> </option>
))} ))}
</select> </select>
{selectedTemplateId && (
<button
onClick={onUpdateTemplate}
title={t("import.config.updateTemplate")}
className="p-1.5 rounded-lg text-[var(--primary)] hover:bg-[var(--muted)] transition-colors"
>
<RefreshCw size={16} />
</button>
)}
{configTemplates.length > 0 && ( {configTemplates.length > 0 && (
<div className="flex gap-1"> <div className="flex gap-1">
{configTemplates.map((tpl) => ( {configTemplates.map((tpl) => (

View file

@ -37,6 +37,7 @@ import { categorizeBatch } from "../services/categorizationService";
import { import {
getAllTemplates, getAllTemplates,
createTemplate, createTemplate,
updateTemplate,
deleteTemplate as deleteTemplateService, deleteTemplate as deleteTemplateService,
} from "../services/importConfigTemplateService"; } from "../services/importConfigTemplateService";
import { parseDate } from "../utils/dateParser"; import { parseDate } from "../utils/dateParser";
@ -65,6 +66,7 @@ interface WizardState {
configuredSourceNames: Set<string>; configuredSourceNames: Set<string>;
importedFilesBySource: Map<string, Set<string>>; importedFilesBySource: Map<string, Set<string>>;
configTemplates: ImportConfigTemplate[]; configTemplates: ImportConfigTemplate[];
selectedTemplateId: number | null;
} }
type WizardAction = type WizardAction =
@ -85,6 +87,7 @@ type WizardAction =
| { type: "SET_IMPORT_PROGRESS"; payload: { current: number; total: number; file: string } } | { type: "SET_IMPORT_PROGRESS"; payload: { current: number; total: number; file: string } }
| { type: "SET_CONFIGURED_SOURCES"; payload: { names: Set<string>; files: Map<string, Set<string>> } } | { type: "SET_CONFIGURED_SOURCES"; payload: { names: Set<string>; files: Map<string, Set<string>> } }
| { type: "SET_CONFIG_TEMPLATES"; payload: ImportConfigTemplate[] } | { type: "SET_CONFIG_TEMPLATES"; payload: ImportConfigTemplate[] }
| { type: "SET_SELECTED_TEMPLATE_ID"; payload: number | null }
| { type: "RESET" }; | { type: "RESET" };
const defaultConfig: SourceConfig = { const defaultConfig: SourceConfig = {
@ -118,6 +121,7 @@ const initialState: WizardState = {
configuredSourceNames: new Set(), configuredSourceNames: new Set(),
importedFilesBySource: new Map(), importedFilesBySource: new Map(),
configTemplates: [], configTemplates: [],
selectedTemplateId: null,
}; };
function reducer(state: WizardState, action: WizardAction): WizardState { function reducer(state: WizardState, action: WizardAction): WizardState {
@ -182,6 +186,8 @@ function reducer(state: WizardState, action: WizardAction): WizardState {
}; };
case "SET_CONFIG_TEMPLATES": case "SET_CONFIG_TEMPLATES":
return { ...state, configTemplates: action.payload }; return { ...state, configTemplates: action.payload };
case "SET_SELECTED_TEMPLATE_ID":
return { ...state, selectedTemplateId: action.payload };
case "RESET": case "RESET":
return { return {
...initialState, ...initialState,
@ -291,6 +297,7 @@ export function useImportWizard() {
dispatch({ type: "SET_SELECTED_SOURCE", payload: sortedSource }); dispatch({ type: "SET_SELECTED_SOURCE", payload: sortedSource });
dispatch({ type: "SET_SELECTED_FILES", payload: newFiles }); dispatch({ type: "SET_SELECTED_FILES", payload: newFiles });
dispatch({ type: "SET_SELECTED_TEMPLATE_ID", payload: null });
// Check if this source already has config in DB // Check if this source already has config in DB
const existing = await getSourceByName(source.folder_name); const existing = await getSourceByName(source.folder_name);
@ -945,6 +952,7 @@ export function useImportWizard() {
hasHeader: !!template.has_header, hasHeader: !!template.has_header,
}; };
dispatch({ type: "SET_SOURCE_CONFIG", payload: newConfig }); dispatch({ type: "SET_SOURCE_CONFIG", payload: newConfig });
dispatch({ type: "SET_SELECTED_TEMPLATE_ID", payload: templateId });
// Reload headers with new config // Reload headers with new config
if (state.selectedFiles.length > 0) { if (state.selectedFiles.length > 0) {
@ -958,11 +966,34 @@ export function useImportWizard() {
} }
}, [state.configTemplates, state.sourceConfig.name, state.selectedFiles, loadHeadersWithConfig]); }, [state.configTemplates, state.sourceConfig.name, state.selectedFiles, loadHeadersWithConfig]);
const deleteConfigTemplate = useCallback(async (id: number) => { const updateConfigTemplate = useCallback(async () => {
await deleteTemplateService(id); if (!state.selectedTemplateId) return;
const template = state.configTemplates.find((t) => t.id === state.selectedTemplateId);
if (!template) return;
const config = state.sourceConfig;
await updateTemplate(state.selectedTemplateId, {
name: template.name,
delimiter: config.delimiter,
encoding: config.encoding,
date_format: config.dateFormat,
skip_lines: config.skipLines,
has_header: config.hasHeader ? 1 : 0,
column_mapping: JSON.stringify(config.columnMapping),
amount_mode: config.amountMode,
sign_convention: config.signConvention,
});
const templates = await getAllTemplates(); const templates = await getAllTemplates();
dispatch({ type: "SET_CONFIG_TEMPLATES", payload: templates }); dispatch({ type: "SET_CONFIG_TEMPLATES", payload: templates });
}, []); }, [state.selectedTemplateId, state.configTemplates, state.sourceConfig]);
const deleteConfigTemplate = useCallback(async (id: number) => {
await deleteTemplateService(id);
if (state.selectedTemplateId === id) {
dispatch({ type: "SET_SELECTED_TEMPLATE_ID", payload: null });
}
const templates = await getAllTemplates();
dispatch({ type: "SET_CONFIG_TEMPLATES", payload: templates });
}, [state.selectedTemplateId]);
return { return {
state, state,
@ -981,6 +1012,7 @@ export function useImportWizard() {
autoDetectConfig, autoDetectConfig,
saveConfigAsTemplate, saveConfigAsTemplate,
applyConfigTemplate, applyConfigTemplate,
updateConfigTemplate,
deleteConfigTemplate, deleteConfigTemplate,
toggleDuplicateRow: (index: number) => toggleDuplicateRow: (index: number) =>
dispatch({ type: "TOGGLE_DUPLICATE_ROW", payload: index }), dispatch({ type: "TOGGLE_DUPLICATE_ROW", payload: index }),

View file

@ -90,7 +90,8 @@
"templateName": "Template name", "templateName": "Template name",
"templateSaved": "Template saved", "templateSaved": "Template saved",
"deleteTemplate": "Delete template", "deleteTemplate": "Delete template",
"noTemplates": "No templates saved" "noTemplates": "No templates saved",
"updateTemplate": "Update template"
}, },
"preview": { "preview": {
"title": "Data Preview", "title": "Data Preview",

View file

@ -90,7 +90,8 @@
"templateName": "Nom du modèle", "templateName": "Nom du modèle",
"templateSaved": "Modèle sauvegardé", "templateSaved": "Modèle sauvegardé",
"deleteTemplate": "Supprimer le modèle", "deleteTemplate": "Supprimer le modèle",
"noTemplates": "Aucun modèle sauvegardé" "noTemplates": "Aucun modèle sauvegardé",
"updateTemplate": "Mettre à jour le modèle"
}, },
"preview": { "preview": {
"title": "Aperçu des données", "title": "Aperçu des données",

View file

@ -32,6 +32,7 @@ export default function ImportPage() {
autoDetectConfig, autoDetectConfig,
saveConfigAsTemplate, saveConfigAsTemplate,
applyConfigTemplate, applyConfigTemplate,
updateConfigTemplate,
deleteConfigTemplate, deleteConfigTemplate,
toggleDuplicateRow, toggleDuplicateRow,
setSkipAllDuplicates, setSkipAllDuplicates,
@ -99,7 +100,9 @@ export default function ImportPage() {
onAutoDetect={autoDetectConfig} onAutoDetect={autoDetectConfig}
onSaveAsTemplate={saveConfigAsTemplate} onSaveAsTemplate={saveConfigAsTemplate}
onApplyTemplate={applyConfigTemplate} onApplyTemplate={applyConfigTemplate}
onUpdateTemplate={updateConfigTemplate}
onDeleteTemplate={deleteConfigTemplate} onDeleteTemplate={deleteConfigTemplate}
selectedTemplateId={state.selectedTemplateId}
isLoading={state.isLoading} isLoading={state.isLoading}
/> />
<div className="flex items-center justify-between pt-6 border-t border-[var(--border)]"> <div className="flex items-center justify-between pt-6 border-t border-[var(--border)]">

View file

@ -30,6 +30,31 @@ export async function createTemplate(
return result.lastInsertId as number; return result.lastInsertId as number;
} }
export async function updateTemplate(
id: number,
template: Omit<ImportConfigTemplate, "id" | "created_at">
): Promise<void> {
const db = await getDb();
await db.execute(
`UPDATE import_config_templates
SET name=$1, delimiter=$2, encoding=$3, date_format=$4, skip_lines=$5,
has_header=$6, column_mapping=$7, amount_mode=$8, sign_convention=$9
WHERE id=$10`,
[
template.name,
template.delimiter,
template.encoding,
template.date_format,
template.skip_lines,
template.has_header,
template.column_mapping,
template.amount_mode,
template.sign_convention,
id,
]
);
}
export async function deleteTemplate(id: number): Promise<void> { export async function deleteTemplate(id: number): Promise<void> {
const db = await getDb(); const db = await getDb();
await db.execute("DELETE FROM import_config_templates WHERE id = $1", [id]); await db.execute("DELETE FROM import_config_templates WHERE id = $1", [id]);