ALL-1874 PR-4: Frontend — RuleEditorModal with create/edit form and test-rule dry-run panel
queuedAgent: will-engineer
Priority: 3
Branch:
wilbo/all-1874-rule-editor-modalPR: #12487
Linear: ALL-1874
## Context
Linear issue ALL-1874. Depends on PR-3 (frontend list tab) and PR-2 (testRule backend query).
## Key files
- `apps/dashboard/components/pages/DataReviewPage/RulesTab.tsx` — stub "Add rule" button from PR-3; wire to RuleEditorModal
- `apps/dashboard/components/pages/DataReviewPage/RuleEditorModal.tsx` — NEW FILE, create here
- `apps/dashboard/hooks/useTestRule.ts` — NEW FILE, wraps the testRule query
- Any existing form primitives: check `apps/dashboard/components/ui/` or Edges component library for Select, Input, Slider, Form
- GraphQL: ensure testRule query is in codegen output (after PR-2 lands)
## UI spec (RuleEditorModal)
### Fields
1. **Name** — text input, required
2. **Description** — textarea, optional
3. **Issue Type** — Select with common values: GEOCODE_FAILURE, FORMAT_WARNING, MISSING_REQUIRED_FIELD, DUPLICATE_RECORD, plus free-text fallback
4. **Field** — text input, optional (dotted path e.g. address.postalCode)
5. **Matcher** — type Select (regex, equals, jsonPath, contains, startsWith, endsWith, range, oneOf) + dynamic config sub-form per type:
- regex: pattern input + flags input + live validity badge
- equals: value input
- jsonPath: path input + expected input
- contains/startsWith/endsWith: value input
- range: min/max number inputs
- oneOf: multi-value chip input
6. **Transform** — Add transform button; each transform row shows type selector + config inputs (same types as PR-1 new types: padLeft, replace, trim, setField, uppercase, lowercase, formatPhone, formatPostalCode, concatenate, mapValue). Support multiple transforms (array, ordered list with move-up/move-down)
7. **Confidence threshold** — number input (0.0-1.0), default 0.9
8. **Priority** — number input, show existing rules priorities as hint
9. **Enabled** — checkbox
### Test-rule dry-run panel (inside modal, below form)
- Paste JSON text area for sampleRecord.normalizedData (JSON object)
- "Test" button: fires testRule GraphQL query with current form values + sample record
- Result: shows Matched: Yes/No, Before fields, After fields side-by-side
- Shows validation errors if matcher/transform form is invalid before sending
### Save behavior
- Create mode: calls createResolutionRule mutation
- Edit mode: calls updateResolutionRule mutation
- On success: close modal, refetch resolutionRules list
- On Zod/GraphQL error from server: show inline error near relevant field
## Acceptance test
- RuleEditorModal renders without crash in Storybook with both create and edit props
- Selecting regex matcher shows pattern + flags inputs; selecting oneOf shows multi-value chip input
- Test-rule panel with valid JSON fires the testRule query (mocked) and renders before/after
- createResolutionRule mutation called on save in create mode with correct JSON shape for all matcher types
## Notes
- Depends on PR-2 (testRule query SDL) and PR-3 (RulesTab wire-up)
- Platform-default rules (workspaceId==null): modal opened in read-only/disable mode for non-super-admin users
- Keep form state in local React state (react-hook-form or plain useState pattern — follow existing modal patterns in the codebase)
- Rule cascading (rules triggering other rules) is scoped out
Event Timeline
created
status_change
queued → in_progress
failed
lease expired — re-queued for retry
in_progress → queued
status_change
queued → in_progress
progress
Lint fix pushed (89acef7178) to PR #12487 after sub-agent dropped exec context. CI re-running.
failed
lease expired — re-queued for retry
in_progress → queued