intermittent commit

This commit is contained in:
2026-07-14 13:22:10 +02:00
parent 3f0b14a726
commit 2e593b7fa4
31 changed files with 3523 additions and 1672 deletions

View File

@@ -4,7 +4,6 @@ type XlsxWorkbookSheet = {
data: unknown[][];
};
type XlsxWorkbookReader = (input: ArrayBuffer) => Promise<XlsxWorkbookSheet[]>;
type RawXlsxWorkbookReader = (input: unknown) => Promise<XlsxWorkbookSheet[]>;
export type ImportFieldDefinition = {
name: string;
@@ -16,7 +15,11 @@ export type ImportFieldDefinition = {
export type ImportAttachmentBasePath = {
id?: string;
name?: string;
path?: string;
source?: string;
allow_individual?: boolean;
unsent_warning?: boolean;
};
export type RecipientImportMode = "append" | "replace";
@@ -29,7 +32,7 @@ export type CsvParseOptions = {
quoted: boolean;
};
export type RecipientImportSourceType = "csv" | "xlsx" | "text";
export type RecipientImportSourceType = "csv" | "xlsx" | "text" | "addresses";
export type RecipientColumnKind = "ignore" | "id" | "active" | "name" | "from" | "to" | "cc" | "bcc" | "reply_to" | "field" | "new_field" | "attachment_pattern";
@@ -99,6 +102,10 @@ export type RecipientImportProvenance = {
imported_at: string;
mode: RecipientImportMode;
source_type: RecipientImportSourceType;
source_id?: string | null;
source_label?: string | null;
source_revision?: string | null;
source_provenance?: Record<string, unknown>;
filename?: string | null;
sheet_name?: string | null;
encoding?: string | null;
@@ -211,15 +218,6 @@ async function loadXlsxWorkbookReader(): Promise<XlsxWorkbookReader> {
const module = await import("read-excel-file/browser");
return module.default as XlsxWorkbookReader;
}
const globals = globalThis as typeof globalThis & {
Buffer?: { from(input: ArrayBuffer): unknown };
process?: { versions?: { node?: string } };
};
if (globals.process?.versions?.node && globals.Buffer?.from) {
const dynamicImport = new Function("specifier", "return import(specifier)") as (specifier: string) => Promise<{ default: RawXlsxWorkbookReader }>;
const module = await dynamicImport("read-excel-file/node");
return (input: ArrayBuffer) => module.default(globals.Buffer!.from(input));
}
const module = await import("read-excel-file/universal");
return module.default as XlsxWorkbookReader;
}
@@ -463,6 +461,37 @@ options: MaterializeImportOptions)
};
}
export function materializeRecipientImportWithAttachmentDefaults(
draft: Record<string, unknown>,
preview: RecipientImportPreview,
options: MaterializeImportOptions)
: Record<string, unknown> {
const needsAttachmentSource = importedRowsNeedAttachmentSource(preview);
const currentAttachments = asRecord(draft.attachments);
let nextBasePaths = normalizeImportAttachmentBasePaths(currentAttachments.base_paths, currentAttachments, true);
let attachmentBasePath: ImportAttachmentBasePath | null = null;
if (needsAttachmentSource) {
if (nextBasePaths.length === 0) {
nextBasePaths = [{ id: "base-path-campaign", name: "Campaign files", path: ".", allow_individual: true, unsent_warning: false }];
}
const selectedIndex = Math.max(0, nextBasePaths.findIndex((basePath) => basePath.allow_individual));
nextBasePaths = nextBasePaths.map((basePath, index) => index === selectedIndex ? { ...basePath, allow_individual: true } : basePath);
attachmentBasePath = options.attachmentBasePath ?? nextBasePaths[selectedIndex] ?? null;
}
const importedDraft = materializeRecipientImport(draft, preview, { ...options, attachmentBasePath });
if (!needsAttachmentSource) return importedDraft;
return {
...importedDraft,
attachments: {
...asRecord(importedDraft.attachments),
base_paths: nextBasePaths,
base_path: nextBasePaths[0]?.path || "."
}
};
}
export function createRecipientImportProvenance(input: {
preview: RecipientImportPreview;
mappings: RecipientColumnMapping[];
@@ -513,6 +542,27 @@ export function importedRowsNeedAttachmentSource(preview: RecipientImportPreview
return preview.rows.some((row) => row.issues.length === 0 && row.patterns.length > 0);
}
function normalizeImportAttachmentBasePaths(value: unknown, attachments: ImportRecord, fallbackAllowIndividual = false): ImportAttachmentBasePath[] {
if (Array.isArray(value) && value.length > 0) {
return value.filter(isRecord).map((basePath, index) => ({
id: getText(basePath, "id", `base-path-${index + 1}`),
name: getText(basePath, "name", `Base path ${index + 1}`),
source: getText(basePath, "source"),
path: getText(basePath, "path", index === 0 ? getText(attachments, "base_path", ".") : "."),
allow_individual: getBool(basePath, "allow_individual"),
unsent_warning: getBool(basePath, "unsent_warning")
}));
}
return [{
id: "base-path-campaign",
name: "Campaign files",
path: getText(attachments, "base_path", "."),
allow_individual: getBool(attachments, "allow_individual", fallbackAllowIndividual),
unsent_warning: false
}];
}
function importedRowToEntry(row: ImportedRecipientRow, attachmentBasePath?: ImportAttachmentBasePath | null): Record<string, unknown> {
return {
id: row.id,
@@ -815,6 +865,14 @@ function getText(record: ImportRecord, key: string, fallback = ""): string {
return fallback;
}
function getBool(record: ImportRecord, key: string, fallback = false): boolean {
const value = record[key];
if (typeof value === "boolean") return value;
if (typeof value === "string") return ["1", "true", "yes", "on"].includes(value.toLowerCase());
if (typeof value === "number") return value !== 0;
return fallback;
}
function humanizeFieldName(value: string): string {
return value.replace(/[_-]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
}