import { asArray, asRecord, isRecord } from "./campaignView"; import { getBool, getText } from "./draftEditor"; export type AttachmentRule = Record; export type AttachmentZipPasswordScope = "local" | "global"; export type AttachmentZipArchive = { id: string; name: string; standard: boolean; password_enabled: boolean; password_field: string; password_scope: AttachmentZipPasswordScope; method: "aes" | "zip_standard"; // Read-only compatibility values retained when normalizing older campaigns. password_mode?: "none" | "direct" | "field" | "template"; password?: string; password_template?: string; }; export type AttachmentZipCollection = { enabled: boolean; archives: AttachmentZipArchive[]; }; export function createAttachmentZipArchive(name = "attachments.zip", standard = false): AttachmentZipArchive { return { id: `zip-${Date.now()}-${Math.random().toString(36).slice(2)}`, name, standard, password_enabled: false, password_field: "", password_scope: "local", method: "aes" }; } export function normalizeAttachmentZipCollection(value: unknown): AttachmentZipCollection { const zip = asRecord(value); const rawArchives = asArray(zip.archives).filter(isRecord); if (rawArchives.length > 0 || Object.prototype.hasOwnProperty.call(zip, "archives")) { return { enabled: getBool(zip, "enabled"), archives: rawArchives.map((archive, index) => { const legacyMode = getText(archive, "password_mode") as AttachmentZipArchive["password_mode"]; return { id: getText(archive, "id", `zip-${index + 1}`), name: getText(archive, "name", getText(archive, "filename_template", "attachments.zip")), standard: getBool(archive, "standard"), password_enabled: getBool(archive, "password_enabled", ["direct", "field", "template"].includes(legacyMode || "")), password_field: getText(archive, "password_field"), password_scope: getText(archive, "password_scope") === "global" ? "global" : "local", method: getText(archive, "method", "aes") === "zip_standard" ? "zip_standard" : "aes", ...(legacyMode ? { password_mode: legacyMode } : {}), ...(getText(archive, "password") ? { password: getText(archive, "password") } : {}), ...(getText(archive, "password_template") ? { password_template: getText(archive, "password_template") } : {}) }; }) }; } // Upgrade the first, single-archive implementation in memory. const legacyEnabled = getBool(zip, "enabled"); const legacyName = getText(zip, "filename_template"); const legacyMode = getText(zip, "password_mode", getText(zip, "password_template") ? "template" : "none") as AttachmentZipArchive["password_mode"]; const meaningful = legacyEnabled || Boolean(legacyName || getText(zip, "password") || getText(zip, "password_field") || getText(zip, "password_template")); if (!meaningful) return { enabled: false, archives: [] }; return { enabled: legacyEnabled, archives: [{ id: "default", name: legacyName || "attachments.zip", standard: true, password_enabled: ["direct", "field", "template"].includes(legacyMode || ""), password_field: getText(zip, "password_field"), password_scope: "local", method: getText(zip, "method", "aes") === "zip_standard" ? "zip_standard" : "aes", ...(legacyMode ? { password_mode: legacyMode } : {}), ...(getText(zip, "password") ? { password: getText(zip, "password") } : {}), ...(getText(zip, "password_template") ? { password_template: getText(zip, "password_template") } : {}) }] }; } export function attachmentRuleZipSelection(rule: AttachmentRule): string { const zip = asRecord(rule.zip); const archiveId = getText(zip, "archive_id"); if (archiveId) return archiveId; const legacyMode = getText(zip, "mode"); if (legacyMode === "exclude") return "exclude"; return "inherit"; } export type ManagedAttachmentSource = { ownerType: "user" | "group"; ownerId: string; }; type ManagedAttachmentSourceSpace = { owner_type: "user" | "group"; owner_id: string; }; const MANAGED_ATTACHMENT_SOURCE_PREFIX = "managed:"; export function encodeManagedAttachmentSource(space: ManagedAttachmentSourceSpace): string { return `${MANAGED_ATTACHMENT_SOURCE_PREFIX}${space.owner_type}:${space.owner_id}`; } export function parseManagedAttachmentSource(value: unknown): ManagedAttachmentSource | null { if (typeof value !== "string" || !value.startsWith(MANAGED_ATTACHMENT_SOURCE_PREFIX)) return null; const [, ownerType, ...ownerIdParts] = value.split(":"); const ownerId = ownerIdParts.join(":").trim(); if (ownerType !== "user" && ownerType !== "group" || !ownerId) return null; return { ownerType, ownerId }; } export type AttachmentBasePath = { id: string; name: string; path: string; source?: string; allow_individual?: boolean; unsent_warning?: boolean; }; export type AttachmentSummary = { direct: number; rules: number; }; export type MockAttachmentPathOption = Partial & { label: string; }; export const mockAttachmentPathOptions: MockAttachmentPathOption[] = [ { label: "i18n:govoplan-campaign.campaign_attachments.926bcbe6", path: "attachments" }, { label: "i18n:govoplan-campaign.campaign_root.7eccd949", path: "." }, { label: "i18n:govoplan-campaign.shared_group_files.fffa6e27", path: "group/shared" }, { label: "i18n:govoplan-campaign.tenant_templates.ac6653bf", path: "tenant/templates" }, { label: "i18n:govoplan-campaign.personal_upload_area.babd7b7f", path: "user/uploads" }]; export function createAttachmentBasePath(name = "New attachment source", path = "."): AttachmentBasePath { return { id: `base-path-${Date.now()}-${Math.random().toString(36).slice(2)}`, name, path, allow_individual: false, unsent_warning: false }; } export function createAttachmentRule(baseDir = "", label = "", basePathId = ""): AttachmentRule { return { id: `attachment-${Date.now()}-${Math.random().toString(36).slice(2)}`, label, base_path_id: basePathId || undefined, base_dir: baseDir, file_filter: "", required: true, include_subdirs: false, zip: { archive_id: "inherit" } }; } export function normalizeAttachmentBasePaths(value: unknown, attachments: Record, fallbackAllowIndividual = false): AttachmentBasePath[] { 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 }]; } export function ensureAttachmentBasePaths(paths: AttachmentBasePath[]): AttachmentBasePath[] { return paths.length > 0 ? paths : [createAttachmentBasePath("Campaign files", ".")]; } export function getIndividualAttachmentBasePaths(paths: AttachmentBasePath[]): AttachmentBasePath[] { return paths.filter((basePath) => basePath.allow_individual); } export function attachmentRuleUsesBasePath(rule: AttachmentRule, basePath: AttachmentBasePath): boolean { const basePathId = getText(rule, "base_path_id"); if (basePathId) return basePathId === basePath.id; return getText(rule, "base_dir") === basePath.path; } export function countIndividualAttachmentRulesForBasePath(entriesValue: unknown, basePath: AttachmentBasePath): number { const entries = asRecord(entriesValue); return asArray(entries.inline). map(asRecord). flatMap((entry) => normalizeAttachmentRules(entry.attachments)). filter((rule) => attachmentRuleUsesBasePath(rule, basePath)). length; } export function removeIndividualAttachmentRulesForBasePath(entriesValue: unknown, basePath: AttachmentBasePath): Record { const entries = asRecord(entriesValue); const inline = asArray(entries.inline).map(asRecord).map((entry) => ({ ...entry, attachments: normalizeAttachmentRules(entry.attachments).filter((rule) => !attachmentRuleUsesBasePath(rule, basePath)) })); return { ...entries, inline }; } export function nextAttachmentLabel(rules: AttachmentRule[]): string { let highest = 0; for (const rule of rules) { const match = /^Attachment\s+(\d+)$/i.exec(getText(rule, "label").trim()); if (match) highest = Math.max(highest, Number(match[1])); } highest = Math.max(highest, rules.length); return `Attachment ${highest + 1}`; } export function normalizeAttachmentRules(value: unknown): AttachmentRule[] { if (!Array.isArray(value)) return []; return value.filter(isRecord).map((rule) => ({ id: getText(rule, "id", `attachment-${Math.random().toString(36).slice(2)}`), label: getText(rule, "label"), base_path_id: getText(rule, "base_path_id"), base_dir: getText(rule, "base_dir", ""), file_filter: getText(rule, "file_filter") || getText(rule, "file") || getText(rule, "filename") || getText(rule, "path"), include_subdirs: getBool(rule, "include_subdirs"), required: getBool(rule, "required", true), missing_behavior: getText(rule, "missing_behavior", "ask"), ambiguous_behavior: getText(rule, "ambiguous_behavior", "ask"), ...(isRecord(rule.zip) ? { zip: rule.zip } : {}) })); } export function summarizeAttachmentRules(rules: AttachmentRule[]): AttachmentSummary { return rules.reduce((summary, rule) => { if (isDirectAttachmentRule(rule)) { summary.direct += 1; } else { summary.rules += 1; } return summary; }, { direct: 0, rules: 0 }); } export function countIndividualAttachmentRules(entriesValue: unknown): number { const entries = asRecord(entriesValue); return asArray(entries.inline). map(asRecord). flatMap((entry) => asArray(entry.attachments)). length; } export function isDirectAttachmentRule(rule: AttachmentRule): boolean { const explicitType = getText(rule, "type"); if (explicitType === "direct") return true; if (explicitType === "pattern") return false; const fileFilter = getText(rule, "file_filter") || getText(rule, "file") || getText(rule, "filename") || getText(rule, "path"); if (!fileFilter) return false; return !/[{}*?\[\]]/.test(fileFilter); }