fix: reload column headers on config change and add YYYYMMDD date format
Headers now refresh live when delimiter, encoding, skipLines, or hasHeader changes. Added YYYYMMDD compact date format to parser and dropdown. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ce584a15ab
commit
88219e657f
3 changed files with 87 additions and 32 deletions
|
|
@ -104,6 +104,7 @@ export default function SourceConfigPanel({
|
||||||
<option value="YYYY-MM-DD">YYYY-MM-DD</option>
|
<option value="YYYY-MM-DD">YYYY-MM-DD</option>
|
||||||
<option value="DD-MM-YYYY">DD-MM-YYYY</option>
|
<option value="DD-MM-YYYY">DD-MM-YYYY</option>
|
||||||
<option value="DD.MM.YYYY">DD.MM.YYYY</option>
|
<option value="DD.MM.YYYY">DD.MM.YYYY</option>
|
||||||
|
<option value="YYYYMMDD">YYYYMMDD</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,11 @@ export function useImportWizard() {
|
||||||
const existing = await getSourceByName(source.folder_name);
|
const existing = await getSourceByName(source.folder_name);
|
||||||
dispatch({ type: "SET_EXISTING_SOURCE", payload: existing });
|
dispatch({ type: "SET_EXISTING_SOURCE", payload: existing });
|
||||||
|
|
||||||
|
let activeDelimiter = defaultConfig.delimiter;
|
||||||
|
let activeEncoding = "utf-8";
|
||||||
|
let activeSkipLines = 0;
|
||||||
|
const activeHasHeader = true;
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
// Restore config from DB
|
// Restore config from DB
|
||||||
const mapping = JSON.parse(existing.column_mapping) as ColumnMapping;
|
const mapping = JSON.parse(existing.column_mapping) as ColumnMapping;
|
||||||
|
|
@ -260,12 +265,14 @@ export function useImportWizard() {
|
||||||
hasHeader: true,
|
hasHeader: true,
|
||||||
};
|
};
|
||||||
dispatch({ type: "SET_SOURCE_CONFIG", payload: config });
|
dispatch({ type: "SET_SOURCE_CONFIG", payload: config });
|
||||||
|
activeDelimiter = existing.delimiter;
|
||||||
|
activeEncoding = existing.encoding;
|
||||||
|
activeSkipLines = existing.skip_lines;
|
||||||
} else {
|
} else {
|
||||||
// Auto-detect encoding for first file
|
// Auto-detect encoding for first file
|
||||||
let encoding = "utf-8";
|
|
||||||
if (source.files.length > 0) {
|
if (source.files.length > 0) {
|
||||||
try {
|
try {
|
||||||
encoding = await invoke<string>("detect_encoding", {
|
activeEncoding = await invoke<string>("detect_encoding", {
|
||||||
filePath: source.files[0].file_path,
|
filePath: source.files[0].file_path,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -278,14 +285,20 @@ export function useImportWizard() {
|
||||||
payload: {
|
payload: {
|
||||||
...defaultConfig,
|
...defaultConfig,
|
||||||
name: source.folder_name,
|
name: source.folder_name,
|
||||||
encoding,
|
encoding: activeEncoding,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load preview headers from first file
|
// Load preview headers from first file
|
||||||
if (source.files.length > 0) {
|
if (source.files.length > 0) {
|
||||||
await loadHeaders(source.files[0].file_path, existing);
|
await loadHeadersWithConfig(
|
||||||
|
source.files[0].file_path,
|
||||||
|
activeDelimiter,
|
||||||
|
activeEncoding,
|
||||||
|
activeSkipLines,
|
||||||
|
activeHasHeader
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({ type: "SET_STEP", payload: "source-config" });
|
dispatch({ type: "SET_STEP", payload: "source-config" });
|
||||||
|
|
@ -293,36 +306,60 @@ export function useImportWizard() {
|
||||||
[] // eslint-disable-line react-hooks/exhaustive-deps
|
[] // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadHeaders = async (
|
const loadHeadersWithConfig = useCallback(
|
||||||
filePath: string,
|
async (filePath: string, delimiter: string, encoding: string, skipLines: number, hasHeader: boolean) => {
|
||||||
existing: ImportSource | null
|
try {
|
||||||
) => {
|
const preview = await invoke<string>("get_file_preview", {
|
||||||
try {
|
filePath,
|
||||||
const encoding = existing?.encoding || "utf-8";
|
encoding,
|
||||||
const preview = await invoke<string>("get_file_preview", {
|
maxLines: skipLines + 5,
|
||||||
filePath,
|
|
||||||
encoding,
|
|
||||||
maxLines: 5,
|
|
||||||
});
|
|
||||||
const delimiter = existing?.delimiter || ";";
|
|
||||||
const parsed = Papa.parse(preview, { delimiter });
|
|
||||||
if (parsed.data.length > 0) {
|
|
||||||
dispatch({
|
|
||||||
type: "SET_PARSED_PREVIEW",
|
|
||||||
payload: {
|
|
||||||
rows: [],
|
|
||||||
headers: (parsed.data[0] as string[]).map((h) => h.trim()),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
const parsed = Papa.parse(preview, { delimiter, skipEmptyLines: true });
|
||||||
|
const data = parsed.data as string[][];
|
||||||
|
const headerRow = hasHeader && data.length > skipLines ? skipLines : -1;
|
||||||
|
if (headerRow >= 0 && data[headerRow]) {
|
||||||
|
dispatch({
|
||||||
|
type: "SET_PARSED_PREVIEW",
|
||||||
|
payload: {
|
||||||
|
rows: [],
|
||||||
|
headers: data[headerRow].map((h) => h.trim()),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (data.length > 0) {
|
||||||
|
// No header row — generate column indices as headers
|
||||||
|
const firstDataRow = data[skipLines] || data[0];
|
||||||
|
dispatch({
|
||||||
|
type: "SET_PARSED_PREVIEW",
|
||||||
|
payload: {
|
||||||
|
rows: [],
|
||||||
|
headers: firstDataRow.map((_, i) => `Col ${i}`),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore preview errors
|
||||||
}
|
}
|
||||||
} catch {
|
},
|
||||||
// ignore preview errors
|
[]
|
||||||
}
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const updateConfig = useCallback((config: SourceConfig) => {
|
const updateConfig = useCallback(
|
||||||
dispatch({ type: "SET_SOURCE_CONFIG", payload: config });
|
(config: SourceConfig) => {
|
||||||
}, []);
|
dispatch({ type: "SET_SOURCE_CONFIG", payload: config });
|
||||||
|
|
||||||
|
// Reload headers when delimiter, encoding, skipLines, or hasHeader changes
|
||||||
|
if (state.selectedFiles.length > 0) {
|
||||||
|
loadHeadersWithConfig(
|
||||||
|
state.selectedFiles[0].file_path,
|
||||||
|
config.delimiter,
|
||||||
|
config.encoding,
|
||||||
|
config.skipLines,
|
||||||
|
config.hasHeader
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[state.selectedFiles, loadHeadersWithConfig]
|
||||||
|
);
|
||||||
|
|
||||||
const toggleFile = useCallback(
|
const toggleFile = useCallback(
|
||||||
(file: ScannedFile) => {
|
(file: ScannedFile) => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* Parse a date string with a given format and return ISO YYYY-MM-DD.
|
* Parse a date string with a given format and return ISO YYYY-MM-DD.
|
||||||
* Supported formats: DD/MM/YYYY, MM/DD/YYYY, YYYY-MM-DD, DD-MM-YYYY, DD.MM.YYYY
|
* Supported formats: DD/MM/YYYY, MM/DD/YYYY, YYYY-MM-DD, DD-MM-YYYY, DD.MM.YYYY, YYYYMMDD
|
||||||
*/
|
*/
|
||||||
export function parseDate(raw: string, format: string): string {
|
export function parseDate(raw: string, format: string): string {
|
||||||
if (!raw || typeof raw !== "string") return "";
|
if (!raw || typeof raw !== "string") return "";
|
||||||
|
|
@ -8,6 +8,23 @@ export function parseDate(raw: string, format: string): string {
|
||||||
const cleaned = raw.trim();
|
const cleaned = raw.trim();
|
||||||
let day: string, month: string, year: string;
|
let day: string, month: string, year: string;
|
||||||
|
|
||||||
|
// Handle compact format YYYYMMDD (no separator)
|
||||||
|
if (format === "YYYYMMDD") {
|
||||||
|
const digits = cleaned.replace(/\D/g, "");
|
||||||
|
if (digits.length !== 8) return "";
|
||||||
|
year = digits.substring(0, 4);
|
||||||
|
month = digits.substring(4, 6);
|
||||||
|
day = digits.substring(6, 8);
|
||||||
|
|
||||||
|
const y = parseInt(year, 10);
|
||||||
|
const m = parseInt(month, 10);
|
||||||
|
const d = parseInt(day, 10);
|
||||||
|
if (isNaN(y) || isNaN(m) || isNaN(d)) return "";
|
||||||
|
if (m < 1 || m > 12 || d < 1 || d > 31) return "";
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Extract parts based on separator
|
// Extract parts based on separator
|
||||||
const parts = cleaned.split(/[/\-\.]/);
|
const parts = cleaned.split(/[/\-\.]/);
|
||||||
if (parts.length !== 3) return "";
|
if (parts.length !== 3) return "";
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue