feat: govern attachment exceptions and ownership transfers
This commit is contained in:
@@ -156,6 +156,9 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
const [showAllReviewJobs, setShowAllReviewJobs] = useState(false);
|
||||
const [jobsLoadedKey, setJobsLoadedKey] = useState("");
|
||||
const [reviewedMessageKeys, setReviewedMessageKeys] = useState<Set<string>>(() => new Set());
|
||||
const [reviewIssueDecisions, setReviewIssueDecisions] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
const [newlyReviewedRequiredKeys, setNewlyReviewedRequiredKeys] = useState<Set<string>>(() => new Set());
|
||||
const [selectedBuiltIndex, setSelectedBuiltIndex] = useState<number | null>(null);
|
||||
const [singleSendConfirmIndex, setSingleSendConfirmIndex] = useState<number | null>(null);
|
||||
@@ -182,7 +185,7 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
const imapDiagnosticsRef = useRef<CampaignJobsResponse>(emptyCampaignJobsResponse());
|
||||
const [selectedDeliveryJobDetail, setSelectedDeliveryJobDetail] = useState<Record<string, unknown> | null>(null);
|
||||
const persistedReview = storedMessageReviewState(version);
|
||||
const persistedReviewKey = `${persistedReview.buildToken}|${persistedReview.inspectionComplete ? "1" : "0"}|${persistedReview.reviewedMessageKeys.join(",")}`;
|
||||
const persistedReviewKey = `${persistedReview.buildToken}|${persistedReview.inspectionComplete ? "1" : "0"}|${persistedReview.reviewedMessageKeys.join(",")}|${JSON.stringify(persistedReview.issueDecisions)}`;
|
||||
|
||||
useEffect(() => {
|
||||
setBuiltReviewRows([]);
|
||||
@@ -192,6 +195,7 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
setReviewQuery({ sort: null, filters: {} });
|
||||
setJobsLoadedKey("");
|
||||
setNewlyReviewedRequiredKeys(new Set());
|
||||
setReviewIssueDecisions({});
|
||||
setSelectedBuiltIndex(null);
|
||||
setMockResult(null);
|
||||
setSelectedMockMessage(null);
|
||||
@@ -215,6 +219,12 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
useEffect(() => {
|
||||
setMessageReviewComplete(persistedReview.inspectionComplete);
|
||||
setReviewedMessageKeys(new Set(persistedReview.reviewedMessageKeys));
|
||||
setReviewIssueDecisions(Object.fromEntries(
|
||||
persistedReview.issueDecisions.flatMap((item) => {
|
||||
const jobId = String(item.job_id ?? "").trim();
|
||||
return jobId ? [[jobId, String(item.reason ?? "")]] : [];
|
||||
})
|
||||
));
|
||||
}, [version?.id, persistedReviewKey]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1010,7 +1020,14 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
const reviewed = new Set<string>(reviewedMessageKeys);
|
||||
await updateCampaignReviewState(settings, campaignId, version.id, {
|
||||
inspection_complete: true,
|
||||
reviewed_message_keys: [...reviewed]
|
||||
reviewed_message_keys: [...reviewed],
|
||||
issue_decisions: Object.entries(reviewIssueDecisions).map(
|
||||
([jobId, reason]) => ({
|
||||
job_id: jobId,
|
||||
decision: "accept" as const,
|
||||
reason: reason.trim() || null
|
||||
})
|
||||
)
|
||||
});
|
||||
setReviewedMessageKeys(reviewed);
|
||||
setNewlyReviewedRequiredKeys(new Set());
|
||||
@@ -1063,15 +1080,6 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
if (index < 0) return;
|
||||
const reviewRow = filteredBuiltReviewRows[index] ?? row;
|
||||
const jobId = String(reviewRow.id ?? "");
|
||||
const reviewKey = String(reviewRow.review_key ?? builtMessageKey(reviewRow, index));
|
||||
setReviewedMessageKeys((current) => {
|
||||
const next = new Set(current);
|
||||
next.add(reviewKey);
|
||||
return next;
|
||||
});
|
||||
if (messageNeedsExplicitReview(reviewRow) && reviewRow.reviewed !== true) {
|
||||
setNewlyReviewedRequiredKeys((current) => new Set(current).add(reviewKey));
|
||||
}
|
||||
if (!jobId || reviewRow.resolved_recipients || reviewRow.attachments || reviewRow.issues) {
|
||||
setSelectedBuiltIndex(index);
|
||||
return;
|
||||
@@ -1091,6 +1099,35 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
}
|
||||
}
|
||||
|
||||
function acceptBuiltMessageReview(
|
||||
row: Record<string, unknown>,
|
||||
index: number,
|
||||
reasonRequired: boolean
|
||||
) {
|
||||
const jobId = String(row.id ?? "").trim();
|
||||
const reviewKey = String(
|
||||
row.review_key ?? builtMessageKey(row, index)
|
||||
);
|
||||
const reason = reviewIssueDecisions[jobId] ?? "";
|
||||
if (!jobId) {
|
||||
setError("The built message has no delivery job id.");
|
||||
return;
|
||||
}
|
||||
if (reasonRequired && !reason.trim()) {
|
||||
setError("Enter a reason for accepting the attachment exception.");
|
||||
return;
|
||||
}
|
||||
setReviewedMessageKeys((current) => new Set(current).add(reviewKey));
|
||||
setNewlyReviewedRequiredKeys((current) =>
|
||||
new Set(current).add(reviewKey)
|
||||
);
|
||||
setReviewIssueDecisions((current) => ({
|
||||
...current,
|
||||
[jobId]: reason
|
||||
}));
|
||||
setError("");
|
||||
}
|
||||
|
||||
async function sendSingleBuiltMessage() {
|
||||
if (!version || busy || singleSendConfirmIndex === null) return;
|
||||
const row = filteredBuiltReviewRows[singleSendConfirmIndex];
|
||||
@@ -1667,6 +1704,28 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
index={selectedBuiltIndex}
|
||||
canStartSingleMessageSend={canStartSingleMessageSend}
|
||||
singleMessageSendBusy={busy === "send"}
|
||||
reviewed={reviewedMessageKeys.has(String(
|
||||
selectedBuiltMessage.review_key
|
||||
?? builtMessageKey(selectedBuiltMessage, selectedBuiltIndex)
|
||||
))}
|
||||
reviewReason={reviewIssueDecisions[
|
||||
String(selectedBuiltMessage.id ?? "")
|
||||
] ?? ""}
|
||||
onReviewReasonChange={(value) => {
|
||||
const jobId = String(selectedBuiltMessage.id ?? "").trim();
|
||||
if (!jobId) return;
|
||||
setReviewIssueDecisions((current) => ({
|
||||
...current,
|
||||
[jobId]: value
|
||||
}));
|
||||
}}
|
||||
onAcceptReview={(reasonRequired) =>
|
||||
acceptBuiltMessageReview(
|
||||
selectedBuiltMessage,
|
||||
selectedBuiltIndex,
|
||||
reasonRequired
|
||||
)
|
||||
}
|
||||
onSelect={openBuiltMessageAtIndex}
|
||||
onSendSingle={(targetIndex) => {
|
||||
const target = filteredBuiltReviewRows[targetIndex];
|
||||
|
||||
Reference in New Issue
Block a user