fix(campaigns): start_date modal + hide future-campaign tests from queue
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Backend: activate endpoint returns 409 with structured warning when start_date is in the future; accepts force=true to bypass. test_crud_service: always excludes tests from draft campaigns with future start_date so they do not appear in the team queue prematurely. Frontend: catches 409 on activate and shows amber confirmation modal with Keep scheduled / Activate now anyway options. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -134,8 +134,12 @@ export async function removeTestFromCampaign(
|
||||
}
|
||||
|
||||
/** Activate a campaign. */
|
||||
export async function activateCampaign(campaignId: string): Promise<Campaign> {
|
||||
const { data } = await client.post<Campaign>(`/campaigns/${campaignId}/activate`);
|
||||
export async function activateCampaign(
|
||||
campaignId: string,
|
||||
options?: { force?: boolean },
|
||||
): Promise<Campaign> {
|
||||
const params = options?.force ? "?force=true" : "";
|
||||
const { data } = await client.post<Campaign>(`/campaigns/${campaignId}/activate${params}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,8 @@ export default function CampaignDetailPage() {
|
||||
const [showAddTestModal, setShowAddTestModal] = useState(false);
|
||||
// 0 = hidden, 1 = first confirmation, 2 = ask about tests
|
||||
const [deleteStep, setDeleteStep] = useState<0 | 1 | 2>(0);
|
||||
// Start-date confirmation modal — shown when campaign has a future start_date
|
||||
const [startDateWarning, setStartDateWarning] = useState<string | null>(null);
|
||||
|
||||
const showToast = (message: string, type: "success" | "error") => {
|
||||
setToast({ message, type });
|
||||
@@ -89,12 +91,21 @@ export default function CampaignDetailPage() {
|
||||
});
|
||||
|
||||
const activateMutation = useMutation({
|
||||
mutationFn: () => activateCampaign(campaignId!),
|
||||
mutationFn: (force = false) => activateCampaign(campaignId!, force ? { force: true } : undefined),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["campaign", campaignId] });
|
||||
setStartDateWarning(null);
|
||||
showToast("Campaign activated", "success");
|
||||
},
|
||||
onError: (err: Error) => showToast(err.message, "error"),
|
||||
onError: (err: unknown) => {
|
||||
// 409 = future start_date warning → show confirmation modal instead of toast
|
||||
const axiosErr = err as { response?: { status?: number; data?: { message?: string; start_date?: string } } };
|
||||
if (axiosErr?.response?.status === 409 && axiosErr.response.data?.message) {
|
||||
setStartDateWarning(axiosErr.response.data.message);
|
||||
} else {
|
||||
showToast((err as Error).message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const completeMutation = useMutation({
|
||||
@@ -297,7 +308,7 @@ export default function CampaignDetailPage() {
|
||||
)}
|
||||
{canManage && campaign.status === "draft" && (
|
||||
<button
|
||||
onClick={() => activateMutation.mutate()}
|
||||
onClick={() => activateMutation.mutate(false)}
|
||||
disabled={activateMutation.isPending}
|
||||
className="flex items-center gap-1.5 rounded-lg bg-cyan-600 px-4 py-2 text-sm font-medium text-white hover:bg-cyan-500 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
@@ -666,6 +677,37 @@ export default function CampaignDetailPage() {
|
||||
onSuccess={() => showToast("Test added to campaign", "success")}
|
||||
/>
|
||||
|
||||
{/* Start-date confirmation modal */}
|
||||
{startDateWarning && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm">
|
||||
<div className="mx-4 w-full max-w-md rounded-xl border border-amber-500/30 bg-gray-900 p-6 shadow-2xl">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="rounded-lg bg-amber-500/10 p-2">
|
||||
<Clock className="h-5 w-5 text-amber-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-white">Campaign not started yet</h3>
|
||||
</div>
|
||||
<p className="mb-6 text-sm text-gray-300 leading-relaxed">{startDateWarning}</p>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setStartDateWarning(null)}
|
||||
className="rounded-lg border border-gray-700 px-4 py-2 text-sm text-gray-400 hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
Keep scheduled
|
||||
</button>
|
||||
<button
|
||||
onClick={() => activateMutation.mutate(true)}
|
||||
disabled={activateMutation.isPending}
|
||||
className="flex items-center gap-1.5 rounded-lg bg-amber-600 px-4 py-2 text-sm font-medium text-white hover:bg-amber-500 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{activateMutation.isPending && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
Activate now anyway
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Toast notification */}
|
||||
{toast && (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user