@explorer02/cfm-survey-sdk 0.2.1 → 0.2.3
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 +18 -8
- package/templates/docs/00-integration/analytics-events-catalog.md +54 -0
- 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 +55 -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 +64 -0
- package/templates/docs/00-integration/end-page-logic.md +64 -0
- package/templates/docs/00-integration/file-upload-aws.md +116 -0
- package/templates/docs/00-integration/logic-fields-catalog.md +105 -0
- package/templates/docs/00-integration/partial-save-and-recovery.md +98 -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 +244 -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 +14 -4
- package/templates/docs/01-components/07-slider-matrix-scale.md +11 -2
- package/templates/docs/01-components/08-file-upload-scale.md +23 -51
- 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 +29 -32
- 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 +93 -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 +43 -17
- 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 +96 -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 +18 -52
- package/templates/docs/03-ui-specs/02-radio.md +80 -37
- package/templates/docs/03-ui-specs/03-text.md +2 -2
- package/templates/docs/03-ui-specs/04-csat.md +68 -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 +44 -25
- package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +19 -43
- package/templates/docs/03-ui-specs/09-slider-matrix.md +18 -1
- package/templates/docs/03-ui-specs/10-file-upload.md +42 -21
- package/templates/docs/03-ui-specs/11-text-and-media.md +29 -29
- package/templates/docs/03-ui-specs/12-survey-chrome.md +58 -14
- package/templates/docs/03-ui-specs/13-heatmap.md +80 -0
- package/templates/docs/03-ui-specs/14-rank-order.md +128 -0
- package/templates/docs/03-ui-specs/README.md +16 -14
- package/templates/docs/03-ui-specs/shared/custom-slider-track.md +28 -35
- package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +5 -0
- package/templates/docs/MANIFEST.json +476 -69
- package/templates/docs/index.md +82 -30
- package/templates/docs/templates/CustomSliderTrack.tsx +144 -0
- package/templates/docs/templates/MatrixDropdown.tsx +216 -0
- package/templates/docs/templates/Question.tsx +221 -154
- package/templates/docs/templates/RankOrderScale.tsx +353 -0
- 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 +126 -67
- package/templates/docs/templates/survey-inventory.schema.json +77 -0
- package/templates/docs/templates/surveyUiIcons.tsx +52 -0
- package/templates/docs/templates/verify-agent-build.sh +85 -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,80 @@
|
|
|
1
|
+
# Skip Logic and Navigation
|
|
2
|
+
|
|
3
|
+
> Integrator-facing behavior contract. Skip evaluation is handled inside the SDK — do not import internal `surveyLogic` paths.
|
|
4
|
+
|
|
5
|
+
## When skip rules run
|
|
6
|
+
|
|
7
|
+
- Skip rules live on individual questions (`question.skipLogics`).
|
|
8
|
+
- When a question has **multiple skip logic blocks**, the SDK evaluates them **top-to-bottom (API order)** and applies the **first** whose condition is true.
|
|
9
|
+
- On **Next**, the SDK evaluates skip logic on the **last question of the current page** only.
|
|
10
|
+
- Skip targets:
|
|
11
|
+
- `QUESTION` — jump forward to another question's page
|
|
12
|
+
- `END_OF_SURVEY` — trigger submit flow (validation bypass for skipped pages)
|
|
13
|
+
|
|
14
|
+
**Forward-only:** backward skip targets are rejected. The respondent cannot skip to an earlier page via skip logic.
|
|
15
|
+
|
|
16
|
+
## Multiselect MCQ — count of choice
|
|
17
|
+
|
|
18
|
+
Multiselect MCQ skip rules may use `field: 'NUM_OF_SELECTED_OPTIONS'` (**Count of Choice** in the builder). The SDK compares the number of selected option ids with numeric operators (`EQUALS`, `NOT_EQUALS`, `GT`, `GTE`, `LT`, `LTE`). Zero selections is treated as missing for `MISSING` / `EXISTS`.
|
|
19
|
+
|
|
20
|
+
## Custom field conditions
|
|
21
|
+
|
|
22
|
+
Skip rules may reference **custom fields** (`conditionEntity.entityType: 'CUSTOM_FIELD'`). See [`custom-field-logic-and-navigation.md`](custom-field-logic-and-navigation.md) for value shapes, operator contract, and the two wiring points (hook options + visibility helpers).
|
|
23
|
+
|
|
24
|
+
## Simple example
|
|
25
|
+
|
|
26
|
+
| Page | Question | Rule |
|
|
27
|
+
|------|----------|------|
|
|
28
|
+
| 1 | Q1 (NPS) | If score > 7 → skip to Q4 |
|
|
29
|
+
| 2 | Q2 | (middle) |
|
|
30
|
+
| 3 | Q3 | (middle) |
|
|
31
|
+
| 4 | Q4 | destination |
|
|
32
|
+
|
|
33
|
+
1. User sets Q1 = **5** → path is pages 1 → 2 → 3 → 4 (sequential).
|
|
34
|
+
2. User sets Q1 = **9** → simulated path becomes pages **1 → 4** (pages 2–3 skipped).
|
|
35
|
+
3. User clicks **Next** from page 1 → lands on page 4.
|
|
36
|
+
|
|
37
|
+
## Answer changes (`onChange`)
|
|
38
|
+
|
|
39
|
+
When the edited question has skip logic, the SDK compares the route **before** and **after** the edit.
|
|
40
|
+
|
|
41
|
+
| Check | Meaning | SDK action |
|
|
42
|
+
|-------|---------|------------|
|
|
43
|
+
| **Path changed** | Page route reshaped (e.g. sequential → skip) | Clear answers on all pages after current page; reset skip/visit tracking ahead; refresh progress |
|
|
44
|
+
| **Pruned only** | Route unchanged but progress credits were stale | Remove off-path earned credits; refresh progress; **keep answers** |
|
|
45
|
+
|
|
46
|
+
**Example — path changed:** User walked page 2, answered Q2, then on page 1 changes Q1 from 5 → 9. Q2 answer is cleared because page 2 is no longer on the active path.
|
|
47
|
+
|
|
48
|
+
**Example — pruned only:** User already on skip path; edits Q1 from 8 → 9 (still skips). Route unchanged; stale progress credits for pages visited earlier may be removed without clearing answers.
|
|
49
|
+
|
|
50
|
+
## Back navigation
|
|
51
|
+
|
|
52
|
+
- Back uses **`pageHistory`**, not `currentPageIndex - 1`.
|
|
53
|
+
- After a skip jump (e.g. page 1 → page 4), Back returns to the **skip origin** (page 1), not the sequential previous page.
|
|
54
|
+
- **Answers are preserved on Back** — the SDK does not sanitize answers when going back.
|
|
55
|
+
- Skip/visit tracking for pages ahead of the back target is reset.
|
|
56
|
+
|
|
57
|
+
## Forward navigation (`onNext`)
|
|
58
|
+
|
|
59
|
+
- Validates **current page only** before moving.
|
|
60
|
+
- On skip: marks intermediate pages in `skippedPageIndices`; may sanitize answers on skipped pages.
|
|
61
|
+
- Sequential Next skips empty pages automatically.
|
|
62
|
+
|
|
63
|
+
## Submit validation
|
|
64
|
+
|
|
65
|
+
- Submit validates **non-skipped pages** only (pages marked skipped via skip logic are excluded).
|
|
66
|
+
- End-of-survey skip triggers submit with `skipValidation: true` for remaining pages.
|
|
67
|
+
|
|
68
|
+
## Optional question telemetry
|
|
69
|
+
|
|
70
|
+
When the user leaves a page (Next or Submit), the SDK logs analytics for **optional questions left blank** on that page. This does not block navigation or affect answers.
|
|
71
|
+
|
|
72
|
+
Event: `optional_question_skipped` with `questionId` and `pageIndex`.
|
|
73
|
+
|
|
74
|
+
## Client responsibilities
|
|
75
|
+
|
|
76
|
+
- Wire `onAction({ type: 'NEXT' })`, `onAction({ type: 'PREVIOUS' })`, and `onAction({ type: 'CHANGE', payload: { questionId, answerValue } })`.
|
|
77
|
+
- Use `state.canGoBack` for the Back button (not `currentPageIndex === 0`).
|
|
78
|
+
- Use `state.primaryButtonType` for Next vs Submit label; SDK maps `NEXT` → `SUBMIT` on the last page when appropriate.
|
|
79
|
+
|
|
80
|
+
See also: [`progress.md`](progress.md), [`useSurveySDK.md`](useSurveySDK.md), [`display-logic-and-navigation.md`](display-logic-and-navigation.md), [`answer-logic-and-navigation.md`](answer-logic-and-navigation.md), [`logic-fields-catalog.md`](logic-fields-catalog.md).
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Survey Lifecycle and Analytics
|
|
2
|
+
|
|
3
|
+
> Covers `sdkFeatures.markAsStarted` and `sdkFeatures.questionActivity` — first-interaction tracking and per-question view telemetry.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Mark as started
|
|
8
|
+
|
|
9
|
+
The SDK automatically marks the survey as started on the **first `CHANGE` action** (any answer update). No separate client call is required when using `useSurveySDK`.
|
|
10
|
+
|
|
11
|
+
Additionally, fire **`SCROLL`** once on the user's first scroll to capture engagement before first answer:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
let scrolled = false;
|
|
16
|
+
const onScroll = () => {
|
|
17
|
+
if (!scrolled) {
|
|
18
|
+
scrolled = true;
|
|
19
|
+
onAction({ type: 'SCROLL' });
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
window.addEventListener('scroll', onScroll, { passive: true });
|
|
23
|
+
return () => window.removeEventListener('scroll', onScroll);
|
|
24
|
+
}, [onAction]);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Attach to the survey container scroll root if the page uses an inner scroll area instead of `window`.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Question activity (`RECORD_QUESTION_VIEW`)
|
|
32
|
+
|
|
33
|
+
Fire when a question enters the viewport so the backend records view events. Use `IntersectionObserver`:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const seen = new Set<string>();
|
|
38
|
+
const observer = new IntersectionObserver(
|
|
39
|
+
(entries) => {
|
|
40
|
+
const ids: string[] = [];
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
if (entry.isIntersecting) {
|
|
43
|
+
const id = entry.target.id.replace(/^question-/, '');
|
|
44
|
+
if (!seen.has(id)) {
|
|
45
|
+
seen.add(id);
|
|
46
|
+
ids.push(id);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (ids.length > 0) {
|
|
51
|
+
onAction({ type: 'RECORD_QUESTION_VIEW', payload: { questionIds: ids } });
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{ threshold: 0.5 }
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
for (const q of currentQuestions) {
|
|
58
|
+
const el = document.getElementById(`question-${q.id}`);
|
|
59
|
+
if (el) observer.observe(el);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return () => observer.disconnect();
|
|
63
|
+
}, [currentQuestions, onAction]);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Requirements
|
|
67
|
+
|
|
68
|
+
- Every question wrapper must have `id={`question-${question.id}`}` (Phase 6 verify).
|
|
69
|
+
- Re-run the effect when `currentQuestions` changes (multi-page surveys, display logic).
|
|
70
|
+
- SDK **deduplicates** question IDs server-side per session — client `seen` set avoids duplicate actions in one mount.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Batching
|
|
75
|
+
|
|
76
|
+
`RECORD_QUESTION_VIEW` accepts `{ questionIds: string[] }`. Batch all newly visible IDs from one observer callback tick rather than one action per question when multiple enter view together.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Debug: `logic_session_summary`
|
|
81
|
+
|
|
82
|
+
When `debug: true` is set in hook options, the SDK emits structured console events including lifecycle transitions. Full event catalog: [`analytics-events-catalog.md`](analytics-events-catalog.md).
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Wiring checklist
|
|
87
|
+
|
|
88
|
+
- [ ] `SCROLL` fired once on first scroll
|
|
89
|
+
- [ ] First `CHANGE` left to SDK (auto mark-as-started)
|
|
90
|
+
- [ ] `IntersectionObserver` on `#question-{id}` wrappers
|
|
91
|
+
- [ ] Effect depends on `currentQuestions` — re-observe on page change
|
|
92
|
+
- [ ] No duplicate `RECORD_QUESTION_VIEW` spam (client dedup + SDK dedup)
|
|
93
|
+
|
|
94
|
+
See [`client-integration-guide.md`](client-integration-guide.md) and [`implementation_plan.md`](../templates/implementation_plan.md) §1b.
|
|
@@ -13,9 +13,7 @@ import {
|
|
|
13
13
|
type UseSurveySDKReturn,
|
|
14
14
|
type SurveyQuestion,
|
|
15
15
|
type AnswerValue,
|
|
16
|
-
type
|
|
17
|
-
CsatEmojiSet,
|
|
18
|
-
getEmojiForIndex,
|
|
16
|
+
type MatrixRowAnswers,
|
|
19
17
|
} from '@explorer02/cfm-survey-sdk'; // or @repo/sdk in monorepo
|
|
20
18
|
```
|
|
21
19
|
|
|
@@ -28,12 +26,14 @@ Ask the client: *"Please provide your survey instance ID (JWT token)."*
|
|
|
28
26
|
## Hook Signature
|
|
29
27
|
|
|
30
28
|
```typescript
|
|
31
|
-
const { surveyQueryResults, submitSurveyResults, state, onAction } = useSurveySDK({
|
|
29
|
+
const { surveyQueryResults, submitSurveyResults, partialSaveResults, state, onAction } = useSurveySDK({
|
|
32
30
|
options: {
|
|
33
31
|
instanceId: string, // JWT from client
|
|
34
32
|
language: string | undefined, // Active language code
|
|
35
33
|
placeholders: Record<string, string>, // Token replacements — MUST be stable ref
|
|
34
|
+
customFieldValues?: CustomFieldValueMap, // CRM/transaction custom field values — MUST be stable ref
|
|
36
35
|
debug: boolean, // Console logging
|
|
36
|
+
savePartialResponse: boolean | { debounceMs?: number }, // Optional partial save (default 1500ms)
|
|
37
37
|
},
|
|
38
38
|
});
|
|
39
39
|
```
|
|
@@ -42,9 +42,52 @@ Define `placeholders` **outside** the component to avoid infinite re-fetch loops
|
|
|
42
42
|
|
|
43
43
|
```typescript
|
|
44
44
|
const SURVEY_PLACEHOLDERS = { FIRST_NAME: 'John' };
|
|
45
|
-
// inside component: options: { placeholders: SURVEY_PLACEHOLDERS }
|
|
46
45
|
```
|
|
47
46
|
|
|
47
|
+
Define `customFieldValues` **outside** the component (same stable-ref rule as `placeholders`):
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import type { CustomFieldValueMap } from '@explorer02/cfm-survey-sdk';
|
|
51
|
+
|
|
52
|
+
const CUSTOM_FIELD_VALUES: CustomFieldValueMap = {
|
|
53
|
+
'_c_6a436c1244764c9d4a6ce2fc': 'q5', // TEXT — string
|
|
54
|
+
'_c_6a436c3644764c9d4a6ce3f5': 2, // NUMBER — number or numeric string
|
|
55
|
+
'_c_6a436c9e44764c9d4a6ce717': 'option-id', // PICKLIST — option id string
|
|
56
|
+
'_c_6a436cbb44764c9d4a6ce7e5': ['opt-a'], // PICKLIST_MULTISELECT — option id array
|
|
57
|
+
'_c_6a4386dc44208eb8b08a9056': 1781029800000, // DATETIME — epoch ms (recommended)
|
|
58
|
+
};
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Wrap hook options in `useMemo` inside the component:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
const surveyOptions = useMemo(
|
|
65
|
+
() => ({
|
|
66
|
+
instanceId,
|
|
67
|
+
language: selectedLanguage,
|
|
68
|
+
placeholders: SURVEY_PLACEHOLDERS,
|
|
69
|
+
customFieldValues: CUSTOM_FIELD_VALUES,
|
|
70
|
+
}),
|
|
71
|
+
[selectedLanguage]
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
useSurveySDK({ options: surveyOptions });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Keys are the `_c_…` custom field ids from survey logic rules (`conditionEntity.id` in skip/display/answer logic). The SDK evaluates skip, display, and answer logic against these values when passed via hook options.
|
|
78
|
+
|
|
79
|
+
| Custom field type | Client value shape |
|
|
80
|
+
|---|---|
|
|
81
|
+
| TEXT / TEXT_MULTI / TEXTAREA | `string` |
|
|
82
|
+
| NUMBER | `number` or numeric string (`0` is a valid value) |
|
|
83
|
+
| PICKLIST | `string` (option id) |
|
|
84
|
+
| PICKLIST_MULTISELECT | `string[]` (option ids) |
|
|
85
|
+
| DATETIME | epoch ms (`number` or numeric string); ISO date strings also accepted |
|
|
86
|
+
|
|
87
|
+
Missing ids in the map are treated as empty for logic evaluation.
|
|
88
|
+
|
|
89
|
+
When the survey uses custom-field logic, also pass the same map into `Question.tsx` visibility helpers — see [`custom-field-logic-and-navigation.md`](custom-field-logic-and-navigation.md).
|
|
90
|
+
|
|
48
91
|
## Return Shape
|
|
49
92
|
|
|
50
93
|
| Namespace | Field | Type | Purpose |
|
|
@@ -55,42 +98,129 @@ const SURVEY_PLACEHOLDERS = { FIRST_NAME: 'John' };
|
|
|
55
98
|
| `submitSurveyResults` | `data` | `SubmitResult \| null` | Success payload |
|
|
56
99
|
| | `error` | `Error \| null` | Submit failure |
|
|
57
100
|
| | `isLoading` | `boolean` | Submit in progress |
|
|
58
|
-
| `
|
|
59
|
-
| | `
|
|
60
|
-
| | `
|
|
101
|
+
| `partialSaveResults` | `data` | `SubmitResult \| null` | Last partial save success payload (when enabled) |
|
|
102
|
+
| | `error` | `Error \| null` | Partial save failure |
|
|
103
|
+
| | `isLoading` | `boolean` | Partial save in progress |
|
|
104
|
+
| `state` | `phase` | `SurveyPhase` | Survey flow phase (see below) |
|
|
105
|
+
| | `currentPageIndex` | `number` | 0-based page index (when `phase.kind === 'content'`) |
|
|
106
|
+
| | `currentQuestions` | `SurveyQuestion[]` | Questions on current page; `questionText` and `questionNumber` resolved by SDK |
|
|
107
|
+
| | `answers` | `SurveyAnswers` | All answers so far |
|
|
61
108
|
| | `validationErrors` | `Record<string, string>` | Per-question error messages |
|
|
62
|
-
| | `progressPercentage` | `number` | 0–100 completion |
|
|
109
|
+
| | `progressPercentage` | `number` | 0–100 path-scoped completion (see `progress.md`) |
|
|
110
|
+
| | `totalQuestionsCount` | `number` | Answerable questions on active path (progress denominator) |
|
|
111
|
+
| | `answeredQuestionsCount` | `number` | Earned credits on active path (progress numerator) |
|
|
112
|
+
| | `canGoBack` | `boolean` | Show Back button |
|
|
113
|
+
| | `primaryButtonType` | `'next' \| 'submit'` | Primary nav label; SDK maps `NEXT` → `SUBMIT` when `'submit'` |
|
|
114
|
+
| | `specialPage` | `SpecialPage \| undefined` | Intro / end / paused page content when applicable |
|
|
63
115
|
| `onAction` | — | `(action: SurveyAction) => void` | **Only** way to mutate state |
|
|
64
116
|
|
|
117
|
+
### Survey phases (`state.phase.kind`)
|
|
118
|
+
|
|
119
|
+
| Phase | When | UI |
|
|
120
|
+
|-------|------|-----|
|
|
121
|
+
| `intro` | Survey has intro page | Render intro; dispatch `START` |
|
|
122
|
+
| `content` | Active questions | Question map + nav |
|
|
123
|
+
| `end` | Successful submit | End/thank-you page from `specialPage` |
|
|
124
|
+
| `paused` | Survey paused flag | Paused message (read-only) |
|
|
125
|
+
| `submitted` | Already filled | Terminal message |
|
|
126
|
+
| `expired` | Survey expired | Terminal message |
|
|
127
|
+
| `quota_fulfilled` | Quota reached | Terminal message |
|
|
128
|
+
|
|
65
129
|
## Action Dispatching
|
|
66
130
|
|
|
67
131
|
Never mutate `state` directly. Use `onAction`:
|
|
68
132
|
|
|
69
133
|
```typescript
|
|
70
134
|
// Answer a question
|
|
71
|
-
onAction({ type: ACTIONS.CHANGE, payload: { questionId: 'q1',
|
|
135
|
+
onAction({ type: ACTIONS.CHANGE, payload: { questionId: 'q1', answerValue: 4 } });
|
|
72
136
|
|
|
73
137
|
// Matrix answer
|
|
74
|
-
onAction({ type: ACTIONS.CHANGE, payload: { questionId: 'q2',
|
|
138
|
+
onAction({ type: ACTIONS.CHANGE, payload: { questionId: 'q2', answerValue: { row1: 5, row2: 2 } } });
|
|
139
|
+
|
|
140
|
+
// Intro → content
|
|
141
|
+
onAction({ type: ACTIONS.START });
|
|
75
142
|
|
|
76
143
|
// Navigate
|
|
77
|
-
onAction({ type: ACTIONS.NEXT }); // Validates current page;
|
|
78
|
-
onAction({ type: ACTIONS.PREVIOUS });
|
|
79
|
-
onAction({ type: ACTIONS.SUBMIT }); // Validates
|
|
144
|
+
onAction({ type: ACTIONS.NEXT }); // Validates current page; maps to SUBMIT when primaryButtonType is 'submit'
|
|
145
|
+
onAction({ type: ACTIONS.PREVIOUS }); // Uses pageHistory (skip-aware Back)
|
|
146
|
+
onAction({ type: ACTIONS.SUBMIT }); // Validates non-skipped pages, then submits
|
|
147
|
+
|
|
148
|
+
// Lifecycle (client detects scroll / visibility, SDK handles API calls)
|
|
149
|
+
onAction({ type: ACTIONS.SCROLL }); // First scroll — marks instance as started (once)
|
|
150
|
+
onAction({
|
|
151
|
+
type: ACTIONS.RECORD_QUESTION_VIEW,
|
|
152
|
+
payload: { questionIds: ['q1'] }, // Question entered viewport
|
|
153
|
+
});
|
|
80
154
|
```
|
|
81
155
|
|
|
82
|
-
String literals also work: `'NEXT'`, `'PREVIOUS'`, `'SUBMIT'`, `'CHANGE'`.
|
|
156
|
+
String literals also work: `'START'`, `'NEXT'`, `'PREVIOUS'`, `'SUBMIT'`, `'CHANGE'`, `'SCROLL'`, `'RECORD_QUESTION_VIEW'`.
|
|
83
157
|
|
|
84
158
|
## Validation
|
|
85
159
|
|
|
86
|
-
- Required questions must be answered before `NEXT` or `SUBMIT`
|
|
160
|
+
- Required questions must be answered before `NEXT` or `SUBMIT` on the **current page**
|
|
161
|
+
- `NEXT` validates current page only; skip logic may bypass intermediate pages
|
|
162
|
+
- `SUBMIT` validates **non-skipped pages** only (pages bypassed by skip logic are excluded)
|
|
87
163
|
- `null` is a valid answer (N/A option)
|
|
88
|
-
- `
|
|
164
|
+
- `TEXT_AND_MEDIA` is display-only — always skipped for validation
|
|
89
165
|
- On failure, SDK scrolls to first error via `document.getElementById(questionId)` — **requires `id={question.id}` on question wrappers**
|
|
90
166
|
|
|
167
|
+
See [`skip-logic-and-navigation.md`](skip-logic-and-navigation.md) for skip, Back, and answer-sanitize behavior.
|
|
168
|
+
|
|
91
169
|
## Submission
|
|
92
170
|
|
|
93
|
-
Triggered by `onAction({ type: ACTIONS.SUBMIT })` or `NEXT`
|
|
171
|
+
Triggered by `onAction({ type: ACTIONS.SUBMIT })` or `NEXT` when `primaryButtonType === 'submit'`. While submitting, `submitSurveyResults.isLoading` is `true`. On success, render end UI when `state.phase.kind === 'end'` (and `state.specialPage` for content). `submitSurveyResults.data` confirms API success.
|
|
172
|
+
|
|
173
|
+
## Mark as Started
|
|
174
|
+
|
|
175
|
+
On the **first scroll or answer change**, the client should dispatch `onAction({ type: ACTIONS.SCROLL })` (once, on first scroll) and/or the SDK fires automatically on `CHANGE`. Both call a one-time GET to `markResponseInstanceAsStarted`. Fire-and-forget — errors do not block the UI.
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
const onScroll = () => onAction({ type: ACTIONS.SCROLL });
|
|
180
|
+
window.addEventListener('scroll', onScroll, { passive: true, once: true });
|
|
181
|
+
return () => window.removeEventListener('scroll', onScroll);
|
|
182
|
+
}, [onAction]);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Question Activity (VIEWED)
|
|
186
|
+
|
|
187
|
+
When a question enters the viewport, dispatch `RECORD_QUESTION_VIEW` with its id(s). The SDK POSTs to `recordQuestionActivityV2` with deduplication (each question recorded once). Requires `id={question.id}` on question wrappers.
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
const observer = new IntersectionObserver(entries => {
|
|
192
|
+
for (const entry of entries) {
|
|
193
|
+
if (entry.isIntersecting && entry.target.id) {
|
|
194
|
+
onAction({
|
|
195
|
+
type: ACTIONS.RECORD_QUESTION_VIEW,
|
|
196
|
+
payload: { questionIds: [entry.target.id] },
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}, { threshold: 0.5 });
|
|
201
|
+
|
|
202
|
+
state.currentQuestions.forEach(q => {
|
|
203
|
+
const el = document.getElementById(q.id);
|
|
204
|
+
if (el) observer.observe(el);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return () => observer.disconnect();
|
|
208
|
+
}, [state.currentQuestions, onAction]);
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Partial Save
|
|
212
|
+
|
|
213
|
+
When `savePartialResponse: true` (or `{ debounceMs: 1500 }`) is passed in options, the SDK debounces answer changes and POSTs partial responses with `surveySubmitted=false`. Partial saves begin only after the first user interaction (not MCQ default seeding). Monitor `partialSaveResults.isLoading` / `partialSaveResults.error` if you need UI feedback.
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
useSurveySDK({
|
|
217
|
+
options: {
|
|
218
|
+
instanceId,
|
|
219
|
+
savePartialResponse: true,
|
|
220
|
+
// or: savePartialResponse: { debounceMs: 2000 },
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
```
|
|
94
224
|
|
|
95
225
|
## Language & Placeholders
|
|
96
226
|
|
|
@@ -98,10 +228,8 @@ Triggered by `onAction({ type: ACTIONS.SUBMIT })` or `NEXT` on the last page. Wh
|
|
|
98
228
|
- `survey.languages` lists available languages for `LanguageSelector`
|
|
99
229
|
- Placeholders replace `{{FIRST_NAME}}` tokens in question text at map time
|
|
100
230
|
|
|
101
|
-
## CSAT
|
|
231
|
+
## CSAT / rating emoji UI (client-implemented)
|
|
102
232
|
|
|
103
|
-
|
|
104
|
-
import { CsatEmojiSet, getEmojiForIndex, CsatStarIcons } from '@explorer02/cfm-survey-sdk';
|
|
105
|
-
```
|
|
233
|
+
CSAT emoji and star icons are **not** exported from the SDK. Implement client-side with `react-icons` (or your icon library). Map column index to sentiment icons proportionally for non-5-point scales.
|
|
106
234
|
|
|
107
|
-
|
|
235
|
+
See `03-ui-specs/04-csat.md` and [`question-type-sdk-matrix.md#csat_matrix`](question-type-sdk-matrix.md#csat_matrix).
|
|
@@ -3,91 +3,113 @@
|
|
|
3
3
|
> **UI spec (authoritative):** `03-ui-specs/12-survey-chrome.md`
|
|
4
4
|
> **Reference implementation:** `apps/client/src/components/SurveyPage.tsx`
|
|
5
5
|
> **Target Component**: `SurveyPage`
|
|
6
|
-
> **Role**:
|
|
6
|
+
> **Role**: Wires `useSurveySDK` to the UI — loading/error states, phase routing, pagination, submission.
|
|
7
7
|
|
|
8
8
|
## Core Responsibility
|
|
9
|
-
This component MUST act as the state machine for the entire survey flow. It should never render questions directly—it delegates that to `Question.tsx`.
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
Act as the **phase state machine** for the survey. Never render questions directly — delegate to `Question.tsx`.
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
## Setup & Wiring (CRITICAL)
|
|
14
13
|
|
|
15
14
|
```tsx
|
|
15
|
+
import type { CustomFieldValueMap } from '@explorer02/cfm-survey-sdk';
|
|
16
|
+
|
|
16
17
|
const SURVEY_PLACEHOLDERS = { FIRST_NAME: 'Customer' }; // Must be outside component!
|
|
18
|
+
const CUSTOM_FIELD_VALUES: CustomFieldValueMap = {}; // Populate when survey uses custom-field logic
|
|
17
19
|
|
|
18
20
|
export default function SurveyPage() {
|
|
19
|
-
const [selectedLanguage, setSelectedLanguage] = useState<string | undefined>(
|
|
21
|
+
const [selectedLanguage, setSelectedLanguage] = useState<string | undefined>();
|
|
20
22
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
instanceId: 'YOUR_INSTANCE_ID_HERE',
|
|
23
|
+
const surveyOptions = useMemo(
|
|
24
|
+
() => ({
|
|
25
|
+
instanceId: 'YOUR_INSTANCE_ID_HERE',
|
|
24
26
|
language: selectedLanguage,
|
|
25
27
|
placeholders: SURVEY_PLACEHOLDERS,
|
|
28
|
+
customFieldValues: CUSTOM_FIELD_VALUES,
|
|
26
29
|
debug: true,
|
|
27
|
-
}
|
|
30
|
+
}),
|
|
31
|
+
[selectedLanguage]
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const { surveyQueryResults, submitSurveyResults, state, onAction } = useSurveySDK({
|
|
35
|
+
options: surveyOptions,
|
|
28
36
|
});
|
|
29
|
-
|
|
37
|
+
|
|
30
38
|
const survey = surveyQueryResults.data;
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
const allQuestions = useMemo(
|
|
40
|
+
() => survey?.pages.flatMap(page => page.questions) ?? [],
|
|
41
|
+
[survey]
|
|
42
|
+
);
|
|
43
|
+
// ... phase routing below
|
|
33
44
|
}
|
|
34
45
|
```
|
|
35
46
|
|
|
36
|
-
|
|
47
|
+
See [`client-integration-guide.md`](../00-integration/client-integration-guide.md) for lifecycle effects (`SCROLL`, `RECORD_QUESTION_VIEW`).
|
|
37
48
|
|
|
38
|
-
|
|
49
|
+
## Render Order (MANDATORY)
|
|
39
50
|
|
|
40
|
-
|
|
41
|
-
If `surveyQueryResults.isLoading` is true, render a Skeleton or Spinner.
|
|
42
|
-
*Include your CSS-only Header and Footer around the spinner.*
|
|
51
|
+
Handle in this order:
|
|
43
52
|
|
|
44
|
-
###
|
|
45
|
-
If `surveyQueryResults.error` is true, render a red Error banner containing `surveyQueryResults.error.message`.
|
|
53
|
+
### 1. Loading / Error / Empty
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
| Condition | UI |
|
|
56
|
+
|-----------|-----|
|
|
57
|
+
| `surveyQueryResults.isLoading` | Spinner + Header/Footer |
|
|
58
|
+
| `surveyQueryResults.error` | Error message |
|
|
59
|
+
| `!survey` | "No survey data" |
|
|
49
60
|
|
|
50
|
-
###
|
|
51
|
-
If `submitSurveyResults.data` exists, the survey is complete.
|
|
52
|
-
*Render the Thank You Screen (e.g., "Thank you for your feedback!"). Do not render the question list.*
|
|
61
|
+
### 2. Special phases (`state.phase`)
|
|
53
62
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
| `phase.kind` | Component | Action |
|
|
64
|
+
|--------------|-----------|--------|
|
|
65
|
+
| `intro` | `IntroPage` | `onAction({ type: 'START' })` |
|
|
66
|
+
| `paused` | `PausedPage` | Read-only |
|
|
67
|
+
| `end` | `EndPage` | Thank-you from `state.specialPage` |
|
|
68
|
+
| `submitted` | Terminal message | Already filled |
|
|
69
|
+
| `expired` | Terminal message | Survey expired |
|
|
70
|
+
| `quota_fulfilled` | Terminal message | Quota reached |
|
|
71
|
+
|
|
72
|
+
See `14-intro-page.md`, `15-end-page.md`, `16-paused-page.md`.
|
|
73
|
+
|
|
74
|
+
### 3. Active content (`phase.kind === 'content'`)
|
|
75
|
+
|
|
76
|
+
1. `Header`
|
|
77
|
+
2. `ProgressBar` (`state.progressPercentage`) — see `00-integration/progress.md`
|
|
78
|
+
3. `LanguageSelector`
|
|
79
|
+
4. Question map (below)
|
|
80
|
+
5. Navigation buttons (below)
|
|
61
81
|
6. `Footer`
|
|
62
82
|
|
|
63
83
|
## The Question Map
|
|
64
84
|
|
|
65
|
-
|
|
85
|
+
Pass **all answers and questions** for placeholder resolution (`{{SURVEY_QUESTION-:-qId}}`):
|
|
66
86
|
|
|
67
87
|
```tsx
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
{state.currentQuestions.map(question => (
|
|
89
|
+
<Question
|
|
90
|
+
key={question.id}
|
|
91
|
+
question={question}
|
|
92
|
+
selectedValue={state.answers[question.id]}
|
|
93
|
+
validationError={state.validationErrors[question.id]}
|
|
94
|
+
allAnswers={state.answers}
|
|
95
|
+
allQuestions={allQuestions}
|
|
96
|
+
customFieldValues={CUSTOM_FIELD_VALUES}
|
|
97
|
+
onSelect={answerValue =>
|
|
98
|
+
onAction({ type: 'CHANGE', payload: { questionId: question.id, answerValue } })
|
|
99
|
+
}
|
|
100
|
+
/>
|
|
101
|
+
))}
|
|
82
102
|
```
|
|
83
103
|
|
|
84
|
-
## Navigation Buttons
|
|
104
|
+
## Navigation Buttons
|
|
85
105
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
-
|
|
89
|
-
- `onClick={() => onAction({ type: 'PREVIOUS' })}`
|
|
90
|
-
- **Next/Submit Button**:
|
|
106
|
+
- **Back:** Render when `state.canGoBack` — `onAction({ type: 'PREVIOUS' })`
|
|
107
|
+
- **Primary:** Label from `state.primaryButtonType` (`'next'` | `'submit'`)
|
|
108
|
+
- Always dispatch `onAction({ type: 'NEXT' })` — SDK maps to submit when `primaryButtonType === 'submit'`
|
|
91
109
|
- `disabled={submitSurveyResults.isLoading}`
|
|
92
|
-
|
|
93
|
-
|
|
110
|
+
|
|
111
|
+
Do **not** use `currentPageIndex === 0` alone to hide Back — intro page and skip-aware history use `canGoBack`.
|
|
112
|
+
|
|
113
|
+
## Scroll & question activity
|
|
114
|
+
|
|
115
|
+
Wire `SCROLL` (once) and `RECORD_QUESTION_VIEW` as documented in `useSurveySDK.md`.
|