Release govoplan-campaign v0.1.28: stabilize saving, review and delivery recovery
Module Package Release / publish-packages (push) Successful in 12s

This commit is contained in:
2026-09-08 01:32:26 +02:00
parent 1b32427813
commit c51fc180fb
111 changed files with 6905 additions and 980 deletions
@@ -1,8 +1,10 @@
import {
Button,
DismissibleAlert,
FormField,
i18nMessage
} from "@govoplan/core-webui";
import { useState } from "react";
import CampaignMessagePreviewOverlay, {
type CampaignMessagePreviewAttachment
@@ -34,6 +36,9 @@ export default function BuiltMessagePreview({
singleMessageSendBusy,
reviewed,
reviewReason,
reviewSaving = false,
reviewDisabled = false,
reviewError = "",
onReviewReasonChange,
onAcceptReview,
onSelect,
@@ -48,8 +53,11 @@ export default function BuiltMessagePreview({
singleMessageSendBusy: boolean;
reviewed: boolean;
reviewReason: string;
reviewSaving?: boolean;
reviewDisabled?: boolean;
reviewError?: string;
onReviewReasonChange: (value: string) => void;
onAcceptReview: (reasonRequired: boolean) => void;
onAcceptReview: (reasonRequired: boolean, reason: string) => Promise<void>;
onSelect: (index: number) => void;
onSendSingle: (index: number) => void;
onClose: () => void;
@@ -128,40 +136,16 @@ export default function BuiltMessagePreview({
}}
actions={
<div className="built-message-review-actions">
{reviewError && <DismissibleAlert tone="danger" dismissible={false}>{reviewError}</DismissibleAlert>}
{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>
</>
<ReviewDecisionForm key={String(row.id ?? row.review_key ?? index)} initialReason={reviewReason}
reasonRequired={reasonRequired} saving={reviewSaving} disabled={reviewDisabled}
onReasonChange={onReviewReasonChange} onAccept={(reason) => onAcceptReview(reasonRequired, reason)} />
) : null}
{reviewed && <p role="status">i18n:govoplan-campaign.review_decision_saved</p>}
<Button
variant={explicitReview && !reviewed ? undefined : "primary"}
disabled={singleMessageSendBusy || Boolean(singleSendDisabledReason)}
disabled={reviewSaving || singleMessageSendBusy || Boolean(singleSendDisabledReason)}
title={singleSendDisabledReason || "Test, send, or resend only this built message"}
onClick={() => onSendSingle(index)}
>
@@ -169,11 +153,32 @@ export default function BuiltMessagePreview({
</Button>
</div>
}
closeDisabled={reviewSaving}
onClose={onClose}
/>
);
}
// Keystrokes rerender only this small form, not the campaign table, template
// rendering and message body. The parent retains a draft ref for navigation.
function ReviewDecisionForm({ initialReason, reasonRequired, saving, disabled, onReasonChange, onAccept }: {
initialReason: string; reasonRequired: boolean; saving: boolean; disabled: boolean;
onReasonChange: (value: string) => void; onAccept: (reason: string) => Promise<void>;
}) {
const [reason, setReason] = useState(initialReason);
return <>
<FormField label={reasonRequired ? "i18n:govoplan-campaign.review_reason_label" : "i18n:govoplan-campaign.review_note_label"}
help="i18n:govoplan-campaign.review_reason_help">
<input value={reason} maxLength={4000} disabled={saving || disabled} onChange={event => {
setReason(event.target.value); onReasonChange(event.target.value);
}} />
</FormField>
<Button variant="primary" disabled={saving || disabled || reasonRequired && !reason.trim()} onClick={() => void onAccept(reason)}>
{saving ? "i18n:govoplan-campaign.review_saving_decision" : "i18n:govoplan-campaign.review_accept_next"}
</Button>
</>;
}
function singleMessageSendDisabledReason(
row: Record<string, unknown>,
canStartSingleMessageSend: boolean
@@ -247,7 +252,7 @@ function builtMessageMetaItems(row: Record<string, unknown>) {
function builtMessageAttachments(
row: Record<string, unknown>
): CampaignMessagePreviewAttachment[] {
return asArray(row.attachments).flatMap((value, index) => {
return asArray(row.attachments).flatMap<CampaignMessagePreviewAttachment>((value, index) => {
const attachment = asRecord(value);
const zipProtection = zipProtectionFromBuiltAttachment(attachment);
const managedMatches = asArray(attachment.managed_matches).map(asRecord);