@explorer02/cfm-survey-sdk 0.2.1 → 0.2.2
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/dist/cli/index.js +18 -18
- package/dist/cli/index.mjs +14 -14
- package/dist/index.d.mts +465 -580
- package/dist/index.d.ts +465 -580
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +6 -4
- package/postinstall.js +96 -69
- package/templates/AGENT.md +14 -8
- package/templates/docs/00-integration/answer-logic-and-navigation.md +50 -0
- package/templates/docs/00-integration/aws-deploy.md +281 -0
- package/templates/docs/00-integration/client-integration-guide.md +351 -0
- package/templates/docs/00-integration/component-checklist.md +45 -22
- package/templates/docs/00-integration/constraints.md +49 -4
- package/templates/docs/00-integration/custom-field-logic-and-navigation.md +174 -0
- package/templates/docs/00-integration/display-logic-and-navigation.md +49 -0
- package/templates/docs/00-integration/end-page-logic.md +64 -0
- package/templates/docs/00-integration/partial-save-and-recovery.md +74 -0
- package/templates/docs/00-integration/placeholders-and-tokens.md +116 -0
- package/templates/docs/00-integration/progress.md +61 -0
- package/templates/docs/00-integration/question-display-variants.md +65 -0
- package/templates/docs/00-integration/question-numbering.md +51 -0
- package/templates/docs/00-integration/question-type-sdk-matrix.md +242 -0
- package/templates/docs/00-integration/setup.md +84 -0
- package/templates/docs/00-integration/skip-logic-and-navigation.md +80 -0
- package/templates/docs/00-integration/survey-lifecycle-analytics.md +94 -0
- package/templates/docs/00-integration/useSurveySDK.md +151 -23
- package/templates/docs/01-components/01-survey-page.md +76 -54
- package/templates/docs/01-components/02-question.md +61 -58
- package/templates/docs/01-components/03-rating-scale.md +11 -1
- package/templates/docs/01-components/04-star-rating-scale.md +45 -0
- package/templates/docs/01-components/05-csat-matrix-scale.md +20 -11
- package/templates/docs/01-components/06-likert-matrix-scale.md +12 -3
- package/templates/docs/01-components/07-slider-matrix-scale.md +11 -2
- package/templates/docs/01-components/08-file-upload-scale.md +10 -1
- package/templates/docs/01-components/09-custom-slider-track.md +2 -2
- package/templates/docs/01-components/11-progress-bar.md +10 -4
- package/templates/docs/01-components/13-matrix-dropdown.md +3 -1
- package/templates/docs/01-components/14-intro-page.md +34 -0
- package/templates/docs/01-components/15-end-page.md +31 -0
- package/templates/docs/01-components/16-paused-page.md +27 -0
- package/templates/docs/01-components/17-heatmap-scale.md +75 -0
- package/templates/docs/01-components/18-rank-order-scale.md +100 -0
- package/templates/docs/01-components/README.md +42 -36
- package/templates/docs/02-reference/config-field-index.md +141 -139
- package/templates/docs/02-reference/exports.md +149 -0
- package/templates/docs/02-reference/question-types/01-rating.md +31 -32
- package/templates/docs/02-reference/question-types/04-csat.md +51 -42
- package/templates/docs/02-reference/question-types/05-rating-scale.md +16 -11
- package/templates/docs/02-reference/question-types/06-slider.md +2 -2
- package/templates/docs/02-reference/question-types/07-matrix-cfm.md +44 -30
- package/templates/docs/02-reference/question-types/08-matrix-csat.md +4 -26
- package/templates/docs/02-reference/question-types/09-matrix-rating.md +29 -13
- package/templates/docs/02-reference/question-types/10-slider-matrix.md +15 -4
- package/templates/docs/02-reference/question-types/11-file-upload.md +13 -2
- package/templates/docs/02-reference/question-types/12-heatmap.md +59 -0
- package/templates/docs/02-reference/question-types/13-rank-order.md +101 -0
- package/templates/docs/02-reference/question-types/README.md +86 -47
- package/templates/docs/02-reference/routing-table.md +33 -18
- package/templates/docs/02-reference/value-derivation.md +63 -0
- package/templates/docs/03-ui-specs/00-question-shell.md +19 -88
- package/templates/docs/03-ui-specs/01-rating.md +12 -54
- package/templates/docs/03-ui-specs/02-radio.md +21 -33
- package/templates/docs/03-ui-specs/03-text.md +2 -2
- package/templates/docs/03-ui-specs/04-csat.md +38 -33
- package/templates/docs/03-ui-specs/05-rating-scale.md +21 -18
- package/templates/docs/03-ui-specs/06-slider.md +3 -1
- package/templates/docs/03-ui-specs/07-matrix-cfm.md +24 -21
- package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +17 -45
- package/templates/docs/03-ui-specs/09-slider-matrix.md +1 -1
- package/templates/docs/03-ui-specs/10-file-upload.md +1 -1
- package/templates/docs/03-ui-specs/11-text-and-media.md +29 -29
- package/templates/docs/03-ui-specs/12-survey-chrome.md +20 -14
- package/templates/docs/03-ui-specs/13-heatmap.md +73 -0
- package/templates/docs/03-ui-specs/14-rank-order.md +59 -0
- package/templates/docs/03-ui-specs/README.md +13 -13
- package/templates/docs/03-ui-specs/shared/custom-slider-track.md +2 -2
- package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +5 -0
- package/templates/docs/MANIFEST.json +451 -69
- package/templates/docs/index.md +80 -29
- package/templates/docs/templates/Question.tsx +221 -154
- package/templates/docs/templates/deploy-to-aws.sh +114 -0
- package/templates/docs/templates/heatmapCoords.ts +58 -0
- package/templates/docs/templates/implementation_plan.md +108 -69
- package/templates/docs/templates/survey-inventory.schema.json +72 -0
- package/templates/docs/templates/verify-agent-build.sh +53 -0
- package/templates/docs/01-components/04-csat-scale.md +0 -40
- package/templates/docs/02-question-types/01-rating.md +0 -44
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# Client Integration Guide
|
|
2
|
+
|
|
3
|
+
> **Start here** after Phase 2 (instanceId). Single map of everything the client must wire to integrate all SDK features correctly.
|
|
4
|
+
|
|
5
|
+
Import only from the package root (`@explorer02/cfm-survey-sdk` or `@repo/sdk`). Never from `/src/` paths.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
```mermaid
|
|
10
|
+
flowchart TB
|
|
11
|
+
subgraph page [SurveyPage orchestrator]
|
|
12
|
+
Hook["useSurveySDK"]
|
|
13
|
+
PhaseRouter["Phase router\nloading / intro / content / end"]
|
|
14
|
+
Lifecycle["SCROLL + RECORD_QUESTION_VIEW effects"]
|
|
15
|
+
end
|
|
16
|
+
subgraph question [Question.tsx dispatcher]
|
|
17
|
+
Dispatch["QUESTION_TYPE switch"]
|
|
18
|
+
Visibility["getVisible*ForAnswers helpers"]
|
|
19
|
+
Placeholders["resolveSurveyQuestionPlaceholders"]
|
|
20
|
+
end
|
|
21
|
+
subgraph scales [Scale components]
|
|
22
|
+
Matrix["LikertMatrixScale / CsatMatrixScale / …"]
|
|
23
|
+
NPS["RatingScale / FileUploadScale / …"]
|
|
24
|
+
end
|
|
25
|
+
Hook --> PhaseRouter
|
|
26
|
+
PhaseRouter --> question
|
|
27
|
+
Dispatch --> scales
|
|
28
|
+
Visibility --> Dispatch
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| Layer | File | Responsibility |
|
|
32
|
+
|-------|------|----------------|
|
|
33
|
+
| Orchestrator | `SurveyPage.tsx` | Hook, phase routing, nav buttons, lifecycle effects, pass props to `Question` |
|
|
34
|
+
| Dispatcher | `Question.tsx` | Route on `question.type`; filter options/rows via visibility helpers; placeholder text |
|
|
35
|
+
| Scales | `*Scale.tsx` | Render one question type; store answers via `onSelect` callback |
|
|
36
|
+
|
|
37
|
+
Reference implementation: `apps/client/src/components/SurveyPage.tsx`.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Feature matrix — who wires what
|
|
42
|
+
|
|
43
|
+
| Feature | Client wires | SDK handles |
|
|
44
|
+
|---------|--------------|-------------|
|
|
45
|
+
| Fetch survey | `instanceId`, loading/error UI | `useFetchSurvey`, survey mapping |
|
|
46
|
+
| Submit | `onAction(NEXT/SUBMIT)`, submit loading UI | Validation, API submit, phase → `end`, end-page selection |
|
|
47
|
+
| End page logic | Render `state.specialPage`; pass `customFieldValues` when end pages reference CRM fields | Display-logic evaluation at submit; see [`end-page-logic.md`](end-page-logic.md) |
|
|
48
|
+
| Answers | `onAction(CHANGE)` with `payload.answerValue` | `state.answers`, sanitization on logic change |
|
|
49
|
+
| Skip logic | Nothing (read doc for UX expectations) | Path simulation, skip jumps, answer clearing |
|
|
50
|
+
| Display logic | Use `state.currentQuestions` (pre-filtered) | Question-level visibility, empty-page skip |
|
|
51
|
+
| Question display variants | Render `question.questionText` from `currentQuestions` (no client variant logic) | Conditional variant text via same logic engine |
|
|
52
|
+
| Answer logic UI | `getVisible*ForAnswers` + pass `customFieldValues` | Condition evaluation engine |
|
|
53
|
+
| Custom fields | Stable `customFieldValues` → hook **and** `Question` | Skip/display/validation/progress evaluation |
|
|
54
|
+
| Progress | Bind `state.progressPercentage` | Active-path denominator + earned credits |
|
|
55
|
+
| Question numbering | Render `question.questionNumber` from `currentQuestions` | Active-path ordinals (same denominator as progress); see [`question-numbering.md`](question-numbering.md) |
|
|
56
|
+
| Placeholders | Stable `placeholders` ref + `language` option | Token replacement at map time |
|
|
57
|
+
| Mark as started | `onAction(SCROLL)` once on first scroll | One-time API call (also fires on CHANGE) |
|
|
58
|
+
| Question activity | `onAction(RECORD_QUESTION_VIEW)` via IntersectionObserver | Deduped VIEWED API calls |
|
|
59
|
+
| Partial save | `savePartialResponse: true` option | Debounced partial POST after user interaction — see [`partial-save-and-recovery.md`](partial-save-and-recovery.md) |
|
|
60
|
+
| Placeholders (detail) | Stable ref + `resolveSurveyQuestionPlaceholders` | CRM tokens at map time; answer tokens at render — see [`placeholders-and-tokens.md`](placeholders-and-tokens.md) |
|
|
61
|
+
| Per-type integration | Read [`question-type-sdk-matrix.md`](question-type-sdk-matrix.md) before building scales | Answer shapes, visibility helpers, performance notes per type |
|
|
62
|
+
| Validation scroll | `id={question.id}` on question wrapper | Scroll-to-first-error on failed NEXT/SUBMIT |
|
|
63
|
+
|
|
64
|
+
Deep dives: [`useSurveySDK.md`](useSurveySDK.md) · [`question-type-sdk-matrix.md`](question-type-sdk-matrix.md) · [`placeholders-and-tokens.md`](placeholders-and-tokens.md) · [`partial-save-and-recovery.md`](partial-save-and-recovery.md) · [`survey-lifecycle-analytics.md`](survey-lifecycle-analytics.md) · [`skip-logic-and-navigation.md`](skip-logic-and-navigation.md) · [`display-logic-and-navigation.md`](display-logic-and-navigation.md) · [`question-display-variants.md`](question-display-variants.md) · [`question-numbering.md`](question-numbering.md) · [`end-page-logic.md`](end-page-logic.md) · [`answer-logic-and-navigation.md`](answer-logic-and-navigation.md) · [`custom-field-logic-and-navigation.md`](custom-field-logic-and-navigation.md) · [`progress.md`](progress.md) · [`constraints.md`](constraints.md)
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Stable references (CRITICAL)
|
|
69
|
+
|
|
70
|
+
Inline option objects cause infinite re-fetch loops and React hook dependency churn. Define **outside** the component and wrap hook options in `useMemo`:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
const SURVEY_PLACEHOLDERS = { FIRST_NAME: 'Customer' };
|
|
74
|
+
|
|
75
|
+
const CUSTOM_FIELD_VALUES: CustomFieldValueMap = {
|
|
76
|
+
'_c_abc123': 'value',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export default function SurveyPage() {
|
|
80
|
+
const [selectedLanguage, setSelectedLanguage] = useState<string | undefined>();
|
|
81
|
+
|
|
82
|
+
const surveyOptions = useMemo(
|
|
83
|
+
() => ({
|
|
84
|
+
instanceId: 'YOUR_JWT_HERE',
|
|
85
|
+
language: selectedLanguage,
|
|
86
|
+
placeholders: SURVEY_PLACEHOLDERS,
|
|
87
|
+
customFieldValues: CUSTOM_FIELD_VALUES,
|
|
88
|
+
savePartialResponse: false as const,
|
|
89
|
+
debug: false,
|
|
90
|
+
}),
|
|
91
|
+
[selectedLanguage]
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const { surveyQueryResults, submitSurveyResults, state, onAction } = useSurveySDK({
|
|
95
|
+
options: surveyOptions,
|
|
96
|
+
});
|
|
97
|
+
// ...
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Minimal SurveyPage skeleton
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
'use client';
|
|
107
|
+
|
|
108
|
+
import { useState, useMemo, useEffect } from 'react';
|
|
109
|
+
import {
|
|
110
|
+
useSurveySDK,
|
|
111
|
+
type CustomFieldValueMap,
|
|
112
|
+
type Survey,
|
|
113
|
+
} from '@explorer02/cfm-survey-sdk';
|
|
114
|
+
import Question from './Question';
|
|
115
|
+
|
|
116
|
+
const SURVEY_PLACEHOLDERS = { FIRST_NAME: 'Customer' };
|
|
117
|
+
const CUSTOM_FIELD_VALUES: CustomFieldValueMap = {}; // fill from CRM when survey uses custom-field logic
|
|
118
|
+
|
|
119
|
+
function SurveyContent({
|
|
120
|
+
survey,
|
|
121
|
+
allQuestions,
|
|
122
|
+
customFieldValues,
|
|
123
|
+
state,
|
|
124
|
+
submitSurveyResults,
|
|
125
|
+
onAction,
|
|
126
|
+
}: {
|
|
127
|
+
survey: Survey;
|
|
128
|
+
allQuestions: Survey['pages'][number]['questions'];
|
|
129
|
+
customFieldValues: CustomFieldValueMap;
|
|
130
|
+
state: ReturnType<typeof useSurveySDK>['state'];
|
|
131
|
+
submitSurveyResults: ReturnType<typeof useSurveySDK>['submitSurveyResults'];
|
|
132
|
+
onAction: ReturnType<typeof useSurveySDK>['onAction'];
|
|
133
|
+
}) {
|
|
134
|
+
// Mark as started on first scroll
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
const onScroll = () => onAction({ type: 'SCROLL' });
|
|
137
|
+
window.addEventListener('scroll', onScroll, { passive: true, once: true });
|
|
138
|
+
return () => window.removeEventListener('scroll', onScroll);
|
|
139
|
+
}, [onAction]);
|
|
140
|
+
|
|
141
|
+
// Question activity (VIEWED)
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
const observer = new IntersectionObserver(
|
|
144
|
+
entries => {
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
if (entry.isIntersecting && entry.target.id) {
|
|
147
|
+
onAction({
|
|
148
|
+
type: 'RECORD_QUESTION_VIEW',
|
|
149
|
+
payload: { questionIds: [entry.target.id] },
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
{ threshold: 0.5 }
|
|
155
|
+
);
|
|
156
|
+
state.currentQuestions.forEach(q => {
|
|
157
|
+
const el = document.getElementById(q.id);
|
|
158
|
+
if (el) observer.observe(el);
|
|
159
|
+
});
|
|
160
|
+
return () => observer.disconnect();
|
|
161
|
+
}, [state.currentQuestions, onAction]);
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<main>
|
|
165
|
+
<ProgressBar progressPercentage={state.progressPercentage} />
|
|
166
|
+
|
|
167
|
+
{state.currentQuestions.map(question => (
|
|
168
|
+
<Question
|
|
169
|
+
key={question.id}
|
|
170
|
+
question={question}
|
|
171
|
+
selectedValue={state.answers[question.id]}
|
|
172
|
+
validationError={state.validationErrors[question.id]}
|
|
173
|
+
allAnswers={state.answers}
|
|
174
|
+
allQuestions={allQuestions}
|
|
175
|
+
customFieldValues={customFieldValues}
|
|
176
|
+
onSelect={answerValue =>
|
|
177
|
+
onAction({ type: 'CHANGE', payload: { questionId: question.id, answerValue } })
|
|
178
|
+
}
|
|
179
|
+
/>
|
|
180
|
+
))}
|
|
181
|
+
|
|
182
|
+
{state.canGoBack && (
|
|
183
|
+
<button type="button" onClick={() => onAction({ type: 'PREVIOUS' })}>Back</button>
|
|
184
|
+
)}
|
|
185
|
+
<button
|
|
186
|
+
type="button"
|
|
187
|
+
onClick={() => onAction({ type: 'NEXT' })}
|
|
188
|
+
disabled={submitSurveyResults.isLoading}
|
|
189
|
+
>
|
|
190
|
+
{state.primaryButtonType === 'submit' ? 'Submit' : 'Next'}
|
|
191
|
+
</button>
|
|
192
|
+
</main>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export default function SurveyPage() {
|
|
197
|
+
const [selectedLanguage, setSelectedLanguage] = useState<string | undefined>();
|
|
198
|
+
|
|
199
|
+
const surveyOptions = useMemo(
|
|
200
|
+
() => ({
|
|
201
|
+
instanceId: 'YOUR_JWT_HERE',
|
|
202
|
+
language: selectedLanguage,
|
|
203
|
+
placeholders: SURVEY_PLACEHOLDERS,
|
|
204
|
+
customFieldValues: CUSTOM_FIELD_VALUES,
|
|
205
|
+
}),
|
|
206
|
+
[selectedLanguage]
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const { surveyQueryResults, submitSurveyResults, state, onAction } = useSurveySDK({
|
|
210
|
+
options: surveyOptions,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const survey = surveyQueryResults.data;
|
|
214
|
+
const allQuestions = useMemo(
|
|
215
|
+
() => survey?.pages.flatMap(page => page.questions) ?? [],
|
|
216
|
+
[survey]
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (surveyQueryResults.isLoading) return <div>Loading…</div>;
|
|
220
|
+
if (surveyQueryResults.error) return <div>Error: {surveyQueryResults.error.message}</div>;
|
|
221
|
+
if (!survey) return <div>No survey data</div>;
|
|
222
|
+
|
|
223
|
+
// Phase router — handle intro / paused / end / terminal before content
|
|
224
|
+
if (state.phase.kind === 'intro') return <IntroPage onStart={() => onAction({ type: 'START' })} />;
|
|
225
|
+
if (state.phase.kind === 'end') return <EndPage page={state.specialPage} />;
|
|
226
|
+
// … paused, submitted, expired, quota_fulfilled
|
|
227
|
+
|
|
228
|
+
if (state.phase.kind === 'content') {
|
|
229
|
+
return (
|
|
230
|
+
<SurveyContent
|
|
231
|
+
survey={survey}
|
|
232
|
+
allQuestions={allQuestions}
|
|
233
|
+
customFieldValues={CUSTOM_FIELD_VALUES}
|
|
234
|
+
state={state}
|
|
235
|
+
submitSurveyResults={submitSurveyResults}
|
|
236
|
+
onAction={onAction}
|
|
237
|
+
/>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
See [`01-components/01-survey-page.md`](../01-components/01-survey-page.md) for full phase routing.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Question dispatcher essentials
|
|
250
|
+
|
|
251
|
+
Copy [`templates/Question.tsx`](../templates/Question.tsx). Dispatch on flat `question.type` — **11** `QUESTION_TYPE` literals, no `subType`.
|
|
252
|
+
|
|
253
|
+
See [`question-type-sdk-matrix.md`](question-type-sdk-matrix.md) for per-type answer shapes, visibility helpers, and `onAction` patterns.
|
|
254
|
+
|
|
255
|
+
| Type literal | Component | Visibility helper |
|
|
256
|
+
|--------------|-----------|-------------------|
|
|
257
|
+
| `MCQ` | Inline | `getVisibleMcqOptionsForAnswers` |
|
|
258
|
+
| `TEXTFIELD` | Inline | — |
|
|
259
|
+
| `NPS_SCALE` | `RatingScale` | `getVisibleScaleColumnsForAnswers` |
|
|
260
|
+
| `CFM_MATRIX` | `LikertMatrixScale` | `getVisibleMatrixItemsForAnswers` |
|
|
261
|
+
| `CSAT_MATRIX`, `RATING_MATRIX` | `CsatMatrixScale` | `getVisibleMatrixItemsForAnswers` |
|
|
262
|
+
| `SLIDER_MATRIX` | `SliderMatrixScale` | `getVisibleStatementRowsForAnswers` |
|
|
263
|
+
| `FILE_UPLOAD` | `FileUploadScale` | — |
|
|
264
|
+
| `TEXT_AND_MEDIA` | Inline media | — |
|
|
265
|
+
| `HEATMAP` | `HeatmapScale` | — |
|
|
266
|
+
| `RANK_ORDER` | `RankOrderScale` | `getVisibleRankOrderOptionsForAnswers` |
|
|
267
|
+
|
|
268
|
+
**Answer logic:** filter before render:
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
getVisibleMcqOptionsForAnswers(question, allAnswers, allQuestions, customFieldValues);
|
|
272
|
+
getVisibleMatrixItemsForAnswers(question, allAnswers, allQuestions, customFieldValues);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Placeholders:** stable `SURVEY_PLACEHOLDERS` ref + `resolveSurveyQuestionPlaceholders(text, allAnswers, questionsById)`. See [`placeholders-and-tokens.md`](placeholders-and-tokens.md).
|
|
276
|
+
|
|
277
|
+
**Wrapper:** every question `<section id={question.id}>` for scroll-to-error.
|
|
278
|
+
|
|
279
|
+
See [`component-checklist.md`](component-checklist.md) and [`answer-logic-and-navigation.md`](answer-logic-and-navigation.md).
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Custom fields — two wiring points
|
|
284
|
+
|
|
285
|
+
When survey logic references `conditionEntity.entityType: 'CUSTOM_FIELD'`:
|
|
286
|
+
|
|
287
|
+
1. **`useSurveySDK` options** — `customFieldValues` map keyed by `_c_…` ids (skip, display, validation, progress).
|
|
288
|
+
2. **`Question.tsx` visibility helpers** — pass the **same map** as the 4th argument to `getVisible*ForAnswers` (answer-logic UI on options/rows/columns).
|
|
289
|
+
|
|
290
|
+
See [`custom-field-logic-and-navigation.md`](custom-field-logic-and-navigation.md).
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Monorepo build prerequisite
|
|
295
|
+
|
|
296
|
+
When using `@repo/sdk` from the monorepo, build the SDK before running the client dev server:
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
cd packages/sdk && npm run build
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Without a built `dist/`, the client fails with missing module errors. Rebuild after SDK changes.
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Debug logging and analytics
|
|
307
|
+
|
|
308
|
+
Enable debug console logs with `useSurveySDK({ options: { debug: true } })`. Logic match logs appear as `[SDK:INFO]` when conditions **newly become true** on answer change:
|
|
309
|
+
|
|
310
|
+
| Log message | When |
|
|
311
|
+
|-------------|------|
|
|
312
|
+
| `Display logic matched` | A gated question becomes visible |
|
|
313
|
+
| `Answer logic matched` | An option / row / column becomes visible via answer logic |
|
|
314
|
+
| `Question variant matched` | A display variant’s condition matches |
|
|
315
|
+
| `End page logic matched` | The winning end page changes (preview before submit) |
|
|
316
|
+
| `Skip logic matched` | Skip logic newly activates on the current question |
|
|
317
|
+
|
|
318
|
+
Analytics events (`[SDK:ANALYTICS]`) fire **always**, even without `debug: true`:
|
|
319
|
+
|
|
320
|
+
| Event | When |
|
|
321
|
+
|-------|------|
|
|
322
|
+
| `display_logic_matched` | Display logic condition newly true |
|
|
323
|
+
| `answer_logic_matched` | Answer-logic sub-entity newly visible |
|
|
324
|
+
| `question_variant_matched` | Variant text switches to a matching variant |
|
|
325
|
+
| `end_page_logic_matched` | Resolved end page changes |
|
|
326
|
+
| `question_viewed` | Question enters viewport (`RECORD_QUESTION_VIEW`) |
|
|
327
|
+
| `skip_logic_triggered` | Skip logic newly activates on answer change |
|
|
328
|
+
| `logic_session_summary` | On successful submit — rollup table + chronological timeline |
|
|
329
|
+
|
|
330
|
+
The submit-time `logic_session_summary` includes counts per logic kind, viewed vs on-path-not-viewed questions, final end page resolution, and a `logicEventTimeline` audit. It prints via `console.table` alongside `survey_session_summary`.
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Pre-flight checklist
|
|
335
|
+
|
|
336
|
+
Before `npm run build`:
|
|
337
|
+
|
|
338
|
+
- [ ] Import from package root only — no `@repo/sdk/src/…` paths
|
|
339
|
+
- [ ] `SURVEY_PLACEHOLDERS` and `CUSTOM_FIELD_VALUES` defined outside component
|
|
340
|
+
- [ ] `surveyOptions` wrapped in `useMemo` (depends on `selectedLanguage` etc.)
|
|
341
|
+
- [ ] `Question.tsx` copied from `docs/templates/Question.tsx` — all **11** type branches, no stub text
|
|
342
|
+
- [ ] `SURVEY_PLACEHOLDERS` defined outside component; `surveyOptions` in `useMemo` (see [`placeholders-and-tokens.md`](placeholders-and-tokens.md))
|
|
343
|
+
- [ ] `id={question.id}` on every question wrapper
|
|
344
|
+
- [ ] `customFieldValues` passed to hook **and** `Question` (when survey uses custom-field logic)
|
|
345
|
+
- [ ] Answer logic: `getVisible*ForAnswers` receives `customFieldValues` 4th arg
|
|
346
|
+
- [ ] `SCROLL` + `RECORD_QUESTION_VIEW` lifecycle effects wired (see [`survey-lifecycle-analytics.md`](survey-lifecycle-analytics.md))
|
|
347
|
+
- [ ] `state.canGoBack` / `state.primaryButtonType` used for nav (not raw page index)
|
|
348
|
+
- [ ] Monorepo: SDK built (`packages/sdk/dist/` exists)
|
|
349
|
+
- [ ] `grep -r "not yet implemented" src/ components/ app/` → zero matches
|
|
350
|
+
|
|
351
|
+
Run `bash templates/verify-agent-build.sh` from project root after Phase 5.
|
|
@@ -1,41 +1,52 @@
|
|
|
1
1
|
# Required Component Files Checklist
|
|
2
2
|
|
|
3
|
-
> **Blocking gate before Phase 5 (BUILD).** After fetching the survey with `instanceId`, list every `
|
|
3
|
+
> **Blocking gate before Phase 5 (BUILD).** After fetching the survey with `instanceId`, list every `question.type` and ensure all required files exist.
|
|
4
4
|
|
|
5
5
|
## Step 1: Inventory Question Types
|
|
6
6
|
|
|
7
|
-
From `survey.pages`, collect unique
|
|
7
|
+
From `survey.pages`, collect unique `question.type` values — see `MANIFEST.json` `typeLiterals`:
|
|
8
8
|
|
|
9
|
-
| questionId | type |
|
|
10
|
-
|
|
11
|
-
| | |
|
|
9
|
+
| questionId | type |
|
|
10
|
+
|------------|------|
|
|
11
|
+
| | |
|
|
12
12
|
|
|
13
13
|
## Step 2: Create Required Files
|
|
14
14
|
|
|
15
15
|
| If survey contains | Agent MUST create before `npm run build` | UI spec |
|
|
16
16
|
|--------------------|------------------------------------------|---------|
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
17
|
+
| `NPS_SCALE` | `RatingScale.tsx` | `03-ui-specs/01-rating.md` |
|
|
18
|
+
| `CFM_MATRIX` | `LikertMatrixScale.tsx`, `MatrixDropdown.tsx` | `03-ui-specs/07-matrix-cfm.md` |
|
|
19
|
+
| `CSAT_MATRIX` | `CsatMatrixScale.tsx`, `MatrixDropdown.tsx`, `CustomSliderTrack.tsx` | `03-ui-specs/04-csat.md` |
|
|
20
|
+
| `RATING_MATRIX` | `CsatMatrixScale.tsx`, `MatrixDropdown.tsx`, `CustomSliderTrack.tsx` | `03-ui-specs/08-matrix-csat-rating.md` |
|
|
21
|
+
| `SLIDER_MATRIX` | `SliderMatrixScale.tsx`, `CustomSliderTrack.tsx` | `03-ui-specs/09-slider-matrix.md` |
|
|
22
|
+
| `FILE_UPLOAD` | `FileUploadScale.tsx` | `03-ui-specs/10-file-upload.md` |
|
|
23
|
+
| `MCQ`, `TEXTFIELD`, `TEXT_AND_MEDIA` | Inline in `Question.tsx` | `03-ui-specs/02-radio.md`, `03-text.md`, `11-text-and-media.md` |
|
|
24
|
+
| `RANK_ORDER` | `RankOrderScale.tsx` (+ `@dnd-kit/*`) | `03-ui-specs/14-rank-order.md` |
|
|
25
|
+
| `HEATMAP` | `HeatmapScale.tsx`, `lib/heatmapCoords.ts` (copy from `templates/heatmapCoords.ts`) | `03-ui-specs/13-heatmap.md` |
|
|
26
|
+
|
|
27
|
+
**Survey chrome (when applicable):**
|
|
28
|
+
|
|
29
|
+
| Component | When |
|
|
30
|
+
|-----------|------|
|
|
31
|
+
| `IntroPage.tsx` | `survey.introPage` |
|
|
32
|
+
| `EndPage.tsx` | `survey.endPages` |
|
|
33
|
+
| `PausedPage.tsx` | `survey.phaseFlags.paused` |
|
|
26
34
|
|
|
27
35
|
Always create **`Question.tsx`** from [`templates/Question.tsx`](../templates/Question.tsx).
|
|
28
36
|
|
|
29
|
-
##
|
|
37
|
+
## Flat Type Routing (CRITICAL)
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
Use `QUESTION_TYPE` — **no `subType`**:
|
|
32
40
|
|
|
33
41
|
```tsx
|
|
34
|
-
{
|
|
42
|
+
import { QUESTION_TYPE } from '@explorer02/cfm-survey-sdk';
|
|
43
|
+
|
|
44
|
+
{question.type === QUESTION_TYPE.CFM_MATRIX && (
|
|
35
45
|
<LikertMatrixScale question={question} selectedValue={...} onSelect={onSelect} />
|
|
36
46
|
)}
|
|
37
47
|
|
|
38
|
-
{question.type ===
|
|
48
|
+
{(question.type === QUESTION_TYPE.CSAT_MATRIX ||
|
|
49
|
+
question.type === QUESTION_TYPE.RATING_MATRIX) && (
|
|
39
50
|
<CsatMatrixScale question={question} selectedValue={...} onSelect={onSelect} />
|
|
40
51
|
)}
|
|
41
52
|
```
|
|
@@ -44,16 +55,28 @@ Always create **`Question.tsx`** from [`templates/Question.tsx`](../templates/Qu
|
|
|
44
55
|
|
|
45
56
|
| File | Used by |
|
|
46
57
|
|------|---------|
|
|
47
|
-
| `CustomSliderTrack.tsx` | `
|
|
48
|
-
| `MatrixDropdown.tsx` | `
|
|
58
|
+
| `CustomSliderTrack.tsx` | `SLIDER_MATRIX`, matrix `displayStyle: graphics` |
|
|
59
|
+
| `MatrixDropdown.tsx` | `CFM_MATRIX`, `CSAT_MATRIX`, `RATING_MATRIX` dropdown/selectbox modes |
|
|
60
|
+
|
|
61
|
+
Store matrix values as column **`id`** — `matrixColumnStoredValue(col)` from SDK.
|
|
62
|
+
|
|
63
|
+
## Answer logic (when survey has gated options / rows / columns)
|
|
64
|
+
|
|
65
|
+
- [ ] MCQ: filter options with `getVisibleMcqOptionsForAnswers(question, answers, allQuestions, customFieldValues)`
|
|
66
|
+
- [ ] Matrix (CSAT/CFM/RATING/SLIDER): filter rows (and CSAT/CFM columns) with `getVisibleMatrixItemsForAnswers(question, answers, allQuestions, customFieldValues)`
|
|
67
|
+
- [ ] Pass `customFieldValues` from `SurveyPage` → `Question` when survey logic references `CUSTOM_FIELD` entities
|
|
68
|
+
- [ ] Wire `onAction({ type: 'CHANGE' })` so answer logic re-evaluates when prior answers change
|
|
49
69
|
|
|
50
|
-
|
|
70
|
+
See [`answer-logic-and-navigation.md`](answer-logic-and-navigation.md) and [`custom-field-logic-and-navigation.md`](custom-field-logic-and-navigation.md).
|
|
51
71
|
|
|
52
72
|
## Step 3: Verify Before Build
|
|
53
73
|
|
|
54
74
|
- [ ] Every type in the survey inventory has a matching component file
|
|
55
|
-
- [ ] `Question.tsx` copied from `docs/templates/Question.tsx` with all branches wired
|
|
75
|
+
- [ ] `Question.tsx` copied from `docs/templates/Question.tsx` with all **11** type branches wired
|
|
76
|
+
- [ ] `SurveyPage` handles all `state.phase` values
|
|
56
77
|
- [ ] No `default` fallback rendering stub text (see `constraints.md` §7)
|
|
78
|
+
- [ ] RANK_ORDER: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities` in `package.json`
|
|
79
|
+
- [ ] Per-type wiring verified against `question-type-sdk-matrix.md`
|
|
57
80
|
- [ ] Grep project: `not yet implemented` → **zero matches**
|
|
58
81
|
|
|
59
82
|
```bash
|
|
@@ -36,9 +36,25 @@ Required for scroll-to-error when validation fails.
|
|
|
36
36
|
|
|
37
37
|
Survey UI files importing the SDK must be client components. Next.js: `'use client'` at top of file.
|
|
38
38
|
|
|
39
|
-
## 6. Stable Placeholder References
|
|
39
|
+
## 6. Stable Placeholder and Custom Field References
|
|
40
40
|
|
|
41
|
-
Define `SURVEY_PLACEHOLDERS` outside the component — inline objects cause infinite re-fetch.
|
|
41
|
+
Define `SURVEY_PLACEHOLDERS` and `CUSTOM_FIELD_VALUES` **outside** the component — inline objects cause infinite re-fetch and unstable hook dependency arrays.
|
|
42
|
+
|
|
43
|
+
Wrap hook `options` in `useMemo`:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
const surveyOptions = useMemo(
|
|
47
|
+
() => ({
|
|
48
|
+
instanceId,
|
|
49
|
+
language: selectedLanguage,
|
|
50
|
+
placeholders: SURVEY_PLACEHOLDERS,
|
|
51
|
+
customFieldValues: CUSTOM_FIELD_VALUES,
|
|
52
|
+
}),
|
|
53
|
+
[selectedLanguage]
|
|
54
|
+
);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
See [`custom-field-logic-and-navigation.md`](custom-field-logic-and-navigation.md) when the survey uses custom-field logic.
|
|
42
58
|
|
|
43
59
|
## 7. No Stub Placeholders (CRITICAL)
|
|
44
60
|
|
|
@@ -58,14 +74,43 @@ grep -r "not yet implemented" src/ components/ app/ 2>/dev/null || true
|
|
|
58
74
|
|
|
59
75
|
---
|
|
60
76
|
|
|
77
|
+
## 8. Deploy Constraints
|
|
78
|
+
|
|
79
|
+
- **Never embed AWS credentials** in client code — JWT goes in `Authorization: Bearer` header at deploy time only
|
|
80
|
+
- **Deploy JWT must match** `SURVEY_INSTANCE_ID` in `SurveyPage.tsx` (surveyId drives S3 prefix)
|
|
81
|
+
- **Next.js AWS deploy requires static export** — no server-only APIs (`getServerSideProps`, etc.)
|
|
82
|
+
- **Max 500 files** per deploy (EC2 API limit)
|
|
83
|
+
- **No absolute `/`-root asset paths** when deploying to nested CloudFront prefix — use `assetPrefix: "./"` and relative public assets
|
|
84
|
+
|
|
85
|
+
See [`aws-deploy.md`](aws-deploy.md) for the full AWS deploy procedure and troubleshooting.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 9. SDK Integration Constraints
|
|
90
|
+
|
|
91
|
+
- **Never import internal evaluators** from `@explorer02/cfm-survey-sdk/src/**` or `@repo/sdk/src/**`
|
|
92
|
+
- **Always use public `getVisible*ForAnswers` helpers** for answer-logic UI filtering — never reimplement condition evaluation
|
|
93
|
+
- **Matrix cell values:** always store column **`id`** via `matrixColumnStoredValue(column)` — never raw label text
|
|
94
|
+
- **Rank order:** use SDK rank helpers (`assignRankWithoutDuplicates`, `normalizeRankOrderAnswers`, etc.) — do not hand-roll rank maps
|
|
95
|
+
- **CSAT emoji/star UI:** implement client-side with `react-icons` — no phantom SDK emoji exports
|
|
96
|
+
- **Read [`question-type-sdk-matrix.md`](question-type-sdk-matrix.md)** before building any scale component
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
61
100
|
## Troubleshooting
|
|
62
101
|
|
|
63
102
|
| Symptom | Fix |
|
|
64
103
|
|---------|-----|
|
|
65
104
|
| `Cannot use import statement outside a module` | Add SDK to `transpilePackages` in `next.config.js` |
|
|
66
|
-
| Infinite API requests | Move `placeholders`
|
|
105
|
+
| Infinite API requests | Move `placeholders` / `customFieldValues` outside component; wrap options in `useMemo` |
|
|
106
|
+
| `useEffect` dependency array size changed | Unstable inline `options` object — use `useMemo` for hook options |
|
|
107
|
+
| Missing `dist/index.mjs` (monorepo) | Run `npm run build` in `packages/sdk` before client dev |
|
|
67
108
|
| `window is not defined` | Add `'use client'` to SDK-consuming files |
|
|
68
109
|
| Validation errors but no scroll | Add `id={question.id}` to question wrapper |
|
|
69
110
|
| Survey data is null, no error | Invalid/expired `instanceId` — ask client for fresh JWT |
|
|
70
111
|
| CSAT emojis missing | `npm install react-icons` |
|
|
71
|
-
| Grey "not yet implemented" text on questions | Copy `docs/templates/Question.tsx`; create scale components per `component-checklist.md`; route matrix by `
|
|
112
|
+
| Grey "not yet implemented" text on questions | Copy `docs/templates/Question.tsx`; create scale components per `component-checklist.md`; route each matrix variant by flat `question.type` |
|
|
113
|
+
| 403 on `/_next/static/...` after AWS deploy | Add `assetPrefix: "./"` to `next.config.js`, rebuild — see `aws-deploy.md` |
|
|
114
|
+
| 403 on survey images `*_p.png` | Use `option.imageUrl` before `option.previewImageUrl` in RankOrderScale |
|
|
115
|
+
| 403 on `index.html.txt?_rsc` | Add `cp out/index.txt out/index.html.txt` to build script |
|
|
116
|
+
| AWS deploy PUT failures | Presigned URLs expired — restart from `/deploy/start` within 5–15 min |
|