145 lines
5.9 KiB
TypeScript
145 lines
5.9 KiB
TypeScript
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<Required<FilesFileExplorerUiCapability>, "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<ImportFileLinkResolution> {
|
|
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<string, FilesManagedFile>();
|
|
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<number> {
|
|
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<string, RenderedImportPattern>();
|
|
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<string, string> {
|
|
const values = new Map<string, string>();
|
|
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, "");
|
|
} |