ui(alerts): applyFireModeChange — clear mode-specific fields on toggle

Prevents stale COUNT_IN_WINDOW threshold/windowSeconds from surviving
PER_EXCHANGE save (would trip the Task 3.3 server-side validator).
Also forces reNotifyMinutes=0 and forDurationSeconds=0 when switching to
PER_EXCHANGE.

Turns green: form-state.test.ts#applyFireModeChange (3 tests).
This commit is contained in:
hsiegeln
2026-04-22 17:48:51 +02:00
parent 4d37dff9f8
commit 9960fd8c36
2 changed files with 36 additions and 3 deletions

View File

@@ -1,6 +1,7 @@
import { FormField, Input, Select } from '@cameleer/design-system';
import type { FormState } from '../form-state';
import { EXCHANGE_FIRE_MODE_OPTIONS } from '../../enums';
import { applyFireModeChange } from '../form-state';
import { EXCHANGE_FIRE_MODE_OPTIONS, type ExchangeFireMode } from '../../enums';
// ExchangeFilter.status is typed as `String` on the backend (no @Schema
// allowableValues yet) so options stay hand-typed. Follow-up: annotate the
@@ -23,7 +24,7 @@ export function ExchangeMatchForm({ form, setForm }: { form: FormState; setForm:
<FormField label="Fire mode">
<Select
value={(c.fireMode as string) ?? 'PER_EXCHANGE'}
onChange={(e) => patch({ fireMode: e.target.value })}
onChange={(e) => setForm(applyFireModeChange(form, e.target.value as ExchangeFireMode))}
options={EXCHANGE_FIRE_MODE_OPTIONS}
/>
</FormField>

View File

@@ -3,7 +3,7 @@ import type {
AlertRuleResponse,
AlertCondition,
} from '../../../api/queries/alertRules';
import type { ConditionKind, Severity, TargetKind } from '../enums';
import type { ConditionKind, ExchangeFireMode, Severity, TargetKind } from '../enums';
export type WizardStep = 'scope' | 'condition' | 'trigger' | 'notify' | 'review';
export const WIZARD_STEPS: WizardStep[] = ['scope', 'condition', 'trigger', 'notify', 'review'];
@@ -137,6 +137,38 @@ export function toRequest(f: FormState): AlertRuleRequest {
} as AlertRuleRequest;
}
/**
* Pure helper for the ExchangeMatchForm's Fire-mode <Select>. Guarantees state
* hygiene across toggles:
*
* - Switching to PER_EXCHANGE clears COUNT_IN_WINDOW-only condition fields
* (threshold, windowSeconds) AND forces top-level reNotifyMinutes = 0
* and forDurationSeconds = 0 — PER_EXCHANGE is exactly-once-per-exchange,
* so re-notify cadence and hold-duration are meaningless and must not leak
* into toRequest().
* - Switching back to COUNT_IN_WINDOW resets threshold/windowSeconds to 0
* (never restoring stale values from the previous COUNT_IN_WINDOW session).
*
* No-op for non-EXCHANGE_MATCH conditions. Returns a new form object.
*/
export function applyFireModeChange(form: FormState, newMode: ExchangeFireMode): FormState {
const c = form.condition as Record<string, unknown>;
if (c.kind !== 'EXCHANGE_MATCH') return form;
const base: FormState = {
...form,
condition: {
...c,
fireMode: newMode,
threshold: 0,
windowSeconds: 0,
} as FormState['condition'],
};
if (newMode === 'PER_EXCHANGE') {
return { ...base, reNotifyMinutes: 0, forDurationSeconds: 0 };
}
return base;
}
export function validateStep(step: WizardStep, f: FormState): string[] {
const errs: string[] = [];
if (step === 'scope') {