ui(alerts): ReviewStep blocks save on empty webhooks+targets

Shows a warning banner and disables the Save button when a rule has
neither webhooks nor targets — would have been rejected at the server
edge (Task 3.3 validator), now caught earlier in the wizard with clear
reason.
This commit is contained in:
hsiegeln
2026-04-22 17:55:13 +02:00
parent 36cb93ecdd
commit f096365e05
2 changed files with 33 additions and 3 deletions

View File

@@ -1,6 +1,26 @@
import { Toggle } from '@cameleer/design-system';
import { useMemo } from 'react';
import { Alert, Toggle } from '@cameleer/design-system';
import { toRequest, type FormState } from './form-state';
/**
* Pure helper: returns a human-readable reason why saving should be blocked,
* or null when the rule is safe to save (from the wizard's perspective).
*
* Currently covers: a rule with no webhooks AND no targets would be rejected
* at the server edge (Task 3.3 validator) and would never notify anyone, so
* the wizard blocks it earlier with a clear reason. The helper is exported
* so `RuleEditorWizard` can also drive the Save button's `disabled` state
* off the same single source of truth.
*/
export function computeSaveBlockReason(form: FormState): string | null {
const noWebhooks = (form.webhooks ?? []).length === 0;
const noTargets = (form.targets ?? []).length === 0;
if (noWebhooks && noTargets) {
return 'Add at least one webhook or target \u2014 a rule with no recipients never notifies anyone.';
}
return null;
}
export function ReviewStep({
form,
setForm,
@@ -9,8 +29,14 @@ export function ReviewStep({
setForm?: (f: FormState) => void;
}) {
const req = toRequest(form);
const saveBlockReason = useMemo(() => computeSaveBlockReason(form), [form]);
return (
<div style={{ display: 'grid', gap: 12, maxWidth: 720 }}>
{saveBlockReason && (
<Alert variant="error" title="Rule cannot be saved yet">
{saveBlockReason}
</Alert>
)}
<div>
<strong>Name:</strong> {form.name}
</div>

View File

@@ -19,7 +19,7 @@ import { ScopeStep } from './ScopeStep';
import { ConditionStep } from './ConditionStep';
import { TriggerStep } from './TriggerStep';
import { NotifyStep } from './NotifyStep';
import { ReviewStep } from './ReviewStep';
import { ReviewStep, computeSaveBlockReason } from './ReviewStep';
import { prefillFromPromotion, type PrefillWarning } from './promotion-prefill';
import { useCatalog } from '../../../api/queries/catalog';
import { useOutboundConnections } from '../../../api/queries/admin/outboundConnections';
@@ -194,7 +194,11 @@ export default function RuleEditorWizard() {
Next
</Button>
) : (
<Button variant="primary" onClick={onSave} disabled={create.isPending || update.isPending}>
<Button
variant="primary"
onClick={onSave}
disabled={create.isPending || update.isPending || computeSaveBlockReason(form) !== null}
>
{isEdit ? 'Save changes' : 'Create rule'}
</Button>
)}