feat: govern attachment exceptions and ownership transfers
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
import { Button, i18nMessage } from "@govoplan/core-webui";
|
||||
import {
|
||||
Button,
|
||||
FormField,
|
||||
i18nMessage
|
||||
} from "@govoplan/core-webui";
|
||||
|
||||
import CampaignMessagePreviewOverlay, {
|
||||
type CampaignMessagePreviewAttachment
|
||||
@@ -16,6 +20,10 @@ import {
|
||||
numberOrUndefined,
|
||||
stringOrUndefined
|
||||
} from "./reviewFormatters";
|
||||
import {
|
||||
messageNeedsExplicitReview,
|
||||
messageRequiresAttachmentOverrideReason
|
||||
} from "./builtMessageQuery";
|
||||
|
||||
export default function BuiltMessagePreview({
|
||||
campaignJson,
|
||||
@@ -24,6 +32,10 @@ export default function BuiltMessagePreview({
|
||||
index,
|
||||
canStartSingleMessageSend,
|
||||
singleMessageSendBusy,
|
||||
reviewed,
|
||||
reviewReason,
|
||||
onReviewReasonChange,
|
||||
onAcceptReview,
|
||||
onSelect,
|
||||
onSendSingle,
|
||||
onClose
|
||||
@@ -34,6 +46,10 @@ export default function BuiltMessagePreview({
|
||||
index: number;
|
||||
canStartSingleMessageSend: boolean;
|
||||
singleMessageSendBusy: boolean;
|
||||
reviewed: boolean;
|
||||
reviewReason: string;
|
||||
onReviewReasonChange: (value: string) => void;
|
||||
onAcceptReview: (reasonRequired: boolean) => void;
|
||||
onSelect: (index: number) => void;
|
||||
onSendSingle: (index: number) => void;
|
||||
onClose: () => void;
|
||||
@@ -73,6 +89,8 @@ export default function BuiltMessagePreview({
|
||||
row,
|
||||
canStartSingleMessageSend
|
||||
);
|
||||
const explicitReview = messageNeedsExplicitReview(row);
|
||||
const reasonRequired = messageRequiresAttachmentOverrideReason(row);
|
||||
|
||||
return (
|
||||
<CampaignMessagePreviewOverlay
|
||||
@@ -109,14 +127,47 @@ export default function BuiltMessagePreview({
|
||||
onLast: () => onSelect(rows.length - 1)
|
||||
}}
|
||||
actions={
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={singleMessageSendBusy || Boolean(singleSendDisabledReason)}
|
||||
title={singleSendDisabledReason || "Test, send, or resend only this built message"}
|
||||
onClick={() => onSendSingle(index)}
|
||||
>
|
||||
{singleMessageSendBusy ? "Sending..." : "Message actions..."}
|
||||
</Button>
|
||||
<div className="built-message-review-actions">
|
||||
{explicitReview && !reviewed ? (
|
||||
<>
|
||||
<FormField
|
||||
label={
|
||||
reasonRequired
|
||||
? "Reason for accepting attachment exception"
|
||||
: "Review note"
|
||||
}
|
||||
help={
|
||||
reasonRequired
|
||||
? "Required. This reason is stored with the frozen build evidence."
|
||||
: "Optional note stored with the review decision."
|
||||
}
|
||||
>
|
||||
<input
|
||||
value={reviewReason}
|
||||
maxLength={4000}
|
||||
onChange={(event) =>
|
||||
onReviewReasonChange(event.target.value)
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={reasonRequired && !reviewReason.trim()}
|
||||
onClick={() => onAcceptReview(reasonRequired)}
|
||||
>
|
||||
Accept review conditions
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
<Button
|
||||
variant={explicitReview && !reviewed ? undefined : "primary"}
|
||||
disabled={singleMessageSendBusy || Boolean(singleSendDisabledReason)}
|
||||
title={singleSendDisabledReason || "Test, send, or resend only this built message"}
|
||||
onClick={() => onSendSingle(index)}
|
||||
>
|
||||
{singleMessageSendBusy ? "Sending..." : "Message actions..."}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
onClose={onClose}
|
||||
/>
|
||||
|
||||
@@ -61,22 +61,39 @@ export function storedMessageReviewState(version: CampaignVersionDetail | null):
|
||||
buildToken: string;
|
||||
inspectionComplete: boolean;
|
||||
reviewedMessageKeys: string[];
|
||||
issueDecisions: Array<Record<string, unknown>>;
|
||||
} {
|
||||
const build = asRecord(version?.build_summary);
|
||||
const buildToken = String(build.build_token ?? build.built_at ?? "");
|
||||
const review = asRecord(asRecord(version?.editor_state).review_send);
|
||||
if (!buildToken || String(review.build_token ?? "") !== buildToken) {
|
||||
return { buildToken, inspectionComplete: false, reviewedMessageKeys: [] };
|
||||
return {
|
||||
buildToken,
|
||||
inspectionComplete: false,
|
||||
reviewedMessageKeys: [],
|
||||
issueDecisions: []
|
||||
};
|
||||
}
|
||||
return {
|
||||
buildToken,
|
||||
inspectionComplete: review.inspection_complete === true,
|
||||
reviewedMessageKeys: asArray(review.reviewed_message_keys).filter(
|
||||
(value): value is string => typeof value === "string"
|
||||
)
|
||||
),
|
||||
issueDecisions: asArray(review.issue_decisions).map(asRecord)
|
||||
};
|
||||
}
|
||||
|
||||
export function messageRequiresAttachmentOverrideReason(
|
||||
row: Record<string, unknown>
|
||||
): boolean {
|
||||
return asArray(row.issues).some((value) => {
|
||||
const issue = asRecord(value);
|
||||
return String(issue.behavior ?? "").toLowerCase() === "ask"
|
||||
&& String(issue.source ?? "").startsWith("attachments");
|
||||
});
|
||||
}
|
||||
|
||||
export function builtMessageKey(row: Record<string, unknown>, index: number): string {
|
||||
return String(row.entry_id ?? row.entry_index ?? index);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user