feat(campaign-ui): review and link managed attachments

This commit is contained in:
2026-07-20 20:08:41 +02:00
parent fce6dd1138
commit 0a6064ec62
9 changed files with 921 additions and 150 deletions

2
.gitignore vendored
View File

@@ -332,6 +332,7 @@ cython_debug/
webui/.policy-test-build/
webui/.template-preview-test-build/
webui/.import-test-build/
webui/.review-preview-test-build/
# GovOPlaN shared ignore rules from govoplan-core
# Local WebUI test/build scratch directories
@@ -340,6 +341,7 @@ webui/.import-test-build/
.policy-test-build/
.template-preview-test-build/
.import-test-build/
.review-preview-test-build/
webui/.component-test-build/
webui/.module-test-build/
*.db

View File

@@ -26,7 +26,8 @@
"scripts": {
"test:policy-ui": "rm -rf .policy-test-build && mkdir -p .policy-test-build && printf '{\"type\":\"commonjs\"}\\n' > .policy-test-build/package.json && tsc -p tsconfig.policy-tests.json && node .policy-test-build/tests/policy-ui.test.js",
"test:template-preview": "rm -rf .template-preview-test-build && mkdir -p .template-preview-test-build && printf '{\"type\":\"commonjs\"}\\n' > .template-preview-test-build/package.json && tsc -p tsconfig.template-preview-tests.json && node .template-preview-test-build/tests/template-preview-draft.test.js",
"test:import-utils": "rm -rf .import-test-build && mkdir -p .import-test-build && printf '{\"type\":\"commonjs\"}\\n' > .import-test-build/package.json && tsc -p tsconfig.import-tests.json && node .import-test-build/tests/import-utils.test.js"
"test:import-utils": "rm -rf .import-test-build && mkdir -p .import-test-build && printf '{\"type\":\"commonjs\"}\\n' > .import-test-build/package.json && tsc -p tsconfig.import-tests.json && node .import-test-build/tests/import-utils.test.js",
"test:review-preview-ui": "rm -rf .review-preview-test-build && mkdir -p .review-preview-test-build && printf '{\"type\":\"commonjs\"}\\n' > .review-preview-test-build/package.json && tsc -p tsconfig.review-preview-tests.json && node .review-preview-test-build/tests/review-preview-ui.test.js"
},
"devDependencies": {
"typescript": "^5.7.2"

File diff suppressed because it is too large Load Diff

View File

@@ -39,7 +39,7 @@ export default function TemplateDataPage({ settings, campaignId }: {settings: Ap
const version = data.currentVersion;
const locked = isAuditLockedVersion(version, data.campaign?.current_version_id);
const { draft, setDraft, displayDraft, dirty, saveState, localError, patch, markDirty, saveDraft } = useCampaignDraftEditor({
const { draft, setDraft, displayDraft, dirty, saveState, localError, patch, markDirty, discardDraft, saveDraft } = useCampaignDraftEditor({
settings,
campaignId,
version,
@@ -121,6 +121,7 @@ export default function TemplateDataPage({ settings, campaignId }: {settings: Ap
const handle = window.setTimeout(() => {
void previewCampaignAttachments(settings, campaignId, version.id, {
include_unmatched: false,
include_unlinked_candidates: true,
campaign_json: campaignJsonForAttachmentPreview(displayDraft)
}).
then((response) => {
@@ -233,7 +234,7 @@ export default function TemplateDataPage({ settings, campaignId }: {settings: Ap
</div>
<div className="button-row compact-actions">
<Button disabled>i18n:govoplan-campaign.manage_templates.23688071</Button>
<Button onClick={reload} disabled={loading}>i18n:govoplan-campaign.reload.cce71553</Button>
<Button onClick={() => void discardDraft()} disabled={loading}>Discard</Button>
<Button variant="primary" onClick={() => saveDraft("manual")} disabled={!dirty || locked || !draft}>i18n:govoplan-campaign.save.efc007a3</Button>
</div>
</div>
@@ -399,45 +400,46 @@ draft: Record<string, unknown>)
: CampaignMessagePreviewAttachment[] {
if (loading) {
return [{
filename: i18nMessage("i18n:govoplan-campaign.resolving_attachment_patterns.87d7d21b"),
detail: i18nMessage("i18n:govoplan-campaign.managed_files_are_being_checked_for_this_recipie.489ecefd")
filename: "Resolving attachment patterns",
detail: "Managed files are being checked for this recipient preview."
}];
}
if (error) {
return [{ filename: i18nMessage("i18n:govoplan-campaign.attachment_preview_unavailable.62211756"), detail: error }];
return [{ filename: "Attachment preview unavailable", detail: error }];
}
return rules.flatMap((rule) => {
const zipProtection = zipProtectionForRule(rule, draft);
const detailParts = [
rule.source === "global" ? i18nMessage("i18n:govoplan-campaign.global.5f1184f7") : i18nMessage("i18n:govoplan-campaign.recipient.90343260"),
rule.source === "global" ? "Global" : "Recipient",
rule.label,
rule.required ? i18nMessage("i18n:govoplan-campaign.required.eed6bfb4") : i18nMessage("i18n:govoplan-campaign.optional.0c6c4102"),
rule.required ? "Required" : "Optional",
rule.pattern].
filter(Boolean);
const detail = detailParts.join(" · ");
const fallbackArchiveLabel = i18nMessage("i18n:govoplan-campaign.recipient_attachments_zip.79d436c7");
const fallbackArchiveLabel = "Recipient attachments ZIP";
if (rule.matches.length > 0) {
return rule.matches.map((match) => ({
filename: match.filename || match.display_path,
label: rule.label,
detail: i18nMessage("i18n:govoplan-campaign.value_value.a8618e4a", { value0: detail, value1: match.display_path ? ` · ${match.display_path}` : "" }),
detail: `${match.linked_to_campaign === false ? "Unlinked candidate" : "Linked"} · ${match.display_path ? `${detail} · ${match.display_path}` : detail}`,
contentType: match.content_type,
sizeBytes: match.size_bytes,
linkedToCampaign: match.linked_to_campaign !== false,
archiveGroup: rule.zip_included ? rule.zip_filename || "recipient-attachments.zip" : null,
archiveLabel: rule.zip_included ? rule.zip_filename || fallbackArchiveLabel : null,
protected: zipProtection.protected,
protectionNote: zipProtection.note
}));
}
const pattern = rule.pattern || i18nMessage("i18n:govoplan-campaign.attachment_pattern.3540c952");
const pattern = rule.pattern || "attachment pattern";
return [{
filename: i18nMessage("i18n:govoplan-campaign.no_file_matched_value", { value0: pattern }),
filename: `No file matched ${pattern}`,
label: rule.label,
detail: i18nMessage("i18n:govoplan-campaign.value_value.a8618e4a", { value0: detail, value1: rule.status !== "ok" ? ` · ${rule.status}` : "" }),
archiveGroup: rule.zip_included ? rule.zip_filename || "recipient-attachments.zip" : null,
archiveLabel: rule.zip_included ? rule.zip_filename || fallbackArchiveLabel : null,
protected: zipProtection.protected,
protectionNote: zipProtection.note
detail: rule.status !== "ok" ? `${detail} · ${rule.status}` : detail,
archiveGroup: null,
archiveLabel: null,
protected: false,
protectionNote: null
}];
});
}

View File

@@ -36,6 +36,7 @@ export type CampaignMessagePreviewOverlayProps = {
raw?: string | null;
rawLabel?: string;
navigation?: CampaignMessagePreviewNavigation;
actions?: ReactNode;
closeLabel?: string;
onClose: () => void;
};
@@ -53,6 +54,7 @@ export default function CampaignMessagePreviewOverlay({
raw,
rawLabel = "i18n:govoplan-campaign.raw_mime.82c612d4",
navigation,
actions,
closeLabel = "i18n:govoplan-campaign.close.bbfa773e",
onClose
}: CampaignMessagePreviewOverlayProps) {
@@ -131,7 +133,10 @@ export default function CampaignMessagePreviewOverlay({
</details>
}
</div>
<footer className="modal-footer"><Button variant="primary" onClick={onClose}>{closeLabel}</Button></footer>
<footer className="modal-footer">
{actions && <div className="button-row compact-actions">{actions}</div>}
<Button variant="primary" onClick={onClose}>{closeLabel}</Button>
</footer>
</div>
</div>);
@@ -146,4 +151,4 @@ function isEditableTarget(target: EventTarget | null): boolean {
export type MessagePreviewAttachment = CampaignMessagePreviewAttachment;
export type MessagePreviewMetaItem = CampaignMessagePreviewMetaItem;
export type MessagePreviewNavigation = CampaignMessagePreviewNavigation;
export type MessagePreviewOverlayProps = CampaignMessagePreviewOverlayProps;
export type MessagePreviewOverlayProps = CampaignMessagePreviewOverlayProps;

View File

@@ -0,0 +1,53 @@
export type AttachmentPreviewFileLike = {
id: string;
display_path: string;
filename: string;
linked_to_campaign?: boolean;
};
export type AttachmentPreviewLike<T extends AttachmentPreviewFileLike> = {
rules: Array<{matches: T[]}>;
linkable_files?: T[];
};
export function attachmentPreviewMatchedFiles<T extends AttachmentPreviewFileLike>(
preview: AttachmentPreviewLike<T> | null
): T[] {
if (!preview) return [];
const byId = new Map<string, T>();
for (const rule of preview.rules) {
for (const file of rule.matches) {
const key = file.id || file.display_path;
if (!key) continue;
const existing = byId.get(key);
if (existing) {
byId.set(key, {
...existing,
linked_to_campaign: existing.linked_to_campaign !== false || file.linked_to_campaign !== false
});
} else {
byId.set(key, file);
}
}
}
return [...byId.values()].sort((left, right) => {
const linkOrder = Number(left.linked_to_campaign === false) - Number(right.linked_to_campaign === false);
if (linkOrder !== 0) return linkOrder;
return (left.display_path || left.filename).localeCompare(right.display_path || right.filename);
});
}
export function attachmentPreviewLinkableFiles<T extends AttachmentPreviewFileLike>(
preview: AttachmentPreviewLike<T> | null
): T[] {
if (!preview) return [];
const source = preview.linkable_files && preview.linkable_files.length > 0
? preview.linkable_files
: attachmentPreviewMatchedFiles(preview).filter((file) => file.linked_to_campaign === false);
const byId = new Map<string, T>();
for (const file of source) {
const key = file.id || file.display_path;
if (key) byId.set(key, file);
}
return [...byId.values()];
}

View File

@@ -60,6 +60,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.all_imap_states.8546b84c": "All IMAP states",
"i18n:govoplan-campaign.all_smtp_states.739597b1": "All SMTP states",
"i18n:govoplan-campaign.all_templates.0bba114c": "All templates",
"i18n:govoplan-campaign.all_unique_managed_files_matched_by_the_current_.0214c3e5": "All unique managed files matched by the current attachment rules are available in this list.",
"i18n:govoplan-campaign.allow_individual_attachments.a6ff0e87": "Allow individual attachments",
"i18n:govoplan-campaign.allow_individual_bcc.a932d94b": "Allow individual BCC",
"i18n:govoplan-campaign.allow_individual_cc.0457c0e2": "Allow individual CC",
@@ -95,6 +96,8 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.as_a_campaign_field_remove_this_placeholder_or_c.6bf2dea8": "as a campaign field, remove this placeholder, or continue editing.",
"i18n:govoplan-campaign.attach_job_csv.adb76197": "Attach job CSV",
"i18n:govoplan-campaign.attach_json_report.d70883b5": "Attach JSON report",
"i18n:govoplan-campaign.attachment_file_links.0be74fd1": "Attachment file links",
"i18n:govoplan-campaign.attachment_file_links_value.ce230e30": "Attachment file links ({value0})",
"i18n:govoplan-campaign.attachment_issues.69748336": "Attachment issues",
"i18n:govoplan-campaign.attachment_label.a340f70e": "Attachment label",
"i18n:govoplan-campaign.attachment_notice.b73a59fe": "Attachment notice",
@@ -105,6 +108,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.keep_original_zip_entry_name.d51558c4": "Keep original ZIP entry name",
"i18n:govoplan-campaign.message_filename_template.0b7ac934": "Message filename template",
"i18n:govoplan-campaign.zip_entry_name_template.83772a73": "ZIP entry name template",
"i18n:govoplan-campaign.attachment_preview_refreshed.50d5b50d": "Attachment preview refreshed.",
"i18n:govoplan-campaign.attachment_preview_unavailable.62211756": "Attachment preview unavailable",
"i18n:govoplan-campaign.attachment_rule_matched_more_files_than_expected.3e2110e8": "Attachment rule matched more files than expected.",
"i18n:govoplan-campaign.attachment_rule_s.0e5ee66a": "attachment rule(s),",
@@ -430,8 +434,17 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.last_loaded_value.35ef046a": "Last loaded: {value0}",
"i18n:govoplan-campaign.last_message.83741110": "Last message",
"i18n:govoplan-campaign.last_result.110b888b": "Last result",
"i18n:govoplan-campaign.link_and_lock.6eac996d": "Link and lock",
"i18n:govoplan-campaign.link_matched_files_before_locking.5d1cf653": "Link matched files before locking?",
"i18n:govoplan-campaign.link_value_file.4d4ce740": "Link {value0} file",
"i18n:govoplan-campaign.link_value_file_s.ca800d96": "Link {value0} file(s)",
"i18n:govoplan-campaign.link_value_files.88b7e6a7": "Link {value0} files",
"i18n:govoplan-campaign.linked_already_part_of_the_campaign_file_snapsho.d037a6bc": "Linked: already part of the campaign file snapshot.",
"i18n:govoplan-campaign.linked_value_attachment_file_s_to_this_campaign.02e5ecf7": "Linked {value0} attachment file(s) to this campaign.",
"i18n:govoplan-campaign.linked.a089f600": "Linked",
"i18n:govoplan-campaign.linking.a5f54e0f": "Linking",
"i18n:govoplan-campaign.linking_matched_attachment_files.92f38088": "Linking matched attachment files…",
"i18n:govoplan-campaign.linking_matched_files_then_validating_the_campa.0d48a1d0": "Linking matched files, then validating the campaign…",
"i18n:govoplan-campaign.linking.6f640897": "Linking…",
"i18n:govoplan-campaign.load_from_library.327ada7c": "Load from library",
"i18n:govoplan-campaign.load_imap_diagnostics.8ebb1891": "Load IMAP diagnostics",
@@ -483,6 +496,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.map.ab478f3e": "Map",
"i18n:govoplan-campaign.mapping_value": "Mapping: {value0}",
"i18n:govoplan-campaign.marks_the_message_for_review.d63beb24": "Marks the message for review.",
"i18n:govoplan-campaign.matched.1bf3ec5b": "Matched",
"i18n:govoplan-campaign.matched_attachments.ead1eeb1": "Matched attachments",
"i18n:govoplan-campaign.matched_files.f79c63bb": "Matched files",
"i18n:govoplan-campaign.matching_of.66a3778e": "matching of",
@@ -541,6 +555,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.my_files.71d01a41": "My files",
"i18n:govoplan-campaign.name_and_scenario.f2bc5241": "Name and scenario",
"i18n:govoplan-campaign.name.709a2322": "Name",
"i18n:govoplan-campaign.need_link.fa4ab530": "Need link",
"i18n:govoplan-campaign.need_linking.a7617722": "Need linking",
"i18n:govoplan-campaign.need_review.201a4493": "Need review",
"i18n:govoplan-campaign.needs_attention.a126722e": "Needs attention",
@@ -588,6 +603,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.no_individual_attachment_source.6b54176a": "No individual attachment source",
"i18n:govoplan-campaign.no_inline_recipients_are_available_yet.e405bacb": "No inline recipients are available yet.",
"i18n:govoplan-campaign.no_jobs_match_the_current_filters.b1501ff5": "No jobs match the current filters.",
"i18n:govoplan-campaign.no_managed_files_matched_the_current_attachment_.dba99f5c": "No managed files matched the current attachment patterns.",
"i18n:govoplan-campaign.no_message_content_is_available.54e8e7e6": "No message content is available.",
"i18n:govoplan-campaign.no_message_id.43390ef7": "No message ID",
"i18n:govoplan-campaign.no_messages_match_the_active_filters.14811cc8": "No messages match the active filters.",
@@ -693,6 +709,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.planned_address_actions.1d4a056a": "Planned address actions",
"i18n:govoplan-campaign.policies.f03ff937": "POLICIES",
"i18n:govoplan-campaign.policy.bb9cf141": "Policy",
"i18n:govoplan-campaign.preview_includes_already_linked_files_and_access.dfef92af": "Preview includes already linked files and accessible unlinked candidates. Locking can link candidates before validation.",
"i18n:govoplan-campaign.preview_message_navigation.d28a8dc0": "Preview message navigation",
"i18n:govoplan-campaign.preview.4bf30626": "Preview:",
"i18n:govoplan-campaign.preview.f1fbb2b4": "Preview",
@@ -757,6 +774,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.refresh_matches.11e36411": "Refresh matches",
"i18n:govoplan-campaign.refresh_status.ade15a52": "Refresh status",
"i18n:govoplan-campaign.refresh.56e3badc": "Refresh",
"i18n:govoplan-campaign.refreshing.505dddc9": "Refreshing",
"i18n:govoplan-campaign.refreshing_queue_status.2a7dea57": "Refreshing queue status…",
"i18n:govoplan-campaign.refreshing_status.d8965739": "Refreshing status…",
"i18n:govoplan-campaign.reload_page.37614e96": "Reload page",
@@ -886,6 +904,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.shared_group_address_books_and_lists.12bac69d": "Shared group address books and lists.",
"i18n:govoplan-campaign.shared_group_files.fffa6e27": "Shared group files",
"i18n:govoplan-campaign.shared_list.b3c94b39": "Shared list",
"i18n:govoplan-campaign.shared_source.7d4a1bf2": "Shared source",
"i18n:govoplan-campaign.shared_with.6203f449": "Shared with",
"i18n:govoplan-campaign.shared.50d0d8dd": "Shared",
"i18n:govoplan-campaign.sheet.53bc47a7": "Sheet",
@@ -1019,6 +1038,8 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.undefined_field_reference.4ff8e266": "Undefined field reference",
"i18n:govoplan-campaign.undefined_placeholder_namespace_detected.2ef5c282": "Undefined placeholder namespace detected:",
"i18n:govoplan-campaign.unknown.bc7819b3": "Unknown",
"i18n:govoplan-campaign.unlinked_candidate_files_are_not_yet_part_of_the.b8fd5998": "Unlinked candidate files are not yet part of the campaign snapshot. They will be linked when you confirm locking.",
"i18n:govoplan-campaign.unlinked_candidate_match_potentially_missing_unt.2bae9433": "Unlinked: candidate match, potentially missing until linked.",
"i18n:govoplan-campaign.unlock_temporary_lock.8a3ad468": "Unlock temporary lock?",
"i18n:govoplan-campaign.unlock_validation.e3066247": "Unlock validation?",
"i18n:govoplan-campaign.unlock_validation.f01952b6": "Unlock validation",
@@ -1092,7 +1113,10 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.value_direct_file_s_value_rule_s_pattern_s.df9b46d9": "{value0} direct file(s), {value1} rule(s) / pattern(s)",
"i18n:govoplan-campaign.value_encryption_value": "{value0} · Encryption: {value1}",
"i18n:govoplan-campaign.value_individual_attachment_value_this_source_di.3c7428e7": "{value0} individual attachment {value1} this source. Disabling it will remove those recipient-specific attachment entries.",
"i18n:govoplan-campaign.value_matched_attachment_file_link.30a84824": "{value0} matched attachment file link",
"i18n:govoplan-campaign.value_matched_attachment_file_links.ce509a3a": "{value0} matched attachment file links",
"i18n:govoplan-campaign.value_message_s_contain_warnings_or_exclusions_b.e7d13991": "{value0} message(s) contain warnings or exclusions but no blocking condition. Completing review will acknowledge these conditions together without opening each message.",
"i18n:govoplan-campaign.value_matched_file_s_are_not_yet_linked_to_t.43d6c926": "{value0} matched file(s) are not yet linked to this campaign. Link them now and continue locking?",
"i18n:govoplan-campaign.value_min.c9d89eae": "{value0}/min",
"i18n:govoplan-campaign.value_minute.aeb1a9ea": "{value0} / minute",
"i18n:govoplan-campaign.value_saving_is_disabled_until_this_is_corrected.cd0a2ca0": "{value0}. Saving is disabled until this is corrected.",
@@ -1111,6 +1135,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.version.2da600bf": "Version",
"i18n:govoplan-campaign.versions_and_import_export.cc05cb43": "Versions and import/export",
"i18n:govoplan-campaign.versions.a239107e": "Versions",
"i18n:govoplan-campaign.view_all_value_matched_attachment_file_links.81e53951": "View all {value0} matched attachment file links",
"i18n:govoplan-campaign.waiting.33d30632": "Waiting",
"i18n:govoplan-campaign.warn.3009d557": "Warn",
"i18n:govoplan-campaign.warning.e9c45563": "Warning",
@@ -1187,6 +1212,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.all_imap_states.8546b84c": "All IMAP states",
"i18n:govoplan-campaign.all_smtp_states.739597b1": "All SMTP states",
"i18n:govoplan-campaign.all_templates.0bba114c": "All templates",
"i18n:govoplan-campaign.all_unique_managed_files_matched_by_the_current_.0214c3e5": "Diese Liste enthält alle eindeutigen verwalteten Dateien, die den aktuellen Anhangsregeln entsprechen.",
"i18n:govoplan-campaign.allow_individual_attachments.a6ff0e87": "Allow individual attachments",
"i18n:govoplan-campaign.allow_individual_bcc.a932d94b": "Allow individual BCC",
"i18n:govoplan-campaign.allow_individual_cc.0457c0e2": "Allow individual CC",
@@ -1222,6 +1248,8 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.as_a_campaign_field_remove_this_placeholder_or_c.6bf2dea8": "as a campaign field, remove this placeholder, or continue editing.",
"i18n:govoplan-campaign.attach_job_csv.adb76197": "Attach job CSV",
"i18n:govoplan-campaign.attach_json_report.d70883b5": "Attach JSON report",
"i18n:govoplan-campaign.attachment_file_links.0be74fd1": "Verknüpfungen von Anhangsdateien",
"i18n:govoplan-campaign.attachment_file_links_value.ce230e30": "Verknüpfungen von Anhangsdateien ({value0})",
"i18n:govoplan-campaign.attachment_issues.69748336": "Attachment issues",
"i18n:govoplan-campaign.attachment_label.a340f70e": "Attachment label",
"i18n:govoplan-campaign.attachment_notice.b73a59fe": "Attachment notice",
@@ -1232,6 +1260,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.keep_original_zip_entry_name.d51558c4": "Urspruenglichen ZIP-Eintragsnamen behalten",
"i18n:govoplan-campaign.message_filename_template.0b7ac934": "Dateinamenvorlage fuer Nachrichten",
"i18n:govoplan-campaign.zip_entry_name_template.83772a73": "Namensvorlage fuer ZIP-Eintraege",
"i18n:govoplan-campaign.attachment_preview_refreshed.50d5b50d": "Attachment preview refreshed.",
"i18n:govoplan-campaign.attachment_preview_unavailable.62211756": "Attachment preview unavailable",
"i18n:govoplan-campaign.attachment_rule_matched_more_files_than_expected.3e2110e8": "Attachment rule matched more files than expected.",
"i18n:govoplan-campaign.attachment_rule_s.0e5ee66a": "attachment rule(s),",
@@ -1557,8 +1586,17 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.last_loaded_value.35ef046a": "Last loaded: {value0}",
"i18n:govoplan-campaign.last_message.83741110": "Last message",
"i18n:govoplan-campaign.last_result.110b888b": "Last result",
"i18n:govoplan-campaign.link_and_lock.6eac996d": "Link and lock",
"i18n:govoplan-campaign.link_matched_files_before_locking.5d1cf653": "Link matched files before locking?",
"i18n:govoplan-campaign.link_value_file.4d4ce740": "{value0} Datei verknüpfen",
"i18n:govoplan-campaign.link_value_file_s.ca800d96": "Link {value0} file(s)",
"i18n:govoplan-campaign.linked.a089f600": "Linked",
"i18n:govoplan-campaign.link_value_files.88b7e6a7": "{value0} Dateien verknüpfen",
"i18n:govoplan-campaign.linked_already_part_of_the_campaign_file_snapsho.d037a6bc": "Verknüpft: bereits Teil des Kampagnen-Datei-Snapshots.",
"i18n:govoplan-campaign.linked_value_attachment_file_s_to_this_campaign.02e5ecf7": "Linked {value0} attachment file(s) to this campaign.",
"i18n:govoplan-campaign.linked.a089f600": "Verknüpft",
"i18n:govoplan-campaign.linking.a5f54e0f": "Wird verknüpft",
"i18n:govoplan-campaign.linking_matched_attachment_files.92f38088": "Linking matched attachment files…",
"i18n:govoplan-campaign.linking_matched_files_then_validating_the_campa.0d48a1d0": "Linking matched files, then validating the campaign…",
"i18n:govoplan-campaign.linking.6f640897": "Linking…",
"i18n:govoplan-campaign.load_from_library.327ada7c": "Load from library",
"i18n:govoplan-campaign.load_imap_diagnostics.8ebb1891": "Load IMAP diagnostics",
@@ -1610,6 +1648,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.map.ab478f3e": "Map",
"i18n:govoplan-campaign.mapping_value": "Mapping: {value0}",
"i18n:govoplan-campaign.marks_the_message_for_review.d63beb24": "Marks the message for review.",
"i18n:govoplan-campaign.matched.1bf3ec5b": "Treffer",
"i18n:govoplan-campaign.matched_attachments.ead1eeb1": "Matched attachments",
"i18n:govoplan-campaign.matched_files.f79c63bb": "Matched files",
"i18n:govoplan-campaign.matching_of.66a3778e": "matching of",
@@ -1668,6 +1707,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.my_files.71d01a41": "My files",
"i18n:govoplan-campaign.name_and_scenario.f2bc5241": "Name and scenario",
"i18n:govoplan-campaign.name.709a2322": "Name",
"i18n:govoplan-campaign.need_link.fa4ab530": "Zu verknüpfen",
"i18n:govoplan-campaign.need_linking.a7617722": "Need linking",
"i18n:govoplan-campaign.need_review.201a4493": "Need review",
"i18n:govoplan-campaign.needs_attention.a126722e": "Benötigt Aufmerksamkeit",
@@ -1715,6 +1755,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.no_individual_attachment_source.6b54176a": "No individual attachment source",
"i18n:govoplan-campaign.no_inline_recipients_are_available_yet.e405bacb": "No inline recipients are available yet.",
"i18n:govoplan-campaign.no_jobs_match_the_current_filters.b1501ff5": "No jobs match the current filters.",
"i18n:govoplan-campaign.no_managed_files_matched_the_current_attachment_.dba99f5c": "Keine verwalteten Dateien entsprechen den aktuellen Anhangsmustern.",
"i18n:govoplan-campaign.no_message_content_is_available.54e8e7e6": "No message content is available.",
"i18n:govoplan-campaign.no_message_id.43390ef7": "No message ID",
"i18n:govoplan-campaign.no_messages_match_the_active_filters.14811cc8": "No messages match the active filters.",
@@ -1820,6 +1861,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.planned_address_actions.1d4a056a": "Planned address actions",
"i18n:govoplan-campaign.policies.f03ff937": "POLICIES",
"i18n:govoplan-campaign.policy.bb9cf141": "Richtlinie",
"i18n:govoplan-campaign.preview_includes_already_linked_files_and_access.dfef92af": "Die Vorschau enthält bereits verknüpfte Dateien und zugängliche, noch nicht verknüpfte Kandidaten. Beim Sperren können Kandidaten vor der Validierung verknüpft werden.",
"i18n:govoplan-campaign.preview_message_navigation.d28a8dc0": "Preview message navigation",
"i18n:govoplan-campaign.preview.4bf30626": "Preview:",
"i18n:govoplan-campaign.preview.f1fbb2b4": "Vorschau",
@@ -1883,7 +1925,8 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.recording_the_completed_message_review.16f58b81": "Recording the completed message review…",
"i18n:govoplan-campaign.refresh_matches.11e36411": "Refresh matches",
"i18n:govoplan-campaign.refresh_status.ade15a52": "Refresh status",
"i18n:govoplan-campaign.refresh.56e3badc": "Refresh",
"i18n:govoplan-campaign.refresh.56e3badc": "Aktualisieren",
"i18n:govoplan-campaign.refreshing.505dddc9": "Wird aktualisiert",
"i18n:govoplan-campaign.refreshing_queue_status.2a7dea57": "Refreshing queue status…",
"i18n:govoplan-campaign.refreshing_status.d8965739": "Refreshing status…",
"i18n:govoplan-campaign.reload_page.37614e96": "Reload page",
@@ -2013,6 +2056,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.shared_group_address_books_and_lists.12bac69d": "Shared group address books and lists.",
"i18n:govoplan-campaign.shared_group_files.fffa6e27": "Shared group files",
"i18n:govoplan-campaign.shared_list.b3c94b39": "Shared list",
"i18n:govoplan-campaign.shared_source.7d4a1bf2": "Gemeinsame Quelle",
"i18n:govoplan-campaign.shared_with.6203f449": "Freigegeben für",
"i18n:govoplan-campaign.shared.50d0d8dd": "Shared",
"i18n:govoplan-campaign.sheet.53bc47a7": "Sheet",
@@ -2146,6 +2190,8 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.undefined_field_reference.4ff8e266": "Undefined field reference",
"i18n:govoplan-campaign.undefined_placeholder_namespace_detected.2ef5c282": "Undefined placeholder namespace detected:",
"i18n:govoplan-campaign.unknown.bc7819b3": "Unknown",
"i18n:govoplan-campaign.unlinked_candidate_files_are_not_yet_part_of_the.b8fd5998": "Nicht verknüpfte Kandidatendateien sind noch nicht Teil des Kampagnen-Snapshots. Sie werden verknüpft, wenn Sie das Sperren bestätigen.",
"i18n:govoplan-campaign.unlinked_candidate_match_potentially_missing_unt.2bae9433": "Nicht verknüpft: möglicher Treffer, der ohne Verknüpfung fehlen kann.",
"i18n:govoplan-campaign.unlock_temporary_lock.8a3ad468": "Unlock temporary lock?",
"i18n:govoplan-campaign.unlock_validation.e3066247": "Unlock validation?",
"i18n:govoplan-campaign.unlock_validation.f01952b6": "Unlock validation",
@@ -2219,7 +2265,10 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.value_direct_file_s_value_rule_s_pattern_s.df9b46d9": "{value0} direct file(s), {value1} rule(s) / pattern(s)",
"i18n:govoplan-campaign.value_encryption_value": "{value0} · Verschlüsselung: {value1}",
"i18n:govoplan-campaign.value_individual_attachment_value_this_source_di.3c7428e7": "{value0} individual attachment {value1} this source. Disabling it will remove those recipient-specific attachment entries.",
"i18n:govoplan-campaign.value_matched_attachment_file_link.30a84824": "{value0} gefundene Verknüpfung einer Anhangsdatei",
"i18n:govoplan-campaign.value_matched_attachment_file_links.ce509a3a": "{value0} gefundene Verknüpfungen von Anhangsdateien",
"i18n:govoplan-campaign.value_message_s_contain_warnings_or_exclusions_b.e7d13991": "{value0} message(s) contain warnings or exclusions but no blocking condition. Completing review will acknowledge these conditions together without opening each message.",
"i18n:govoplan-campaign.value_matched_file_s_are_not_yet_linked_to_t.43d6c926": "{value0} matched file(s) are not yet linked to this campaign. Link them now and continue locking?",
"i18n:govoplan-campaign.value_min.c9d89eae": "{value0}/min",
"i18n:govoplan-campaign.value_minute.aeb1a9ea": "{value0} / minute",
"i18n:govoplan-campaign.value_saving_is_disabled_until_this_is_corrected.cd0a2ca0": "{value0}. Saving is disabled until this is corrected.",
@@ -2238,6 +2287,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-campaign.version.2da600bf": "Version",
"i18n:govoplan-campaign.versions_and_import_export.cc05cb43": "Versions and import/export",
"i18n:govoplan-campaign.versions.a239107e": "Versions",
"i18n:govoplan-campaign.view_all_value_matched_attachment_file_links.81e53951": "Alle {value0} gefundenen Verknüpfungen von Anhangsdateien anzeigen",
"i18n:govoplan-campaign.waiting.33d30632": "Waiting",
"i18n:govoplan-campaign.warn.3009d557": "Warn",
"i18n:govoplan-campaign.warning.e9c45563": "Warning",

View File

@@ -0,0 +1,113 @@
import {
attachmentPreviewLinkableFiles,
attachmentPreviewMatchedFiles,
type AttachmentPreviewFileLike,
type AttachmentPreviewLike
} from "../src/features/campaigns/utils/attachmentPreview";
declare function require(name: string): {
readFileSync(path: string, encoding: string): string;
createHash(algorithm: string): {
update(value: string): {digest(encoding: "hex"): string};
};
};
const { readFileSync } = require("node:fs");
const { createHash } = require("node:crypto");
function assert(condition: unknown, message = "assertion failed"): void {
if (!condition) throw new Error(message);
}
function previewFile(index: number): AttachmentPreviewFileLike {
return {
id: `file-${index}`,
display_path: `/campaign/attachments/file-${String(index).padStart(2, "0")}.pdf`,
filename: `file-${String(index).padStart(2, "0")}.pdf`,
linked_to_campaign: index % 2 === 0
};
}
function previewRule(matches: AttachmentPreviewFileLike[]): {matches: AttachmentPreviewFileLike[]} {
return {
matches
};
}
const files = Array.from({ length: 25 }, (_, index) => previewFile(index));
const duplicatePromotedToLinked = { ...files[1], linked_to_campaign: true };
const preview: AttachmentPreviewLike<AttachmentPreviewFileLike> = {
rules: [
previewRule(files.slice(0, 14)),
previewRule([...files.slice(14), duplicatePromotedToLinked])
]
};
const matched = attachmentPreviewMatchedFiles(preview);
assert(matched.length === 25, "all unique attachment matches remain available; the review model must not truncate at twelve");
assert(matched.find((file) => file.id === "file-1")?.linked_to_campaign === true, "duplicate matches preserve the strongest linked state");
assert(matched.every((file, index) => index === 0 || Number(matched[index - 1].linked_to_campaign === false) <= Number(file.linked_to_campaign === false)), "linked matches sort before unlinked candidates");
const fallbackLinkable = attachmentPreviewLinkableFiles(preview);
assert(fallbackLinkable.length === 11, "linkable fallback contains every unique unlinked candidate");
const explicitLinkablePreview = {
...preview,
linkable_files: [files[3], files[3], files[5]]
};
assert(attachmentPreviewLinkableFiles(explicitLinkablePreview).length === 2, "explicit linkable candidates are deduplicated without a display cap");
const reviewSource = readFileSync("src/features/campaigns/ReviewSendPage.tsx", "utf8");
assert(!reviewSource.includes("attachmentPreviewMatchedFiles(preview).slice("), "review attachment links must not use an arbitrary display slice");
assert(reviewSource.includes("<AttachmentLinkingFileList files={matchedFiles} compact />"), "compact review exposes the complete bounded attachment list");
assert(reviewSource.includes('aria-haspopup="dialog"'), "the matched count advertises its attachment detail dialog");
assert(reviewSource.includes("tabIndex={0}"), "bounded attachment lists are keyboard-focusable scroll regions");
assert(reviewSource.includes('i18nMessage("i18n:govoplan-campaign.attachment_file_links_value.ce230e30"'), "dynamic attachment dialog title uses the i18n message contract");
assert(reviewSource.includes('i18nMessage("i18n:govoplan-campaign.view_all_value_matched_attachment_file_links.81e53951"'), "dynamic attachment count aria label uses the i18n message contract");
assert(!reviewSource.includes(">Attachment file links<"), "attachment review heading is not raw English");
assert(!reviewSource.includes('label="Matched"'), "attachment review facts are not raw English");
assert(!reviewSource.includes("`Link ${unlinkedCount}"), "attachment link actions are not assembled from raw English");
assert(!reviewSource.includes("Linked: already part of the campaign file snapshot."), "attachment link-state detail is not raw English");
assert(!reviewSource.includes("Unlinked: candidate match, potentially missing until linked."), "unlinked attachment detail is not raw English");
const newTranslations = [
["i18n:govoplan-campaign.all_unique_managed_files_matched_by_the_current_.0214c3e5", "All unique managed files matched by the current attachment rules are available in this list.", "Diese Liste enthält alle eindeutigen verwalteten Dateien, die den aktuellen Anhangsregeln entsprechen."],
["i18n:govoplan-campaign.attachment_file_links.0be74fd1", "Attachment file links", "Verknüpfungen von Anhangsdateien"],
["i18n:govoplan-campaign.attachment_file_links_value.ce230e30", "Attachment file links ({value0})", "Verknüpfungen von Anhangsdateien ({value0})"],
["i18n:govoplan-campaign.link_value_file.4d4ce740", "Link {value0} file", "{value0} Datei verknüpfen"],
["i18n:govoplan-campaign.link_value_files.88b7e6a7", "Link {value0} files", "{value0} Dateien verknüpfen"],
["i18n:govoplan-campaign.linked_already_part_of_the_campaign_file_snapsho.d037a6bc", "Linked: already part of the campaign file snapshot.", "Verknüpft: bereits Teil des Kampagnen-Datei-Snapshots."],
["i18n:govoplan-campaign.linking.a5f54e0f", "Linking", "Wird verknüpft"],
["i18n:govoplan-campaign.matched.1bf3ec5b", "Matched", "Treffer"],
["i18n:govoplan-campaign.need_link.fa4ab530", "Need link", "Zu verknüpfen"],
["i18n:govoplan-campaign.no_managed_files_matched_the_current_attachment_.dba99f5c", "No managed files matched the current attachment patterns.", "Keine verwalteten Dateien entsprechen den aktuellen Anhangsmustern."],
["i18n:govoplan-campaign.preview_includes_already_linked_files_and_access.dfef92af", "Preview includes already linked files and accessible unlinked candidates. Locking can link candidates before validation.", "Die Vorschau enthält bereits verknüpfte Dateien und zugängliche, noch nicht verknüpfte Kandidaten. Beim Sperren können Kandidaten vor der Validierung verknüpft werden."],
["i18n:govoplan-campaign.refreshing.505dddc9", "Refreshing", "Wird aktualisiert"],
["i18n:govoplan-campaign.shared_source.7d4a1bf2", "Shared source", "Gemeinsame Quelle"],
["i18n:govoplan-campaign.unlinked_candidate_files_are_not_yet_part_of_the.b8fd5998", "Unlinked candidate files are not yet part of the campaign snapshot. They will be linked when you confirm locking.", "Nicht verknüpfte Kandidatendateien sind noch nicht Teil des Kampagnen-Snapshots. Sie werden verknüpft, wenn Sie das Sperren bestätigen."],
["i18n:govoplan-campaign.unlinked_candidate_match_potentially_missing_unt.2bae9433", "Unlinked: candidate match, potentially missing until linked.", "Nicht verknüpft: möglicher Treffer, der ohne Verknüpfung fehlen kann."],
["i18n:govoplan-campaign.value_matched_attachment_file_link.30a84824", "{value0} matched attachment file link", "{value0} gefundene Verknüpfung einer Anhangsdatei"],
["i18n:govoplan-campaign.value_matched_attachment_file_links.ce509a3a", "{value0} matched attachment file links", "{value0} gefundene Verknüpfungen von Anhangsdateien"],
["i18n:govoplan-campaign.view_all_value_matched_attachment_file_links.81e53951", "View all {value0} matched attachment file links", "Alle {value0} gefundenen Verknüpfungen von Anhangsdateien anzeigen"]
] as const;
const translationCatalog = readFileSync("src/i18n/generatedTranslations.ts", "utf8");
for (const [key, english, german] of newTranslations) {
const digest = createHash("sha1").update(english).digest("hex").slice(0, 8);
assert(key.endsWith(`.${digest}`), `${key} uses the stable SHA-1 suffix for its English source`);
assert(translationCatalog.split(`"${key}"`).length - 1 === 2, `${key} is present exactly once in both language catalogs`);
assert(translationCatalog.includes(`"${key}": ${JSON.stringify(english)}`), `${key} has its English catalog value`);
assert(translationCatalog.includes(`"${key}": ${JSON.stringify(german)}`), `${key} has its German catalog value`);
}
const overlaySource = readFileSync("src/features/campaigns/components/MessagePreviewOverlay.tsx", "utf8");
assert(overlaySource.includes("message-preview-backdrop"), "message previews expose a layout-specific responsive backdrop hook");
assert(overlaySource.includes("{actions && <div"), "built-message footer actions remain in the stable shared preview footer");
assert(overlaySource.includes('aria-label={closeLabel}'), "the preview close control has an accessible label");
const styles = readFileSync("src/styles/campaign-workspace.css", "utf8");
assert(styles.includes("height: min(780px, calc(100dvh - 48px));"), "desktop previews use a stable responsive height");
assert(styles.includes("height: calc(100dvh - 16px);"), "small viewports use the available dynamic viewport height");
assert(styles.includes(".message-preview-modal .modal-footer"), "preview footer has a stable layout rule");
assert(styles.includes(".attachment-linking-file-list.is-compact"), "compact attachment links use a bounded scroll surface");
assert(styles.includes(".attachment-linking-file-list.is-detail"), "attachment detail links use an independent scroll surface");

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM"],
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"module": "CommonJS",
"moduleResolution": "Node",
"noEmit": false,
"outDir": ".review-preview-test-build",
"rootDir": "."
},
"include": [
"tests/review-preview-ui.test.ts",
"src/features/campaigns/utils/attachmentPreview.ts"
]
}