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="DD-MM-YYYY">DD-MM-YYYY</option>
|
||||
<option value="DD.MM.YYYY">DD.MM.YYYY</option>
|
||||
<option value="YYYYMMDD">YYYYMMDD</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -244,6 +244,11 @@ export function useImportWizard() {
|
|||
const existing = await getSourceByName(source.folder_name);
|
||||
dispatch({ type: "SET_EXISTING_SOURCE", payload: existing });
|
||||
|
||||
let activeDelimiter = defaultConfig.delimiter;
|
||||
let activeEncoding = "utf-8";
|
||||
let activeSkipLines = 0;
|
||||
const activeHasHeader = true;
|
||||
|
||||
if (existing) {
|
||||
// Restore config from DB
|
||||
const mapping = JSON.parse(existing.column_mapping) as ColumnMapping;
|
||||
|
|
@ -260,12 +265,14 @@ export function useImportWizard() {
|
|||
hasHeader: true,
|
||||
};
|
||||
dispatch({ type: "SET_SOURCE_CONFIG", payload: config });
|
||||
activeDelimiter = existing.delimiter;
|
||||
activeEncoding = existing.encoding;
|
||||
activeSkipLines = existing.skip_lines;
|
||||
} else {
|
||||
// Auto-detect encoding for first file
|
||||
let encoding = "utf-8";
|
||||
if (source.files.length > 0) {
|
||||
try {
|
||||
encoding = await invoke<string>("detect_encoding", {
|
||||
activeEncoding = await invoke<string>("detect_encoding", {
|
||||
filePath: source.files[0].file_path,
|
||||
});
|
||||
} catch {
|
||||
|
|
@ -278,14 +285,20 @@ export function useImportWizard() {
|
|||
payload: {
|
||||
...defaultConfig,
|
||||
name: source.folder_name,
|
||||
encoding,
|
||||
encoding: activeEncoding,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Load preview headers from first file
|
||||
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" });
|
||||
|
|
@ -293,36 +306,60 @@ export function useImportWizard() {
|
|||
[] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
|
||||
const loadHeaders = async (
|
||||
filePath: string,
|
||||
existing: ImportSource | null
|
||||
) => {
|
||||
try {
|
||||
const encoding = existing?.encoding || "utf-8";
|
||||
const preview = await invoke<string>("get_file_preview", {
|
||||
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 loadHeadersWithConfig = useCallback(
|
||||
async (filePath: string, delimiter: string, encoding: string, skipLines: number, hasHeader: boolean) => {
|
||||
try {
|
||||
const preview = await invoke<string>("get_file_preview", {
|
||||
filePath,
|
||||
encoding,
|
||||
maxLines: skipLines + 5,
|
||||
});
|
||||
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) => {
|
||||
dispatch({ type: "SET_SOURCE_CONFIG", payload: config });
|
||||
}, []);
|
||||
const updateConfig = useCallback(
|
||||
(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(
|
||||
(file: ScannedFile) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* 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 {
|
||||
if (!raw || typeof raw !== "string") return "";
|
||||
|
|
@ -8,6 +8,23 @@ export function parseDate(raw: string, format: string): string {
|
|||
const cleaned = raw.trim();
|
||||
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
|
||||
const parts = cleaned.split(/[/\-\.]/);
|
||||
if (parts.length !== 3) return "";
|
||||
|
|
|
|||
Loading…
Reference in a new issue