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'; 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({ export function ReviewStep({
form, form,
setForm, setForm,
@@ -9,8 +29,14 @@ export function ReviewStep({
setForm?: (f: FormState) => void; setForm?: (f: FormState) => void;
}) { }) {
const req = toRequest(form); const req = toRequest(form);
const saveBlockReason = useMemo(() => computeSaveBlockReason(form), [form]);
return ( return (
<div style={{ display: 'grid', gap: 12, maxWidth: 720 }}> <div style={{ display: 'grid', gap: 12, maxWidth: 720 }}>
{saveBlockReason && (
<Alert variant="error" title="Rule cannot be saved yet">
{saveBlockReason}
</Alert>
)}
<div> <div>
<strong>Name:</strong> {form.name} <strong>Name:</strong> {form.name}
</div> </div>

View File

@@ -19,7 +19,7 @@ import { ScopeStep } from './ScopeStep';
import { ConditionStep } from './ConditionStep'; import { ConditionStep } from './ConditionStep';
import { TriggerStep } from './TriggerStep'; import { TriggerStep } from './TriggerStep';
import { NotifyStep } from './NotifyStep'; import { NotifyStep } from './NotifyStep';
import { ReviewStep } from './ReviewStep'; import { ReviewStep, computeSaveBlockReason } from './ReviewStep';
import { prefillFromPromotion, type PrefillWarning } from './promotion-prefill'; import { prefillFromPromotion, type PrefillWarning } from './promotion-prefill';
import { useCatalog } from '../../../api/queries/catalog'; import { useCatalog } from '../../../api/queries/catalog';
import { useOutboundConnections } from '../../../api/queries/admin/outboundConnections'; import { useOutboundConnections } from '../../../api/queries/admin/outboundConnections';
@@ -194,7 +194,11 @@ export default function RuleEditorWizard() {
Next Next
</Button> </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'} {isEdit ? 'Save changes' : 'Create rule'}
</Button> </Button>
)} )}