import type { ApiSettings, FilesFileExplorerUiCapability, FilesManagedFile } from "@govoplan/core-webui"; import { parseManagedAttachmentSource, type AttachmentBasePath } from "./attachments"; import type { ImportedRecipientRow, RecipientImportPreview } from "./bulkImport"; export type RenderedImportPattern = { key: string; rowNumber: number; sourcePattern: string; renderedPattern: string; }; export type ImportFileLinkCapability = Pick, "listFiles" | "resolveFilePatterns" | "shareFilesWithTarget">; export type ImportFileLinkResolution = { basePath: AttachmentBasePath; patterns: RenderedImportPattern[]; matchedPatterns: {pattern: RenderedImportPattern;matches: FilesManagedFile[];}[]; unmatchedPatterns: RenderedImportPattern[]; files: FilesManagedFile[]; linkedFiles: FilesManagedFile[]; linkableFiles: FilesManagedFile[]; }; export async function resolveImportedAttachmentLinks( filesCapability: ImportFileLinkCapability, settings: ApiSettings, campaignId: string, basePath: AttachmentBasePath, preview: RecipientImportPreview) : Promise { const parsedSource = parseManagedAttachmentSource(basePath.source); if (!parsedSource) { throw new Error("i18n:govoplan-campaign.the_selected_attachment_source_is_not_connected_.965a0056"); } const patterns = renderedImportPatterns(preview); if (patterns.length === 0) { return { basePath, patterns, matchedPatterns: [], unmatchedPatterns: [], files: [], linkedFiles: [], linkableFiles: [] }; } const root = normalizedPathPrefix(basePath.path); const [resolved, visibleFiles] = await Promise.all([ filesCapability.resolveFilePatterns(settings, { patterns: patterns.map((item) => item.renderedPattern), owner_type: parsedSource.ownerType, owner_id: parsedSource.ownerId, path_prefix: root || undefined, include_unmatched: false }), filesCapability.listFiles(settings, { owner_type: parsedSource.ownerType, owner_id: parsedSource.ownerId, path_prefix: root || undefined })] ); const visibleById = new Map(visibleFiles.files.map((file) => [file.id, file])); const fileById = new Map(); const matchedPatterns = patterns.map((pattern, index) => { const matches = (resolved.patterns[index]?.matches ?? []). map((file) => visibleById.get(file.id) ?? file); matches.forEach((file) => fileById.set(file.id, file)); return { pattern, matches }; }); const files = [...fileById.values()].sort((left, right) => left.display_path.localeCompare(right.display_path)); const linkedFiles = files.filter((file) => isSharedWithCampaign(file, campaignId)); const linkableFiles = files.filter((file) => !isSharedWithCampaign(file, campaignId)); return { basePath, patterns, matchedPatterns, unmatchedPatterns: matchedPatterns.filter((item) => item.matches.length === 0).map((item) => item.pattern), files, linkedFiles, linkableFiles }; } export async function bulkLinkFilesToCampaign(filesCapability: ImportFileLinkCapability, settings: ApiSettings, campaignId: string, files: FilesManagedFile[]): Promise { const uniqueIds = [...new Set(files.map((file) => file.id).filter(Boolean))]; if (uniqueIds.length === 0) return 0; const response = await filesCapability.shareFilesWithTarget(settings, uniqueIds, { type: "campaign", id: campaignId, label: "campaign" }); return response.shared_count; } export function renderedImportPatterns(preview: RecipientImportPreview): RenderedImportPattern[] { const byKey = new Map(); preview.rows. filter((row) => row.issues.length === 0). forEach((row) => { row.patterns.forEach((sourcePattern) => { const renderedPattern = renderPatternForImportedRow(sourcePattern, row).trim(); if (!renderedPattern) return; const key = `${row.rowNumber}:${sourcePattern}:${renderedPattern}`; if (!byKey.has(key)) byKey.set(key, { key, rowNumber: row.rowNumber, sourcePattern, renderedPattern }); }); }); return [...byKey.values()]; } export function isSharedWithCampaign(file: FilesManagedFile, campaignId: string): boolean { return Boolean(file.shares?.some((share) => share.target_type === "campaign" && share.target_id === campaignId && !share.revoked_at)); } function renderPatternForImportedRow(pattern: string, row: ImportedRecipientRow): string { const values = valuesForImportedRow(row); const replace = (rawKey: string, original: string) => { const key = normalizeTemplateKey(rawKey); const value = values.get(key); return value === undefined ? original : value; }; const dollarRendered = pattern.replace(/\\?\$\{([^}]+)\}/g, (match, key) => match.startsWith("\\") ? match.slice(1) : replace(key, match)); return dollarRendered.replace(/\\?\{\{\s*([^}]+?)\s*\}\}/g, (match, key) => match.startsWith("\\") ? match.slice(1) : replace(key, match)); } function valuesForImportedRow(row: ImportedRecipientRow): Map { const values = new Map(); const setValue = (key: string, value: string) => { const trimmedKey = key.trim(); if (!trimmedKey) return; values.set(trimmedKey, value); values.set(`local:${trimmedKey}`, value); values.set(`local::${trimmedKey}`, value); }; setValue("id", row.id); setValue("name", row.name); setValue("email", row.email); Object.entries(row.fields).forEach(([key, value]) => setValue(key, String(value ?? ""))); return values; } function normalizeTemplateKey(value: string): string { const key = value.trim(); if (key.startsWith("local::") || key.startsWith("global::")) return key; if (key.startsWith("local:")) return `local::${key.slice("local:".length)}`; if (key.startsWith("global:")) return `global::${key.slice("global:".length)}`; return key; } function normalizedPathPrefix(path: string): string { const trimmed = path.trim(); if (!trimmed || trimmed === "." || trimmed === "/") return ""; return trimmed.replace(/^[\/]+|[\/]+$/g, ""); }