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:
parent
981291f048
commit
ac295d9048
6 changed files with 82 additions and 7 deletions
|
|
@ -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) => (
|
||||||
|
|
|
||||||
|
|
@ -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 }),
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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)]">
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue