feat(campaign): govern attachment reuse
This commit is contained in:
@@ -58,6 +58,9 @@ export default function AttachmentsDataPage({ settings, campaignId }: {settings:
|
||||
const basePaths = useMemo(() => normalizeAttachmentBasePaths(attachments.base_paths, attachments), [attachments]);
|
||||
const globalRules = useMemo(() => normalizeAttachmentRules(attachments.global), [attachments.global]);
|
||||
const zipConfig = useMemo(() => normalizeAttachmentZipCollection(attachments.zip), [attachments.zip]);
|
||||
const reusePolicy = asRecord(attachments.reuse_policy);
|
||||
const reuseAction = ["allow", "warn", "review", "block"].includes(String(reusePolicy.action)) ? String(reusePolicy.action) : "allow";
|
||||
const reuseAllowance = ["same_recipient", "same_message"].includes(String(reusePolicy.allow_within)) ? String(reusePolicy.allow_within) : "none";
|
||||
const residualFiles = asRecord(attachments.residual_files);
|
||||
const residualMode = ["report", "attach"].includes(String(residualFiles.mode)) ? String(residualFiles.mode) : "none";
|
||||
const residualRecipient = asRecord(residualFiles.recipient);
|
||||
@@ -113,6 +116,16 @@ export default function AttachmentsDataPage({ settings, campaignId }: {settings:
|
||||
});
|
||||
}
|
||||
|
||||
function patchReusePolicy(next: Record<string, unknown>) {
|
||||
if (locked) return;
|
||||
patch(["attachments", "reuse_policy"], {
|
||||
action: "allow",
|
||||
allow_within: "none",
|
||||
...reusePolicy,
|
||||
...next
|
||||
});
|
||||
}
|
||||
|
||||
function patchBasePath(index: number, patch: Partial<AttachmentBasePath>) {
|
||||
patchBasePaths(basePaths.map((basePath, currentIndex) => currentIndex === index ? { ...basePath, ...patch } : basePath));
|
||||
}
|
||||
@@ -306,6 +319,29 @@ export default function AttachmentsDataPage({ settings, campaignId }: {settings:
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="Attachment reuse policy" collapsible>
|
||||
<div className="campaign-residual-file-form">
|
||||
<FormField label="Duplicate-file action" help="Applied when the same resolved source file appears more than once outside the configured allowance.">
|
||||
<select value={reuseAction} disabled={locked} onChange={(event) => patchReusePolicy({ action: event.target.value })}>
|
||||
<option value="allow">Allow and record</option>
|
||||
<option value="warn">Allow with warning</option>
|
||||
<option value="review">Require explicit review</option>
|
||||
<option value="block">Block delivery</option>
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Allowed reuse boundary" help="A boundary is an exception to the selected action. No exception applies the action to every repeated use.">
|
||||
<select value={reuseAllowance} disabled={locked} onChange={(event) => patchReusePolicy({ allow_within: event.target.value })}>
|
||||
<option value="none">No exception</option>
|
||||
<option value="same_recipient">Within the same recipient</option>
|
||||
<option value="same_message">Within the same built message</option>
|
||||
</select>
|
||||
</FormField>
|
||||
</div>
|
||||
<DismissibleAlert tone={reuseAction === "block" ? "warning" : "info"} dismissible={false} compact>
|
||||
Every repeated-file finding is retained in the build protocol. Review requires a reason bound to the exact build; Block prevents affected messages from being queued.
|
||||
</DismissibleAlert>
|
||||
</Card>
|
||||
|
||||
<Card title="Unassigned file disposition" collapsible>
|
||||
<div className="campaign-residual-file-form">
|
||||
<FormField label="Action" help="Only sources with Unsent enabled are inspected. The existing warning policy remains active when no disposition is selected.">
|
||||
|
||||
Reference in New Issue
Block a user