@habit.analytics/habit-claims-journey-components 1.1.13 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +646 -151
- package/dist/components/NavLink.d.ts +8 -0
- package/dist/components/form-configurator/CreateFieldDialog.d.ts +10 -0
- package/dist/components/form-configurator/FieldPropertiesDialog.d.ts +7 -0
- package/dist/components/form-configurator/FormConfigurator.d.ts +11 -0
- package/dist/components/form-configurator/JourneyBuilderWidget.d.ts +21 -0
- package/dist/components/form-configurator/fields/FieldsTab.d.ts +7 -0
- package/dist/components/form-configurator/init/InitTab.d.ts +3 -0
- package/dist/components/form-configurator/json/JsonTab.d.ts +3 -0
- package/dist/components/form-configurator/steps/StepEditor.d.ts +7 -0
- package/dist/components/form-configurator/steps/StepNavigationEditor.d.ts +7 -0
- package/dist/components/form-configurator/steps/StepsList.d.ts +3 -0
- package/dist/components/form-configurator/summary/SummaryPanelEditor.d.ts +3 -0
- package/dist/components/journey-builder/ConditionalEdge.d.ts +10 -0
- package/dist/components/journey-builder/CreateNodeDialog.d.ts +18 -0
- package/dist/components/journey-builder/EdgePropertiesEditor.d.ts +1 -0
- package/dist/components/journey-builder/FormPreviewPanel.d.ts +3 -0
- package/dist/components/journey-builder/GraphValidationPanel.d.ts +1 -0
- package/dist/components/journey-builder/GroupNode.d.ts +4 -0
- package/dist/components/journey-builder/ImportJourneyDialog.d.ts +1 -0
- package/dist/components/journey-builder/JourneyBuilder.d.ts +12 -0
- package/dist/components/journey-builder/JourneyBuilderWidget.d.ts +81 -0
- package/dist/components/journey-builder/JourneyCanvas.d.ts +4 -0
- package/dist/components/journey-builder/KeyboardShortcutsHelp.d.ts +6 -0
- package/dist/components/journey-builder/NodePropertiesEditor.d.ts +1 -0
- package/dist/components/journey-builder/NodeSearchPalette.d.ts +6 -0
- package/dist/components/journey-builder/PortalContext.d.ts +7 -0
- package/dist/components/journey-builder/PropertiesPanel.d.ts +1 -0
- package/dist/components/journey-builder/QuestionNode.d.ts +11 -0
- package/dist/components/journey-builder/SpecsCatalog.d.ts +17 -0
- package/dist/components/journey-builder/SpecsContext.d.ts +3 -0
- package/dist/components/journey-builder/ThemePanel.d.ts +1 -0
- package/dist/components/journey-builder/edge-editor/ConditionRow.d.ts +18 -0
- package/dist/components/journey-builder/edge-editor/ConditionTemplates.d.ts +6 -0
- package/dist/components/journey-builder/edge-editor/ConditionTestPanel.d.ts +9 -0
- package/dist/components/journey-builder/edge-editor/VisualConditionEditor.d.ts +15 -0
- package/dist/components/journey-builder/edge-editor/condition-utils.d.ts +24 -0
- package/dist/components/journey-builder/preview-widgets/BooleanWidget.d.ts +13 -0
- package/dist/components/journey-builder/preview-widgets/CurrencyWidget.d.ts +9 -0
- package/dist/components/journey-builder/preview-widgets/DateTimeWidget.d.ts +10 -0
- package/dist/components/journey-builder/preview-widgets/DateWidget.d.ts +10 -0
- package/dist/components/journey-builder/preview-widgets/EmailWidget.d.ts +10 -0
- package/dist/components/journey-builder/preview-widgets/LocationWidget.d.ts +9 -0
- package/dist/components/journey-builder/preview-widgets/PhoneWidget.d.ts +10 -0
- package/dist/components/journey-builder/preview-widgets/SelectWidget.d.ts +12 -0
- package/dist/components/journey-builder/preview-widgets/TextWidget.d.ts +10 -0
- package/dist/components/journey-builder/preview-widgets/TimeWidget.d.ts +14 -0
- package/dist/components/journey-builder/preview-widgets/UploadWidget.d.ts +12 -0
- package/dist/components/journey-builder/preview-widgets/WidgetRenderer.d.ts +12 -0
- package/dist/components/runner/AnswerHistoryList.d.ts +11 -0
- package/dist/components/runner/FormRunnerWidget.d.ts +41 -0
- package/dist/components/runner/GroupedFormScreen.d.ts +14 -0
- package/dist/components/ui/accordion.d.ts +7 -0
- package/dist/components/ui/alert-dialog.d.ts +20 -0
- package/dist/components/ui/alert.d.ts +8 -0
- package/dist/components/ui/aspect-ratio.d.ts +3 -0
- package/dist/components/ui/avatar.d.ts +6 -0
- package/dist/components/ui/badge.d.ts +9 -0
- package/dist/components/ui/breadcrumb.d.ts +19 -0
- package/dist/components/ui/button.d.ts +11 -0
- package/dist/components/ui/calendar.d.ts +8 -0
- package/dist/components/ui/card.d.ts +8 -0
- package/dist/components/ui/carousel.d.ts +18 -0
- package/dist/components/ui/chart.d.ts +62 -0
- package/dist/components/ui/checkbox.d.ts +4 -0
- package/dist/components/ui/collapsible.d.ts +5 -0
- package/dist/components/ui/command.d.ts +82 -0
- package/dist/components/ui/context-menu.d.ts +27 -0
- package/dist/components/ui/dialog.d.ts +19 -0
- package/dist/components/ui/drawer.d.ts +22 -0
- package/dist/components/ui/dropdown-menu.d.ts +27 -0
- package/dist/components/ui/form.d.ts +23 -0
- package/dist/components/ui/hover-card.d.ts +6 -0
- package/dist/components/ui/input-otp.d.ts +34 -0
- package/dist/components/ui/input.d.ts +3 -0
- package/dist/components/ui/label.d.ts +5 -0
- package/dist/components/ui/menubar.d.ts +33 -0
- package/dist/components/ui/navigation-menu.d.ts +12 -0
- package/dist/components/ui/pagination.d.ts +28 -0
- package/dist/components/ui/popover.d.ts +6 -0
- package/dist/components/ui/progress.d.ts +4 -0
- package/dist/components/ui/radio-group.d.ts +5 -0
- package/dist/components/ui/resizable.d.ts +23 -0
- package/dist/components/ui/scroll-area.d.ts +5 -0
- package/dist/components/ui/select.d.ts +13 -0
- package/dist/components/ui/separator.d.ts +4 -0
- package/dist/components/ui/sheet.d.ts +25 -0
- package/dist/components/ui/sidebar.d.ts +66 -0
- package/dist/components/ui/skeleton.d.ts +2 -0
- package/dist/components/ui/slider.d.ts +4 -0
- package/dist/components/ui/sonner.d.ts +4 -0
- package/dist/components/ui/switch.d.ts +4 -0
- package/dist/components/ui/table.d.ts +10 -0
- package/dist/components/ui/tabs.d.ts +7 -0
- package/dist/components/ui/textarea.d.ts +5 -0
- package/dist/components/ui/toast.d.ts +15 -0
- package/dist/components/ui/toaster.d.ts +1 -0
- package/dist/components/ui/toggle-group.d.ts +12 -0
- package/dist/components/ui/toggle.d.ts +12 -0
- package/dist/components/ui/tooltip.d.ts +7 -0
- package/dist/components/ui/use-toast.d.ts +2 -0
- package/dist/data/country-codes.d.ts +7 -0
- package/dist/hooks/use-mobile.d.ts +1 -0
- package/dist/hooks/use-toast.d.ts +44 -0
- package/dist/hooks/useKeyboardShortcuts.d.ts +20 -0
- package/dist/hooks/useThemePortalVars.d.ts +2 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/locales/en.d.ts +165 -0
- package/dist/i18n/locales/es.d.ts +3 -0
- package/dist/i18n/locales/pl.d.ts +3 -0
- package/dist/i18n/locales/pt.d.ts +3 -0
- package/dist/i18n/useTranslation.d.ts +19 -0
- package/dist/index.cjs +63 -129
- package/dist/index.d.ts +10 -0
- package/dist/index.mjs +22574 -28441
- package/dist/lib/format-answer.d.ts +23 -0
- package/dist/lib/graph-validation.d.ts +15 -0
- package/dist/lib/theme-css.d.ts +5 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/schemas/form-config.schema.json.d.ts +220 -0
- package/dist/schemas/index.d.ts +3 -0
- package/dist/schemas/journey-spec.schema.json.d.ts +49 -0
- package/dist/stores/formConfiguratorStore.d.ts +81 -0
- package/dist/stores/journeyBuilderStore.d.ts +99 -0
- package/dist/types/journey.d.ts +530 -0
- package/package.json +6 -7
- package/src/index.ts +0 -31
- package/src/types/journey.ts +0 -273
- package/src/types/json-logic-js.d.ts +0 -5
package/README.md
CHANGED
|
@@ -1,33 +1,36 @@
|
|
|
1
1
|
# Journey Builder & Form Runner
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A linear, step-based form configurator and a self-contained form runner, distributed as two embeddable React widgets.
|
|
4
4
|
|
|
5
|
-
**Builder** —
|
|
5
|
+
- **Builder (`JourneyBuilderWidget`)** — configure a journey as ordered **Steps → Rows → Fields**, define per-step **navigation rules** (JSON Logic) and optional **API calls**, plus a **Summary** panel and **Init** values. Outputs a portable `JourneySpec` (v2.0).
|
|
6
|
+
- **Runner (`FormRunnerWidget`)** — feeds that `JourneySpec` to a step-by-step engine that evaluates conditions, optionally calls APIs between steps (delegated to the host), collects answers, and renders a review/summary screen.
|
|
6
7
|
|
|
7
|
-
**
|
|
8
|
+
> **v2.0 — breaking change vs v1.x.** The visual graph canvas (React Flow / `@xyflow/react`) has been removed. Legacy v1.x journeys (`nodes`/`edges`) are still accepted and auto-converted on import for read-only consumption, but new journeys are authored against the **`form_config`** model described below.
|
|
8
9
|
|
|
9
10
|
---
|
|
10
11
|
|
|
11
12
|
## Architecture
|
|
12
13
|
|
|
13
14
|
```
|
|
14
|
-
|
|
15
|
-
│
|
|
16
|
-
│
|
|
17
|
-
│
|
|
18
|
-
│ │
|
|
19
|
-
│ │
|
|
20
|
-
│ │ specs
|
|
21
|
-
│ │
|
|
22
|
-
│ │
|
|
23
|
-
│ │
|
|
24
|
-
│ │
|
|
25
|
-
│ │
|
|
26
|
-
│
|
|
27
|
-
│
|
|
28
|
-
│
|
|
29
|
-
│
|
|
30
|
-
|
|
15
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
16
|
+
│ Host Application │
|
|
17
|
+
│ │
|
|
18
|
+
│ ┌──────────────────────────┐ ┌────────────────────────┐ │
|
|
19
|
+
│ │ JourneyBuilderWidget │ │ FormRunnerWidget │ │
|
|
20
|
+
│ │ │ │ │ │
|
|
21
|
+
│ │ specs ──► Catalog │ │ journey ──► Engine │ │
|
|
22
|
+
│ │ journey ──► Editor │ │ initialAnswers ──► │ │
|
|
23
|
+
│ │ Steps/Rows │ │ prefill │ │
|
|
24
|
+
│ │ Fields tab │ │ onStepApiCall ◄──── │ │
|
|
25
|
+
│ │ Init / JSON │ │ onStepApiError ◄──── │ │
|
|
26
|
+
│ │ Summary │ │ onFileSelect ◄──── │ │
|
|
27
|
+
│ │ onSave / onPublish ──► │ │ onSubmit(answers) ──► │ │
|
|
28
|
+
│ │ onChange ──► │ │ onSave (draft) ──► │ │
|
|
29
|
+
│ └──────────────────────────┘ └────────────────────────┘ │
|
|
30
|
+
│ │
|
|
31
|
+
│ Both consume `JourneySpec` + types from │
|
|
32
|
+
│ `src/types/journey.ts` │
|
|
33
|
+
└─────────────────────────────────────────────────────────────┘
|
|
31
34
|
```
|
|
32
35
|
|
|
33
36
|
---
|
|
@@ -37,69 +40,86 @@ A visual graph-based form builder and step-by-step form runner, built as two ind
|
|
|
37
40
|
| Layer | Technology |
|
|
38
41
|
|---|---|
|
|
39
42
|
| UI framework | React 18 + TypeScript |
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
| Styling | Tailwind CSS + shadcn/ui |
|
|
43
|
+
| State management | Zustand (scoped per widget instance, 50-step undo/redo, dirty tracking) |
|
|
44
|
+
| Condition engine | `json-logic-js` |
|
|
45
|
+
| Styling | Tailwind CSS (prefixed `cj-`) + scoped CSS variables (`--claims-journey-*`) |
|
|
44
46
|
| Build tool | Vite |
|
|
45
47
|
|
|
48
|
+
Peer deps: `react`, `react-dom`. The previous `@xyflow/react` peer dep has been **removed** in v2.0.
|
|
49
|
+
|
|
46
50
|
---
|
|
47
51
|
|
|
48
52
|
## Quick Start
|
|
49
53
|
|
|
50
54
|
```bash
|
|
51
|
-
# Clone and install
|
|
52
|
-
git clone <YOUR_GIT_URL>
|
|
53
|
-
cd journey-builder
|
|
54
55
|
npm install
|
|
55
|
-
|
|
56
|
-
# Start dev server
|
|
57
56
|
npm run dev
|
|
58
57
|
```
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
- `/` — Builder with sample
|
|
62
|
-
- `/runner` — Runner loaded from the builder's current
|
|
59
|
+
Demo app at `http://localhost:5173`:
|
|
60
|
+
- `/` — Builder with sample specs
|
|
61
|
+
- `/runner` — Runner loaded from the builder's current journey
|
|
63
62
|
|
|
64
63
|
---
|
|
65
64
|
|
|
66
65
|
## Installation (as a library)
|
|
67
66
|
|
|
68
67
|
```bash
|
|
69
|
-
npm install journey-
|
|
68
|
+
npm install habit-claims-journey-components
|
|
70
69
|
```
|
|
71
70
|
|
|
72
|
-
```
|
|
71
|
+
```tsx
|
|
73
72
|
import {
|
|
74
73
|
JourneyBuilderWidget,
|
|
75
74
|
FormRunnerWidget,
|
|
76
|
-
} from 'journey-
|
|
75
|
+
} from 'habit-claims-journey-components';
|
|
77
76
|
|
|
78
77
|
import type {
|
|
79
78
|
JourneySpec,
|
|
80
79
|
ClaimPropertySpec,
|
|
81
|
-
|
|
80
|
+
FormConfig,
|
|
81
|
+
EnrichedAnswer,
|
|
82
|
+
StepApiRequest,
|
|
83
|
+
InitApiRequest,
|
|
84
|
+
} from 'habit-claims-journey-components';
|
|
82
85
|
```
|
|
83
86
|
|
|
87
|
+
CSS is auto-injected — **do not import a separate `style.css`**.
|
|
88
|
+
|
|
89
|
+
The parent of `JourneyBuilderWidget` must have an explicit height (the widget fills `100%`).
|
|
90
|
+
|
|
91
|
+
See [INTEGRATION.md](./INTEGRATION.md) for full install methods and copy-paste setup.
|
|
92
|
+
|
|
84
93
|
---
|
|
85
94
|
|
|
86
95
|
## Usage
|
|
87
96
|
|
|
88
97
|
### JourneyBuilderWidget
|
|
89
98
|
|
|
90
|
-
The visual graph editor. Provide field specifications and receive the journey graph via callbacks.
|
|
91
|
-
|
|
92
99
|
```tsx
|
|
93
|
-
import { JourneyBuilderWidget } from 'journey-
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
import { JourneyBuilderWidget } from 'habit-claims-journey-components';
|
|
101
|
+
import type { ClaimPropertySpec, JourneySpec } from 'habit-claims-journey-components';
|
|
102
|
+
|
|
103
|
+
function BuilderPage({
|
|
104
|
+
specs,
|
|
105
|
+
existing,
|
|
106
|
+
}: {
|
|
107
|
+
specs: ClaimPropertySpec[];
|
|
108
|
+
existing?: JourneySpec | null;
|
|
109
|
+
}) {
|
|
96
110
|
return (
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
111
|
+
<div style={{ width: '100%', height: '100vh' }}>
|
|
112
|
+
<JourneyBuilderWidget
|
|
113
|
+
specs={specs}
|
|
114
|
+
journey={existing}
|
|
115
|
+
onSave={(spec) => saveDraft(spec)}
|
|
116
|
+
onPublish={(spec) => publish(spec)}
|
|
117
|
+
onChange={(spec) => console.log('changed', spec)}
|
|
118
|
+
onOpenRunner={() => navigate('/runner')}
|
|
119
|
+
readOnly={false}
|
|
120
|
+
googleMapsApiKey={import.meta.env.VITE_GOOGLE_MAPS_KEY}
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
103
123
|
);
|
|
104
124
|
}
|
|
105
125
|
```
|
|
@@ -108,25 +128,48 @@ function BuilderPage({ specs, existingJourney }) {
|
|
|
108
128
|
|
|
109
129
|
| Prop | Type | Required | Description |
|
|
110
130
|
|---|---|---|---|
|
|
111
|
-
| `specs` | `ClaimPropertySpec[]` |
|
|
112
|
-
| `journey` | `JourneySpec \| null` | — | Existing journey to load on mount |
|
|
113
|
-
| `onSave` | `(spec: JourneySpec) => void` | — |
|
|
114
|
-
| `
|
|
131
|
+
| `specs` | `ClaimPropertySpec[]` | — | Field specifications shown in the catalog and "Add field" dialogs |
|
|
132
|
+
| `journey` | `JourneySpec \| null` | — | Existing journey to load on mount. v1.x graph and v2.x `form_config` both accepted |
|
|
133
|
+
| `onSave` | `(spec: JourneySpec) => void` | — | Fired by the Save button. Marks the store as clean |
|
|
134
|
+
| `onPublish` | `(spec: JourneySpec) => void` | — | Fired by the Publish button |
|
|
135
|
+
| `onChange` | `(spec: JourneySpec) => void` | — | Fired on every config mutation (steps/rows/fields/navigation/summary/init/theme) |
|
|
136
|
+
| `onUnlockEdit` | `() => void` | — | Shown as "Unlock edit" when `readOnly` is `true` |
|
|
137
|
+
| `onOpenRunner` | `() => void` | — | Shown as a "Preview" button in the header |
|
|
138
|
+
| `readOnly` | `boolean` | — | Disables editing, hides destructive controls, surfaces the unlock action |
|
|
139
|
+
| `googleMapsApiKey` | `string` | — | Enables the Google Places address widget in previews |
|
|
140
|
+
|
|
141
|
+
The builder UI exposes four tabs: **Steps**, **Fields**, **Init**, **JSON**.
|
|
115
142
|
|
|
116
143
|
---
|
|
117
144
|
|
|
118
145
|
### FormRunnerWidget
|
|
119
146
|
|
|
120
|
-
The step-by-step form engine. Provide a journey spec and receive collected answers.
|
|
121
|
-
|
|
122
147
|
```tsx
|
|
123
|
-
import { FormRunnerWidget } from 'journey-
|
|
148
|
+
import { FormRunnerWidget } from 'habit-claims-journey-components';
|
|
149
|
+
import type { JourneySpec, StepApiRequest } from 'habit-claims-journey-components';
|
|
124
150
|
|
|
125
|
-
function RunnerPage({ journey }) {
|
|
151
|
+
function RunnerPage({ journey }: { journey: JourneySpec }) {
|
|
126
152
|
return (
|
|
127
153
|
<FormRunnerWidget
|
|
128
154
|
journey={journey}
|
|
155
|
+
initialAnswers={{ /* optional pre-fill */ }}
|
|
156
|
+
enrichedOutput
|
|
129
157
|
onSubmit={(answers) => submitToApi(answers)}
|
|
158
|
+
onSave={(partial) => saveDraft(partial)}
|
|
159
|
+
onFileSelect={async (file, key) => uploadToS3(file, key)}
|
|
160
|
+
onStepApiCall={async (req: StepApiRequest) => {
|
|
161
|
+
const res = await fetch(req.url, {
|
|
162
|
+
method: req.method,
|
|
163
|
+
headers: { 'Content-Type': 'application/json', ...req.headers },
|
|
164
|
+
body: req.body ? JSON.stringify(req.body) : undefined,
|
|
165
|
+
});
|
|
166
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
167
|
+
return res.json();
|
|
168
|
+
}}
|
|
169
|
+
onStepApiError={(err, req) => trackError(err, req)}
|
|
170
|
+
onInitApiCall={async (req) => (await fetch(req.url, { method: req.method, headers: req.headers })).json()}
|
|
171
|
+
googleMapsApiKey={import.meta.env.VITE_GOOGLE_MAPS_KEY}
|
|
172
|
+
fullScreen
|
|
130
173
|
/>
|
|
131
174
|
);
|
|
132
175
|
}
|
|
@@ -136,120 +179,271 @@ function RunnerPage({ journey }) {
|
|
|
136
179
|
|
|
137
180
|
| Prop | Type | Required | Description |
|
|
138
181
|
|---|---|---|---|
|
|
139
|
-
| `journey` | `JourneySpec` | ✅ | The journey
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
182
|
+
| `journey` | `JourneySpec` | ✅ | The journey to run. v1.x or v2.x |
|
|
183
|
+
| `onSubmit` | `(answers: Record<string, unknown>) => void` | — | Called when the user confirms the summary screen. Defaults to copying JSON to clipboard |
|
|
184
|
+
| `onSave` | `(partial: Record<string, unknown>) => void` | — | Called whenever the user reaches a checkpoint mid-journey, suitable for draft persistence |
|
|
185
|
+
| `onFileSelect` | `(file: File, questionKey: string) => Promise<string \| UploadResult \| void> \| void` | — | Host-side upload handler for upload widgets. Returns a URL or `{ url, name, ... }` |
|
|
186
|
+
| `onStepApiCall` | `(req: StepApiRequest) => Promise<unknown>` | — | **Recommended.** Delegates per-step API calls to the host (auth, CORS, retries, telemetry). Return value is stored as that step's `api_response` and is referenceable from later JSON Logic and the Summary |
|
|
187
|
+
| `onStepApiError` | `(err: unknown, req: StepApiRequest) => void` | — | Called when a step API call fails. When provided, the runner skips its default error toast. Fires regardless of the `on_error` policy. Exceptions thrown by this handler are caught and logged — they never crash the runner |
|
|
188
|
+
| `onInitApiCall` | `(req: InitApiRequest) => Promise<unknown>` | — | Delegates the optional init/prefill API call to the host |
|
|
189
|
+
| `initialAnswers` | `Record<string, unknown>` | — | Seeds the runner with existing values. The engine walks forward until it hits the first unanswered question, or straight to the summary if everything is filled |
|
|
190
|
+
| `enrichedOutput` | `boolean` | — | When `true`, `onSubmit` receives `{ [key]: { data, schema } }` instead of the flat answers map |
|
|
191
|
+
| `googleMapsApiKey` | `string` | — | Enables the Google Places address widget |
|
|
192
|
+
| `fullScreen` | `boolean` | — | Renders the runner full-viewport (otherwise it fills its parent) |
|
|
193
|
+
|
|
194
|
+
##### `StepApiRequest`
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
type StepApiRequest = {
|
|
198
|
+
step_id: string;
|
|
199
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
200
|
+
url: string;
|
|
201
|
+
headers: Record<string, string>;
|
|
202
|
+
body?: unknown; // already-interpolated JSON
|
|
203
|
+
answers: Record<string, unknown>; // current answers snapshot
|
|
204
|
+
};
|
|
205
|
+
```
|
|
142
206
|
|
|
143
|
-
|
|
207
|
+
##### `InitApiRequest`
|
|
144
208
|
|
|
145
|
-
|
|
209
|
+
```ts
|
|
210
|
+
type InitApiRequest = {
|
|
211
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
212
|
+
url: string;
|
|
213
|
+
headers?: Record<string, string>;
|
|
214
|
+
};
|
|
215
|
+
```
|
|
146
216
|
|
|
147
|
-
|
|
217
|
+
##### Step API `on_error` policy
|
|
148
218
|
|
|
149
|
-
|
|
219
|
+
Each step's `navigation.api_call.on_error` controls what happens when the call (or `onStepApiCall`) throws:
|
|
150
220
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
options: ClaimPropertySpecOption[] | null;
|
|
161
|
-
help_text: string | null;
|
|
162
|
-
placeholder: string | null;
|
|
163
|
-
// ...additional display fields
|
|
164
|
-
}
|
|
165
|
-
```
|
|
221
|
+
| Value | Behavior |
|
|
222
|
+
|---|---|
|
|
223
|
+
| `block` *(default)* | Stay on the current step, surface an error. `apiResponses[step_id]` is left untouched (preserves any previous successful response) |
|
|
224
|
+
| `continue` | Proceed using the step's navigation rules. `api_response` for this step stays unset; any previous successful value is preserved |
|
|
225
|
+
| `goto:<step_id>` | Jump straight to the given step (e.g. an error screen), bypassing rules |
|
|
226
|
+
|
|
227
|
+
`onStepApiError` is always called when present, regardless of policy. If the handler itself throws, the runner catches the error, logs it (`onStepApiError handler threw`), and still honors the configured `on_error` policy.
|
|
228
|
+
|
|
229
|
+
---
|
|
166
230
|
|
|
167
|
-
|
|
231
|
+
## Data Model — `JourneySpec` (v2.0)
|
|
168
232
|
|
|
169
|
-
|
|
233
|
+
A `JourneySpec` is a portable envelope that wraps a `form_config` plus journey-level metadata.
|
|
170
234
|
|
|
171
|
-
```
|
|
235
|
+
```ts
|
|
172
236
|
interface JourneySpec {
|
|
173
237
|
journey_id: string;
|
|
174
238
|
claimspec_id: string;
|
|
175
|
-
version: string;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
239
|
+
version: string; // "2.0.x" for the linear model
|
|
240
|
+
form_config: FormConfig; // ◀ the new authoring model
|
|
241
|
+
theme?: JourneyTheme;
|
|
242
|
+
mode?: JourneyMode;
|
|
243
|
+
|
|
244
|
+
// Legacy v1.x — preserved when re-exporting an imported v1 spec.
|
|
245
|
+
nodes?: JourneySpecNode[];
|
|
246
|
+
edges?: JourneySpecEdge[];
|
|
247
|
+
start_node_id?: string | null;
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### `FormConfig`
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
interface FormConfig {
|
|
255
|
+
steps: FormStep[]; // ordered
|
|
256
|
+
fields: FormField[]; // flat catalog, referenced by id
|
|
257
|
+
summary: SummaryPanelConfig;
|
|
258
|
+
init: InitConfig;
|
|
179
259
|
}
|
|
180
260
|
```
|
|
181
261
|
|
|
182
|
-
### `
|
|
183
|
-
|
|
184
|
-
```
|
|
185
|
-
interface
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
262
|
+
### `FormStep`
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
interface FormStep {
|
|
266
|
+
step_id: string;
|
|
267
|
+
title: string;
|
|
268
|
+
description?: string;
|
|
269
|
+
rows: FormRow[]; // each row has field_ids[]
|
|
270
|
+
navigation: {
|
|
271
|
+
api_call?: StepApiCall; // fired when leaving the step
|
|
272
|
+
rules: NavRule[]; // JSON Logic; first match wins
|
|
273
|
+
button_label?: string;
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### `NavRule`
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
interface NavRule {
|
|
282
|
+
rule_id: string;
|
|
283
|
+
condition: Record<string, unknown> | null; // null = always (default)
|
|
284
|
+
next_step_id: string; // or "__submit__"
|
|
285
|
+
priority?: number;
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### `StepApiCall`
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
interface StepApiCall {
|
|
293
|
+
enabled: boolean;
|
|
294
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
295
|
+
url: string;
|
|
296
|
+
headers?: Record<string, string>;
|
|
297
|
+
body_template?: string; // tokens like {{answers.foo}} interpolated by the runner
|
|
298
|
+
on_error?: 'block' | 'continue' | `goto:${string}`; // defaults to 'block'
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### `FormField`
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
interface FormField {
|
|
306
|
+
field_id: string;
|
|
307
|
+
namespace: string; // answer key
|
|
308
|
+
label: string;
|
|
309
|
+
labelOverride?: string;
|
|
310
|
+
specId?: string; // or "custom"
|
|
311
|
+
schema: string; // e.g. "v1_string", "v2_date-1"
|
|
312
|
+
options?: ClaimPropertySpecOption[] | null;
|
|
313
|
+
widget?: string; // override (e.g. "email", "textarea")
|
|
196
314
|
required?: boolean;
|
|
315
|
+
actor?: 'customer' | 'operator';
|
|
316
|
+
bindings?: { target_namespace: string }[];
|
|
317
|
+
uploadConfig?: UploadConfig;
|
|
318
|
+
allowFuture?: boolean;
|
|
319
|
+
placeholder?: string;
|
|
320
|
+
helpText?: string;
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### `SummaryPanelConfig`
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
interface SummaryPanelConfig {
|
|
328
|
+
title: string;
|
|
329
|
+
show_pricing?: boolean;
|
|
330
|
+
sections: SummarySection[];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
interface SummarySection {
|
|
334
|
+
section_id: string;
|
|
335
|
+
title: string;
|
|
336
|
+
visible_from_step?: number; // 1-based
|
|
337
|
+
until_step?: number; // 1-based
|
|
338
|
+
display_fields: SummaryDisplayField[];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
interface SummaryDisplayField {
|
|
342
|
+
display_id: string;
|
|
343
|
+
source: 'form_field' | 'api_response';
|
|
344
|
+
field_id?: string; // when source === 'form_field'
|
|
345
|
+
step_id?: string; // when source === 'api_response'
|
|
346
|
+
response_path?: string; // dot-path inside api_response
|
|
347
|
+
label: string;
|
|
348
|
+
format?: 'text' | 'date' | 'datetime' | 'currency' | 'number';
|
|
197
349
|
}
|
|
198
350
|
```
|
|
199
351
|
|
|
200
|
-
### `
|
|
352
|
+
### `InitConfig`
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
interface InitConfig {
|
|
356
|
+
values: Record<string, unknown>;
|
|
357
|
+
api?: {
|
|
358
|
+
enabled: boolean;
|
|
359
|
+
method: 'GET' | 'POST';
|
|
360
|
+
url: string;
|
|
361
|
+
headers?: Record<string, string>;
|
|
362
|
+
mapping?: Record<string, string>; // namespace -> response_path
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
```
|
|
201
366
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
367
|
+
### `JourneyTheme`
|
|
368
|
+
|
|
369
|
+
CSS variables injected on the runner/builder root, overriding `--claims-journey-*` defaults. Colors are **HSL triplets** (e.g. `"222 47% 11%"`).
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
interface JourneyTheme {
|
|
373
|
+
primaryColor?: string;
|
|
374
|
+
backgroundColor?: string;
|
|
375
|
+
cardColor?: string;
|
|
376
|
+
foregroundColor?: string;
|
|
377
|
+
fontFamilyHeading?: string;
|
|
378
|
+
fontFamilyBody?: string;
|
|
379
|
+
fontSizeBase?: number; // px
|
|
380
|
+
borderRadius?: number; // rem
|
|
381
|
+
spacing?: 'compact' | 'cozy' | 'spacious';
|
|
210
382
|
}
|
|
211
383
|
```
|
|
212
384
|
|
|
213
385
|
---
|
|
214
386
|
|
|
387
|
+
## JSON Schema validation
|
|
388
|
+
|
|
389
|
+
Draft-07 JSON Schemas describing `FormConfig` and `JourneySpec` v2 ship with the package and are re-exported as plain objects:
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
import Ajv from 'ajv';
|
|
393
|
+
import { formConfigSchema, journeySpecSchema } from 'habit-claims-journey-components';
|
|
394
|
+
|
|
395
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
396
|
+
ajv.addSchema(formConfigSchema); // resolved by $id from journeySpecSchema
|
|
397
|
+
const validate = ajv.compile(journeySpecSchema);
|
|
398
|
+
|
|
399
|
+
if (!validate(spec)) console.error(validate.errors);
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Schema sources:
|
|
403
|
+
- [`src/schemas/form-config.schema.json`](./src/schemas/form-config.schema.json)
|
|
404
|
+
- [`src/schemas/journey-spec.schema.json`](./src/schemas/journey-spec.schema.json)
|
|
405
|
+
|
|
406
|
+
See [docs/SCHEMAS.md](./docs/SCHEMAS.md) for the full reference of required fields, types, and enumerations.
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
215
410
|
## Supported Schema Types
|
|
216
411
|
|
|
217
412
|
| Schema | Widget | Validation |
|
|
218
413
|
|---|---|---|
|
|
219
414
|
| `v1_string` | Text input | — |
|
|
220
415
|
| `v2_single_option_select-1` | Dropdown select | Must pick an option |
|
|
221
|
-
| `v2_date-1` | Date picker | Valid date |
|
|
416
|
+
| `v2_date-1` | Date picker | Valid date; `allowFuture` controls future dates |
|
|
222
417
|
| `v2_currency-1` | Currency input | Numeric |
|
|
223
|
-
| `v2_single_asset_upload` | File upload |
|
|
418
|
+
| `v2_single_asset_upload` | File upload (via `onFileSelect`) | Type/size from `uploadConfig` |
|
|
224
419
|
| `v2_phone-1` | Phone with country code | Length check |
|
|
225
420
|
| `v2_email-1` | Email input | Format regex |
|
|
226
421
|
|
|
227
|
-
|
|
422
|
+
Fields can override the default widget via `widget` (e.g. a `v1_string` rendered as `"textarea"` or `"email"`).
|
|
228
423
|
|
|
229
424
|
---
|
|
230
425
|
|
|
231
426
|
## Builder Features
|
|
232
427
|
|
|
233
|
-
- **
|
|
234
|
-
- **
|
|
235
|
-
- **
|
|
236
|
-
- **
|
|
237
|
-
- **
|
|
238
|
-
- **
|
|
239
|
-
- **
|
|
240
|
-
- **
|
|
241
|
-
- **Import/Export** — full-fidelity JourneySpec JSON via file upload or paste
|
|
242
|
-
- **Path highlighting** — visual tracking during preview
|
|
428
|
+
- **Steps tab** — three-pane layout (Steps list / Step editor / Summary editor) with drag-and-drop rows and inline field reuse from the catalog
|
|
429
|
+
- **Fields tab** — global catalog with Used/Unused filter and quick-jump chips that take you straight to the Step using each field
|
|
430
|
+
- **Init tab** — static initial values + optional prefill API call mapping (`namespace → response_path`)
|
|
431
|
+
- **JSON tab** — full `JourneySpec` view with copy / download / paste-to-import escape hatch
|
|
432
|
+
- **Per-step API calls** — method, URL, headers, JSON body template (`{{answers.foo}}`), and `on_error` policy
|
|
433
|
+
- **Per-step navigation rules** — JSON Logic conditions, priority, `__submit__` target
|
|
434
|
+
- **Theme panel** — primary/background/card/foreground colors, fonts, base size, radius, spacing scale
|
|
435
|
+
- **Persistence** — dirty-state tracking, Save / Publish, 50-action undo/redo, read-only mode with unlock action
|
|
243
436
|
|
|
244
437
|
## Runner Features
|
|
245
438
|
|
|
246
|
-
- **Step-by-step navigation** with
|
|
247
|
-
- **
|
|
248
|
-
- **
|
|
249
|
-
- **
|
|
250
|
-
- **
|
|
251
|
-
- **
|
|
252
|
-
- **
|
|
439
|
+
- **Step-by-step navigation** with non-destructive back/forward and mid-journey draft persistence (via `initialAnswers`)
|
|
440
|
+
- **Auto-progression** through steps whose only effect is a navigation rule
|
|
441
|
+
- **JSON Logic** evaluation against `{ answers, api_responses }`
|
|
442
|
+
- **Host-delegated APIs** — per-step (`onStepApiCall`) and init (`onInitApiCall`), with three `on_error` policies
|
|
443
|
+
- **File upload** — async `onFileSelect` returning a URL or rich `UploadResult`
|
|
444
|
+
- **Schema-aware widgets** with mobile-first touch targets (48px min) and iOS-zoom-safe inputs
|
|
445
|
+
- **Vertical summary screen** rendering the configured `SummaryPanelConfig`
|
|
446
|
+
- **Enriched output** — `enrichedOutput` flag wraps each answer as `{ data, schema }`
|
|
253
447
|
|
|
254
448
|
---
|
|
255
449
|
|
|
@@ -257,40 +451,341 @@ Nodes support **widget overrides** — any field can adopt a different widget (e
|
|
|
257
451
|
|
|
258
452
|
```
|
|
259
453
|
src/
|
|
260
|
-
├── index.ts
|
|
261
|
-
├── types/journey.ts
|
|
454
|
+
├── index.ts # Barrel exports
|
|
455
|
+
├── types/journey.ts # Public types & helpers
|
|
262
456
|
├── components/
|
|
263
|
-
│ ├──
|
|
264
|
-
│ │ ├── JourneyBuilderWidget.tsx
|
|
265
|
-
│ │ ├──
|
|
266
|
-
│ │ ├──
|
|
267
|
-
│ │ ├──
|
|
268
|
-
│ │ ├──
|
|
269
|
-
│ │ ├──
|
|
270
|
-
│ │
|
|
271
|
-
│ │ ├── EdgePropertiesEditor.tsx
|
|
272
|
-
│ │ ├── ConditionalEdge.tsx # Custom edge with labels
|
|
273
|
-
│ │ ├── CreateNodeDialog.tsx # Custom field creation
|
|
274
|
-
│ │ ├── ImportJourneyDialog.tsx
|
|
275
|
-
│ │ ├── FormPreviewPanel.tsx # Inline preview
|
|
276
|
-
│ │ ├── GraphValidationPanel.tsx # Validation results
|
|
277
|
-
│ │ ├── edge-editor/ # Condition editing components
|
|
278
|
-
│ │ └── preview-widgets/ # Schema-specific input widgets
|
|
457
|
+
│ ├── form-configurator/ # ◀ v2.0 Builder
|
|
458
|
+
│ │ ├── JourneyBuilderWidget.tsx # Entry point
|
|
459
|
+
│ │ ├── FormConfigurator.tsx # Tabs + layout
|
|
460
|
+
│ │ ├── steps/ # Steps tab
|
|
461
|
+
│ │ ├── fields/ # Fields tab (Used/Unused filter)
|
|
462
|
+
│ │ ├── init/ # Init tab
|
|
463
|
+
│ │ ├── json/ # JSON tab
|
|
464
|
+
│ │ └── summary/ # Summary editor
|
|
279
465
|
│ ├── runner/
|
|
280
|
-
│ │
|
|
281
|
-
│
|
|
282
|
-
│ └──
|
|
466
|
+
│ │ └── FormRunnerWidget.tsx # ◀ Runner entry point
|
|
467
|
+
│ ├── journey-builder/ # Shared previews, theme panel, portal context
|
|
468
|
+
│ │ └── preview-widgets/ # Schema-specific widgets
|
|
469
|
+
│ └── ui/ # shadcn/ui primitives
|
|
283
470
|
├── stores/
|
|
284
|
-
│ └──
|
|
471
|
+
│ └── formConfiguratorStore.ts # Zustand store + legacy converter
|
|
285
472
|
├── lib/
|
|
286
|
-
│ └──
|
|
287
|
-
└── pages/
|
|
473
|
+
│ └── theme-css.ts # CSS-variable builder for JourneyTheme
|
|
474
|
+
└── pages/ # Demo app
|
|
288
475
|
├── Index.tsx
|
|
289
476
|
└── Runner.tsx
|
|
290
477
|
```
|
|
291
478
|
|
|
292
479
|
---
|
|
293
480
|
|
|
481
|
+
## Migration v1 → v2
|
|
482
|
+
|
|
483
|
+
v2.0 replaces the React Flow graph canvas with a linear **Steps → Rows → Fields** configurator. Legacy v1.x specs (`nodes`/`edges`/`start_node_id`) are still accepted on import and auto-converted to a `form_config`.
|
|
484
|
+
|
|
485
|
+
### Removed dependency
|
|
486
|
+
|
|
487
|
+
```diff
|
|
488
|
+
- "@xyflow/react": "^12.x" // no longer a peer dependency
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### CSS
|
|
492
|
+
|
|
493
|
+
```diff
|
|
494
|
+
- import 'habit-claims-journey-components/style.css';
|
|
495
|
+
+ // CSS is auto-injected by the bundle — no separate import needed
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### `JourneyBuilderWidget` props
|
|
499
|
+
|
|
500
|
+
| v1.x | v2.x | Notes |
|
|
501
|
+
|---|---|---|
|
|
502
|
+
| `specs` | `specs` | Unchanged |
|
|
503
|
+
| `journey` | `journey` | Accepts both v1 and v2 shapes |
|
|
504
|
+
| `onSave(spec)` | `onSave(spec)` | Now also marks the store as clean |
|
|
505
|
+
| — | `onPublish(spec)` | **New** — dedicated publish action |
|
|
506
|
+
| — | `onChange(spec)` | **New** — fires on every mutation |
|
|
507
|
+
| — | `onOpenRunner()` | **New** — header "Preview" button |
|
|
508
|
+
| — | `readOnly` | **New** — disables editing |
|
|
509
|
+
| — | `onUnlockEdit()` | **New** — shown when `readOnly` is on |
|
|
510
|
+
| — | `googleMapsApiKey` | **New** — enables Location widget previews |
|
|
511
|
+
| `onNodeClick`, `onEdgeClick`, `nodeTypes`, `edgeTypes` | — | **Removed** (no canvas) |
|
|
512
|
+
|
|
513
|
+
```diff
|
|
514
|
+
<JourneyBuilderWidget
|
|
515
|
+
specs={specs}
|
|
516
|
+
journey={existing}
|
|
517
|
+
onSave={spec => save(spec)}
|
|
518
|
+
+ onPublish={spec => publish(spec)}
|
|
519
|
+
+ onChange={spec => setDraft(spec)}
|
|
520
|
+
+ onOpenRunner={() => navigate('/runner')}
|
|
521
|
+
+ readOnly={false}
|
|
522
|
+
+ googleMapsApiKey={import.meta.env.VITE_GOOGLE_MAPS_KEY}
|
|
523
|
+
/>
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### `FormRunnerWidget` props
|
|
527
|
+
|
|
528
|
+
| v1.x | v2.x | Notes |
|
|
529
|
+
|---|---|---|
|
|
530
|
+
| `journey` | `journey` | Now drives `form_config` (or auto-converted legacy graph) |
|
|
531
|
+
| `onSubmit(answers)` | `onSubmit(answers)` | Set `enrichedOutput` for `{ data, schema }` shape |
|
|
532
|
+
| `onFileSelect(file, key)` | `onFileSelect(file, key)` | May now return `UploadResult` `{ url, filename, mimetype?, size? }` |
|
|
533
|
+
| `onApiCall(req)` | **`onStepApiCall(req: StepApiRequest)`** | Renamed; request includes `step_id` and `answers` snapshot |
|
|
534
|
+
| — | `onStepApiError(err, req)` | **New** — suppresses the default error toast; safe even if it throws |
|
|
535
|
+
| — | `onInitApiCall(req: InitApiRequest)` | **New** — host-delegated init/prefill call |
|
|
536
|
+
| — | `onSave(partial)` | **New** — mid-journey draft persistence |
|
|
537
|
+
| `initialValues` | **`initialAnswers`** | Renamed for consistency with the engine |
|
|
538
|
+
| — | `enrichedOutput` | **New** — wraps every answer as `{ data, schema }` |
|
|
539
|
+
| — | `fullScreen` | **New** — viewport-fill layout |
|
|
540
|
+
| `mode`, `groups` | — | Ignored; layout is now driven by `form_config.steps`/`rows` |
|
|
541
|
+
|
|
542
|
+
```diff
|
|
543
|
+
<FormRunnerWidget
|
|
544
|
+
journey={journey}
|
|
545
|
+
- initialValues={draft}
|
|
546
|
+
+ initialAnswers={draft}
|
|
547
|
+
+ enrichedOutput
|
|
548
|
+
- onApiCall={async (req) => fetchJson(req.url, req)}
|
|
549
|
+
+ onStepApiCall={async (req) => fetchJson(req.url, req)}
|
|
550
|
+
+ onStepApiError={(err, req) => logger.warn(err, req)}
|
|
551
|
+
+ onInitApiCall={async (req) => fetchJson(req.url, req)}
|
|
552
|
+
+ onSave={(partial) => saveDraft(partial)}
|
|
553
|
+
onSubmit={(answers) => submit(answers)}
|
|
554
|
+
/>
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### `JourneySpec` shape
|
|
558
|
+
|
|
559
|
+
```diff
|
|
560
|
+
{
|
|
561
|
+
"journey_id": "j-001",
|
|
562
|
+
"claimspec_id": "cs-001",
|
|
563
|
+
- "version": "1.0",
|
|
564
|
+
- "nodes": [ /* JourneySpecNode[] */ ],
|
|
565
|
+
- "edges": [ /* JourneySpecEdge[] */ ],
|
|
566
|
+
- "start_node_id": "node-1",
|
|
567
|
+
- "mode": "sequential",
|
|
568
|
+
- "groups": []
|
|
569
|
+
+ "version": "2.0",
|
|
570
|
+
+ "form_config": {
|
|
571
|
+
+ "steps": [ /* FormStep[] — rows -> field_ids */ ],
|
|
572
|
+
+ "fields": [ /* FormField[] — flat catalog */ ],
|
|
573
|
+
+ "summary": { "title": "Review", "sections": [] },
|
|
574
|
+
+ "init": { "values": {} }
|
|
575
|
+
+ }
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
Importing a v1 spec is transparent: the store converts `nodes`/`edges` to `form_config` on load. Re-exporting preserves the legacy fields for downstream consumers that still read them.
|
|
580
|
+
|
|
581
|
+
### Step API errors
|
|
582
|
+
|
|
583
|
+
v1 always blocked navigation on a failing API call. v2 introduces an explicit per-step policy on `navigation.api_call.on_error`:
|
|
584
|
+
|
|
585
|
+
| Value | Behavior |
|
|
586
|
+
|---|---|
|
|
587
|
+
| `block` *(default)* | Stay on the step; `apiResponses[step_id]` is preserved |
|
|
588
|
+
| `continue` | Run navigation rules; `api_response` for this step stays unset |
|
|
589
|
+
| `goto:<step_id>` | Jump to the named step (e.g. an error screen) |
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## Full Examples
|
|
594
|
+
|
|
595
|
+
### Minimal `FormConfig`
|
|
596
|
+
|
|
597
|
+
A two-step journey with a select that branches to a follow-up step or skips to submit.
|
|
598
|
+
|
|
599
|
+
```json
|
|
600
|
+
{
|
|
601
|
+
"steps": [
|
|
602
|
+
{
|
|
603
|
+
"step_id": "step-vehicle",
|
|
604
|
+
"title": "Your vehicle",
|
|
605
|
+
"rows": [
|
|
606
|
+
{ "row_id": "r1", "layout": "vertical", "field_ids": ["f-plate", "f-has-damage"] }
|
|
607
|
+
],
|
|
608
|
+
"navigation": {
|
|
609
|
+
"rules": [
|
|
610
|
+
{
|
|
611
|
+
"rule_id": "rule-damage",
|
|
612
|
+
"condition": { "==": [{ "var": "answers.vehicle.has_damage" }, "yes"] },
|
|
613
|
+
"next_step_id": "step-damage",
|
|
614
|
+
"priority": 10
|
|
615
|
+
},
|
|
616
|
+
{ "rule_id": "rule-default", "condition": null, "next_step_id": "__submit__" }
|
|
617
|
+
]
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
"step_id": "step-damage",
|
|
622
|
+
"title": "Describe the damage",
|
|
623
|
+
"rows": [{ "row_id": "r2", "layout": "vertical", "field_ids": ["f-damage-notes"] }],
|
|
624
|
+
"navigation": {
|
|
625
|
+
"rules": [{ "rule_id": "r-end", "condition": null, "next_step_id": "__submit__" }]
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
],
|
|
629
|
+
"fields": [
|
|
630
|
+
{ "field_id": "f-plate", "namespace": "vehicle.plate", "label": "License plate", "schema": "v1_string", "required": true },
|
|
631
|
+
{
|
|
632
|
+
"field_id": "f-has-damage",
|
|
633
|
+
"namespace": "vehicle.has_damage",
|
|
634
|
+
"label": "Is the vehicle damaged?",
|
|
635
|
+
"schema": "v2_single_option_select-1",
|
|
636
|
+
"required": true,
|
|
637
|
+
"options": [
|
|
638
|
+
{ "data": "yes", "label": "Yes" },
|
|
639
|
+
{ "data": "no", "label": "No" }
|
|
640
|
+
]
|
|
641
|
+
},
|
|
642
|
+
{ "field_id": "f-damage-notes", "namespace": "vehicle.damage_notes", "label": "Damage description", "schema": "v1_string", "widget": "textarea" }
|
|
643
|
+
],
|
|
644
|
+
"summary": {
|
|
645
|
+
"title": "Review your claim",
|
|
646
|
+
"sections": [
|
|
647
|
+
{
|
|
648
|
+
"section_id": "sec-vehicle",
|
|
649
|
+
"title": "Vehicle",
|
|
650
|
+
"display_fields": [
|
|
651
|
+
{ "display_id": "d-plate", "source": "form_field", "field_id": "f-plate", "label": "Plate" },
|
|
652
|
+
{ "display_id": "d-damage", "source": "form_field", "field_id": "f-damage-notes", "label": "Damage" }
|
|
653
|
+
]
|
|
654
|
+
}
|
|
655
|
+
]
|
|
656
|
+
},
|
|
657
|
+
"init": { "values": { "vehicle.country": "BR" } }
|
|
658
|
+
}
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### Full `JourneySpec` with API call + init prefill
|
|
662
|
+
|
|
663
|
+
Per-step `api_call` with `on_error: "goto:..."`, a summary section reading from `api_response`, and an `init.api` prefill.
|
|
664
|
+
|
|
665
|
+
```json
|
|
666
|
+
{
|
|
667
|
+
"journey_id": "j-auto-claim-001",
|
|
668
|
+
"claimspec_id": "cs-auto-1",
|
|
669
|
+
"version": "2.0",
|
|
670
|
+
"form_config": {
|
|
671
|
+
"steps": [
|
|
672
|
+
{
|
|
673
|
+
"step_id": "step-id",
|
|
674
|
+
"title": "Find your policy",
|
|
675
|
+
"rows": [{ "row_id": "r1", "layout": "horizontal", "field_ids": ["f-doc"] }],
|
|
676
|
+
"navigation": {
|
|
677
|
+
"api_call": {
|
|
678
|
+
"enabled": true,
|
|
679
|
+
"method": "POST",
|
|
680
|
+
"url": "https://api.example.com/policies/lookup",
|
|
681
|
+
"headers": { "x-tenant": "acme" },
|
|
682
|
+
"body_template": "{\"document\":\"{{answers.customer.document}}\"}",
|
|
683
|
+
"on_error": "goto:step-not-found"
|
|
684
|
+
},
|
|
685
|
+
"rules": [
|
|
686
|
+
{ "rule_id": "r-ok", "condition": null, "next_step_id": "step-confirm" }
|
|
687
|
+
]
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
"step_id": "step-confirm",
|
|
692
|
+
"title": "Confirm your policy",
|
|
693
|
+
"rows": [{ "row_id": "r2", "layout": "vertical", "field_ids": ["f-accept"] }],
|
|
694
|
+
"navigation": {
|
|
695
|
+
"rules": [{ "rule_id": "r-end", "condition": null, "next_step_id": "__submit__" }]
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
"step_id": "step-not-found",
|
|
700
|
+
"title": "We couldn't find your policy",
|
|
701
|
+
"rows": [],
|
|
702
|
+
"navigation": { "rules": [] }
|
|
703
|
+
}
|
|
704
|
+
],
|
|
705
|
+
"fields": [
|
|
706
|
+
{ "field_id": "f-doc", "namespace": "customer.document", "label": "Document ID", "schema": "v1_string", "required": true },
|
|
707
|
+
{ "field_id": "f-accept", "namespace": "consent.accepted", "label": "I confirm the details above", "schema": "v2_single_option_select-1", "required": true,
|
|
708
|
+
"options": [{ "data": "yes", "label": "Yes" }] }
|
|
709
|
+
],
|
|
710
|
+
"summary": {
|
|
711
|
+
"title": "Review",
|
|
712
|
+
"sections": [
|
|
713
|
+
{
|
|
714
|
+
"section_id": "sec-policy",
|
|
715
|
+
"title": "Policy",
|
|
716
|
+
"display_fields": [
|
|
717
|
+
{ "display_id": "d-holder", "source": "api_response", "step_id": "step-id", "response_path": "policy.holder.name", "label": "Holder" },
|
|
718
|
+
{ "display_id": "d-plate", "source": "api_response", "step_id": "step-id", "response_path": "policy.vehicle.plate", "label": "Plate" }
|
|
719
|
+
]
|
|
720
|
+
}
|
|
721
|
+
]
|
|
722
|
+
},
|
|
723
|
+
"init": {
|
|
724
|
+
"values": {},
|
|
725
|
+
"api": {
|
|
726
|
+
"enabled": true,
|
|
727
|
+
"method": "GET",
|
|
728
|
+
"url": "https://api.example.com/session/prefill",
|
|
729
|
+
"headers": { "x-tenant": "acme" },
|
|
730
|
+
"mapping": {
|
|
731
|
+
"customer.full_name": "user.name",
|
|
732
|
+
"customer.email": "user.email"
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
"theme": {
|
|
738
|
+
"primaryColor": "222 47% 11%",
|
|
739
|
+
"fontFamilyHeading": "Poppins, sans-serif",
|
|
740
|
+
"fontFamilyBody": "Roboto, sans-serif",
|
|
741
|
+
"borderRadius": 0.75,
|
|
742
|
+
"spacing": "cozy"
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
### Handling `StepApiRequest` on the host
|
|
748
|
+
|
|
749
|
+
The runner builds a `StepApiRequest` whenever a step has an enabled `api_call`. `body` is already interpolated from `body_template` against the live `answers`; `answers` is a snapshot for host-side enrichment (auth, tenant context, etc.).
|
|
750
|
+
|
|
751
|
+
```ts
|
|
752
|
+
import type { StepApiRequest } from 'habit-claims-journey-components';
|
|
753
|
+
|
|
754
|
+
async function onStepApiCall(req: StepApiRequest) {
|
|
755
|
+
// req = { step_id, method, url, headers, body?, answers }
|
|
756
|
+
const res = await fetch(req.url, {
|
|
757
|
+
method: req.method,
|
|
758
|
+
headers: {
|
|
759
|
+
'Content-Type': 'application/json',
|
|
760
|
+
authorization: `Bearer ${getToken()}`,
|
|
761
|
+
...req.headers,
|
|
762
|
+
},
|
|
763
|
+
body: req.body !== undefined ? JSON.stringify(req.body) : undefined,
|
|
764
|
+
});
|
|
765
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
766
|
+
// The returned value is stored as api_responses[req.step_id]
|
|
767
|
+
// and is referenceable from later JSON Logic rules and the Summary.
|
|
768
|
+
return res.json();
|
|
769
|
+
}
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
### Handling `InitApiRequest` on the host
|
|
773
|
+
|
|
774
|
+
Fired once on runner mount when `form_config.init.api.enabled` is `true`. The response is walked through `init.api.mapping` (namespace → dot-path) to seed `answers` before the first step renders.
|
|
775
|
+
|
|
776
|
+
```ts
|
|
777
|
+
import type { InitApiRequest } from 'habit-claims-journey-components';
|
|
778
|
+
|
|
779
|
+
async function onInitApiCall(req: InitApiRequest) {
|
|
780
|
+
// req = { method, url, headers? }
|
|
781
|
+
const res = await fetch(req.url, { method: req.method, headers: req.headers });
|
|
782
|
+
if (!res.ok) throw new Error(`Init HTTP ${res.status}`);
|
|
783
|
+
return res.json();
|
|
784
|
+
}
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
---
|
|
788
|
+
|
|
294
789
|
## License
|
|
295
790
|
|
|
296
791
|
MIT
|