@explorer02/cfm-survey-sdk 0.2.2 → 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/package.json +1 -1
- package/templates/AGENT.md +8 -4
- package/templates/docs/00-integration/analytics-events-catalog.md +54 -0
- package/templates/docs/00-integration/client-integration-guide.md +1 -1
- package/templates/docs/00-integration/component-checklist.md +10 -0
- package/templates/docs/00-integration/display-logic-and-navigation.md +16 -1
- 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 +24 -0
- package/templates/docs/00-integration/question-type-sdk-matrix.md +11 -9
- package/templates/docs/00-integration/setup.md +3 -3
- package/templates/docs/00-integration/skip-logic-and-navigation.md +1 -1
- package/templates/docs/00-integration/survey-lifecycle-analytics.md +1 -1
- package/templates/docs/01-components/06-likert-matrix-scale.md +2 -1
- package/templates/docs/01-components/08-file-upload-scale.md +15 -52
- package/templates/docs/01-components/13-matrix-dropdown.md +28 -33
- package/templates/docs/01-components/17-heatmap-scale.md +4 -4
- package/templates/docs/01-components/18-rank-order-scale.md +24 -31
- package/templates/docs/02-reference/question-types/11-file-upload.md +36 -21
- package/templates/docs/02-reference/question-types/README.md +11 -1
- package/templates/docs/03-ui-specs/01-rating.md +10 -2
- package/templates/docs/03-ui-specs/02-radio.md +80 -25
- package/templates/docs/03-ui-specs/04-csat.md +35 -5
- package/templates/docs/03-ui-specs/07-matrix-cfm.md +20 -4
- package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +5 -1
- package/templates/docs/03-ui-specs/09-slider-matrix.md +17 -0
- package/templates/docs/03-ui-specs/10-file-upload.md +41 -20
- package/templates/docs/03-ui-specs/12-survey-chrome.md +38 -0
- package/templates/docs/03-ui-specs/13-heatmap.md +9 -2
- package/templates/docs/03-ui-specs/14-rank-order.md +98 -29
- package/templates/docs/03-ui-specs/README.md +7 -5
- package/templates/docs/03-ui-specs/shared/custom-slider-track.md +28 -35
- package/templates/docs/MANIFEST.json +28 -3
- package/templates/docs/index.md +5 -4
- package/templates/docs/templates/CustomSliderTrack.tsx +144 -0
- package/templates/docs/templates/MatrixDropdown.tsx +216 -0
- package/templates/docs/templates/RankOrderScale.tsx +353 -0
- package/templates/docs/templates/implementation_plan.md +20 -0
- package/templates/docs/templates/survey-inventory.schema.json +28 -23
- package/templates/docs/templates/surveyUiIcons.tsx +52 -0
- package/templates/docs/templates/verify-agent-build.sh +32 -0
|
@@ -1,46 +1,41 @@
|
|
|
1
1
|
# Matrix Dropdown
|
|
2
2
|
|
|
3
|
-
> **UI spec (authoritative):** `03-ui-specs/shared/matrix-dropdown.md`
|
|
4
|
-
> **
|
|
5
|
-
> **Component
|
|
6
|
-
> **Used by
|
|
3
|
+
> **UI spec (authoritative):** [`03-ui-specs/shared/matrix-dropdown.md`](../03-ui-specs/shared/matrix-dropdown.md) — implement the **custom panel** (not a native `<select>`).
|
|
4
|
+
> **Portable template:** copy [`templates/MatrixDropdown.tsx`](../templates/MatrixDropdown.tsx) when available.
|
|
5
|
+
> **Component:** `MatrixDropdown`
|
|
6
|
+
> **Used by:** `CsatMatrixScale`, `LikertMatrixScale`
|
|
7
7
|
|
|
8
8
|
## Role
|
|
9
|
-
Renders a dropdown selector for a single row in a matrix question. Used when `gridLayout === 'dropdown'` or `displayStyle === 'dropdown'`.
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
Renders a custom dropdown selector for a single matrix row. Used when `gridLayout === 'dropdown'` or `displayStyle` is `'dropdown'` / `'selectbox'`.
|
|
11
|
+
|
|
12
|
+
Emits stored column **`id`** values via `matrixColumnStoredValue(col)` — see [`02-reference/value-derivation.md`](../02-reference/value-derivation.md).
|
|
13
|
+
|
|
14
|
+
## Props (wiring only)
|
|
12
15
|
|
|
13
|
-
## Props Interface
|
|
14
16
|
```typescript
|
|
15
17
|
type MatrixDropdownProps = {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
options: ScaleColumn[]; // visible columns for this row
|
|
19
|
+
value: string | number | null | undefined;
|
|
18
20
|
onChange: (value: string | number | null) => void;
|
|
21
|
+
multiple?: boolean;
|
|
19
22
|
placeholder?: string;
|
|
20
23
|
};
|
|
21
24
|
```
|
|
22
25
|
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
<option value="" disabled>Select an option...</option>
|
|
40
|
-
{columns.map(col => (
|
|
41
|
-
<option key={String(col.value)} value={String(col.value)}>
|
|
42
|
-
{col.label}
|
|
43
|
-
</option>
|
|
44
|
-
))}
|
|
45
|
-
</select>
|
|
46
|
-
```
|
|
26
|
+
## Implementation
|
|
27
|
+
|
|
28
|
+
Do **not** use a native `<select>`. Follow the shared ui-spec for:
|
|
29
|
+
|
|
30
|
+
- Click-to-open panel with click-outside dismiss
|
|
31
|
+
- Single-select: one choice; multi-select: tag chips + toggle
|
|
32
|
+
- N/A mutual exclusion when `hasNotApplicableOption` applies
|
|
33
|
+
- Placeholder text: `"Select..."` (dropdown) or full-width cards (selectbox)
|
|
34
|
+
|
|
35
|
+
See [`03-ui-specs/shared/matrix-dropdown.md`](../03-ui-specs/shared/matrix-dropdown.md) for layout, states, and agent checklist.
|
|
36
|
+
|
|
37
|
+
## SDK Integration Checklist
|
|
38
|
+
|
|
39
|
+
- [ ] Copy `templates/MatrixDropdown.tsx` or implement per shared ui-spec
|
|
40
|
+
- [ ] `onChange` stores column **`id`** via `matrixColumnStoredValue(col)`
|
|
41
|
+
- [ ] Filter columns with `getVisibleMatrixItemsForAnswers` before passing to dropdown
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **UI spec (authoritative):** `03-ui-specs/13-heatmap.md`
|
|
4
4
|
> **Reference implementation:** `apps/client/src/components/HeatmapScale.tsx`
|
|
5
|
-
> **
|
|
5
|
+
> **Portable coord helper:** copy [`templates/heatmapCoords.ts`](../templates/heatmapCoords.ts)
|
|
6
6
|
> **Target Component**: `HeatmapScale`
|
|
7
7
|
> **Handles**: `type: 'HEATMAP'`
|
|
8
8
|
|
|
@@ -15,8 +15,8 @@ Render the heatmap image, capture click spots in normalized 0–1 coordinates, t
|
|
|
15
15
|
```typescript
|
|
16
16
|
type HeatmapScaleProps = {
|
|
17
17
|
question: HeatmapQuestion;
|
|
18
|
-
selectedValue?:
|
|
19
|
-
onSelect: (answer:
|
|
18
|
+
selectedValue?: HeatmapClickPoint[];
|
|
19
|
+
onSelect: (answer: HeatmapClickPoint[] | undefined) => void;
|
|
20
20
|
};
|
|
21
21
|
```
|
|
22
22
|
|
|
@@ -34,7 +34,7 @@ type HeatmapScaleProps = {
|
|
|
34
34
|
|
|
35
35
|
## Coordinate Helpers
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
Copy [`templates/heatmapCoords.ts`](../templates/heatmapCoords.ts):
|
|
38
38
|
|
|
39
39
|
- `getNormalizedClickFromImage(event, imageElement)`
|
|
40
40
|
- `findSpotNearClick(clickX, clickY, spots, imageRect)`
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Rank Order Architectural Blueprint
|
|
2
2
|
|
|
3
3
|
> **UI spec (authoritative):** `03-ui-specs/14-rank-order.md`
|
|
4
|
-
> **
|
|
4
|
+
> **Portable template:** copy [`templates/RankOrderScale.tsx`](../templates/RankOrderScale.tsx)
|
|
5
5
|
> **Target Component:** `RankOrderScale`
|
|
6
6
|
> **Handles:** `type: 'RANK_ORDER'`
|
|
7
7
|
|
|
8
8
|
## Core Responsibility
|
|
9
9
|
|
|
10
|
-
Render rankable options in
|
|
10
|
+
Render rankable options in **dropdown** or **drag-and-drop** mode, three option display variants, bubble ranks via `onSelect`.
|
|
11
11
|
|
|
12
12
|
## Props Contract
|
|
13
13
|
|
|
@@ -21,9 +21,9 @@ type RankOrderScaleProps = {
|
|
|
21
21
|
|
|
22
22
|
## State Management
|
|
23
23
|
|
|
24
|
-
- `selectedValue` from SDK is source of truth —
|
|
25
|
-
- Dropdown
|
|
26
|
-
- Drag-and-drop
|
|
24
|
+
- `selectedValue` from SDK is source of truth — no duplicate local answer state
|
|
25
|
+
- Dropdown: `assignRankWithoutDuplicates` on rank change
|
|
26
|
+
- Drag-and-drop: `buildRankOrderFromOrderedOptionIds` after reorder; `getOrderedOptionIdsFromRanks` for display order
|
|
27
27
|
|
|
28
28
|
## Layout Orchestrator
|
|
29
29
|
|
|
@@ -36,20 +36,9 @@ export function RankOrderScale({ question, selectedValue, onSelect }: RankOrderS
|
|
|
36
36
|
}
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
##
|
|
39
|
+
## shuffleOptions
|
|
40
40
|
|
|
41
|
-
-
|
|
42
|
-
- Left: rank `<select>` with `-` (unassigned) and `1..N`
|
|
43
|
-
- Selecting rank `R` clears `R` from any other option (SDK helper)
|
|
44
|
-
- Option content depends on `question.optionDisplay`
|
|
45
|
-
- **Image URLs:** prefer `option.imageUrl` over `option.previewImageUrl` — preview `*_p.*` URLs often return 403 on CloudFront
|
|
46
|
-
|
|
47
|
-
## Drag-and-Drop Mode
|
|
48
|
-
|
|
49
|
-
- Use `@dnd-kit/core` + `@dnd-kit/sortable` for smooth reorder
|
|
50
|
-
- List order maps to ranks: position 0 → rank 1
|
|
51
|
-
- Show rank badge on each row
|
|
52
|
-
- Provide Up/Down buttons for keyboard accessibility
|
|
41
|
+
When API `SHUFFLE_ORDER` is true, SDK shuffles options at map time. **Do not re-shuffle in the client** — logic and option ids must stay aligned with fetched order.
|
|
53
42
|
|
|
54
43
|
## Wiring in Question.tsx
|
|
55
44
|
|
|
@@ -57,6 +46,7 @@ export function RankOrderScale({ question, selectedValue, onSelect }: RankOrderS
|
|
|
57
46
|
import {
|
|
58
47
|
QUESTION_TYPE,
|
|
59
48
|
getVisibleRankOrderOptionsForAnswers,
|
|
49
|
+
normalizeRankOrderAnswers,
|
|
60
50
|
} from '@explorer02/cfm-survey-sdk';
|
|
61
51
|
|
|
62
52
|
const rankOrderQuestion = useMemo(() => {
|
|
@@ -64,10 +54,7 @@ const rankOrderQuestion = useMemo(() => {
|
|
|
64
54
|
return {
|
|
65
55
|
...question,
|
|
66
56
|
options: getVisibleRankOrderOptionsForAnswers(
|
|
67
|
-
question,
|
|
68
|
-
allAnswers,
|
|
69
|
-
allQuestions,
|
|
70
|
-
customFieldValues
|
|
57
|
+
question, allAnswers, allQuestions, customFieldValues
|
|
71
58
|
),
|
|
72
59
|
};
|
|
73
60
|
}, [question, allAnswers, allQuestions, customFieldValues]);
|
|
@@ -75,7 +62,11 @@ const rankOrderQuestion = useMemo(() => {
|
|
|
75
62
|
{question.type === QUESTION_TYPE.RANK_ORDER && rankOrderQuestion && (
|
|
76
63
|
<RankOrderScale
|
|
77
64
|
question={rankOrderQuestion}
|
|
78
|
-
selectedValue={
|
|
65
|
+
selectedValue={normalizeRankOrderAnswers(
|
|
66
|
+
typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue)
|
|
67
|
+
? selectedValue
|
|
68
|
+
: undefined
|
|
69
|
+
)}
|
|
79
70
|
onSelect={onSelect}
|
|
80
71
|
/>
|
|
81
72
|
)}
|
|
@@ -83,18 +74,20 @@ const rankOrderQuestion = useMemo(() => {
|
|
|
83
74
|
|
|
84
75
|
## Dependencies
|
|
85
76
|
|
|
86
|
-
|
|
77
|
+
```bash
|
|
78
|
+
npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Next.js SSR
|
|
87
82
|
|
|
88
|
-
-
|
|
89
|
-
- `@dnd-kit/sortable`
|
|
90
|
-
- `@dnd-kit/utilities`
|
|
83
|
+
Template uses `'use client'`. For drag-and-drop, optional `dynamic(..., { ssr: false })` in Question.tsx if hydration warnings occur.
|
|
91
84
|
|
|
92
85
|
## SDK Integration Checklist (agent)
|
|
93
86
|
|
|
87
|
+
- [ ] Copy [`templates/RankOrderScale.tsx`](../templates/RankOrderScale.tsx)
|
|
94
88
|
- [ ] Type literal: `QUESTION_TYPE.RANK_ORDER`
|
|
95
|
-
- [ ] Answer
|
|
96
|
-
- [ ]
|
|
97
|
-
- [ ]
|
|
98
|
-
- [ ] Rank utils: `assignRankWithoutDuplicates`, `buildRankOrderFromOrderedOptionIds`, `normalizeRankOrderAnswers`
|
|
89
|
+
- [ ] Answer: `RankOrderAnswers`
|
|
90
|
+
- [ ] Visibility: `getVisibleRankOrderOptionsForAnswers(..., customFieldValues?)`
|
|
91
|
+
- [ ] Utils: `assignRankWithoutDuplicates`, `buildRankOrderFromOrderedOptionIds`, `normalizeRankOrderAnswers`
|
|
99
92
|
- [ ] Prefer `option.imageUrl` over `previewImageUrl`
|
|
100
93
|
- [ ] Matrix row: [`question-type-sdk-matrix.md#rank_order`](../00-integration/question-type-sdk-matrix.md#rank_order)
|
|
@@ -2,44 +2,59 @@
|
|
|
2
2
|
|
|
3
3
|
> **Type**: `'FILE_UPLOAD'` | **API Source**: `FILE_UPLOAD`
|
|
4
4
|
> **Mapper**: `fileUploadMapper.ts` | **Component**: `FileUploadScale`
|
|
5
|
+
> **Upload flow**: [`00-integration/file-upload-aws.md`](../../00-integration/file-upload-aws.md)
|
|
5
6
|
|
|
6
7
|
## TypeScript Type
|
|
7
8
|
|
|
8
9
|
```typescript
|
|
9
10
|
type FileUploadQuestion = QuestionBase & {
|
|
10
11
|
type: 'FILE_UPLOAD';
|
|
11
|
-
uploadMessage?: string;
|
|
12
|
-
supportedFileFormats?: string[];
|
|
13
|
-
maxFileCount?: number;
|
|
14
|
-
fileSizeLimit?: number;
|
|
12
|
+
uploadMessage?: string;
|
|
13
|
+
supportedFileFormats?: string[];
|
|
14
|
+
maxFileCount?: number;
|
|
15
|
+
fileSizeLimit?: number;
|
|
15
16
|
fileSizeLimitType?: 'PER_FILE' | 'IN_TOTAL';
|
|
16
17
|
};
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
/** Stored in state.answers — required for submit */
|
|
20
|
+
type UploadedFileAnswer = {
|
|
21
|
+
id: string;
|
|
22
|
+
mediaUrl: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
mimeType?: string;
|
|
25
|
+
size?: number;
|
|
21
26
|
};
|
|
22
27
|
```
|
|
23
28
|
|
|
24
|
-
## Answer Shape: `
|
|
25
|
-
|
|
29
|
+
## Answer Shape: `UploadedFileAnswer[]`
|
|
30
|
+
|
|
31
|
+
Submitted via `quesIdVsAttachmentDetails` (NOT `questionToAnswers`). SDK submit formatter only accepts objects with `id` and `mediaUrl` — **raw browser `File[]` is dropped at submit**.
|
|
32
|
+
|
|
33
|
+
## Two-phase client flow
|
|
34
|
+
|
|
35
|
+
1. **Pick + validate locally** — extension, count, size (`PER_FILE` / `IN_TOTAL`)
|
|
36
|
+
2. **Upload each file** — `POST /get-upload-url` → presigned PUT → collect `{ id, mediaUrl, name, mimeType, size }`
|
|
37
|
+
3. **`onAction(CHANGE)`** — pass `UploadedFileAnswer[]` to SDK
|
|
38
|
+
|
|
39
|
+
See [`file-upload-aws.md`](../../00-integration/file-upload-aws.md) for API contract.
|
|
26
40
|
|
|
27
41
|
## Rendering Guidance
|
|
28
|
-
|
|
42
|
+
|
|
43
|
+
- Drag-and-drop zone + click-to-upload
|
|
29
44
|
- Show accepted formats from `supportedFileFormats`
|
|
30
|
-
- Set `<input type="file" accept=".pdf,.png
|
|
31
|
-
- Enforce `maxFileCount`
|
|
32
|
-
- Show file list with name, size,
|
|
33
|
-
-
|
|
34
|
-
|
|
45
|
+
- Set `<input type="file" accept=".pdf,.png,...">` from formats
|
|
46
|
+
- Enforce `maxFileCount` and `fileSizeLimit` before upload
|
|
47
|
+
- Show file list with name, size, remove button, uploading spinner per file
|
|
48
|
+
- `onSelect(undefined)` when all files removed
|
|
49
|
+
|
|
50
|
+
## Skip logic
|
|
51
|
+
|
|
52
|
+
Presence-only (`EXISTS` / `MISSING`) — evaluated on page entry; no discrete value-change events required.
|
|
35
53
|
|
|
36
54
|
## SDK Integration Checklist (agent)
|
|
37
55
|
|
|
38
56
|
- [ ] Type literal: `QUESTION_TYPE.FILE_UPLOAD`
|
|
39
|
-
- [ ] Answer stored as: `
|
|
40
|
-
- [ ] onAction CHANGE
|
|
41
|
-
- [ ]
|
|
42
|
-
- [ ] Custom fields: skip/display rules only — no visibility helper
|
|
43
|
-
- [ ] Logic features: skip ✓ display ✓ answer ✗ variants ✓ numbering ✓
|
|
44
|
-
- [ ] Performance: upload to presigned URL before CHANGE; show progress UI
|
|
57
|
+
- [ ] Answer stored as: `UploadedFileAnswer[]` after upload completes
|
|
58
|
+
- [ ] onAction CHANGE: `{ questionId, answerValue: UploadedFileAnswer[] }`
|
|
59
|
+
- [ ] Do **not** store raw `File[]` as final answer
|
|
45
60
|
- [ ] Matrix row: [`question-type-sdk-matrix.md#file_upload`](../../00-integration/question-type-sdk-matrix.md#file_upload)
|
|
@@ -49,10 +49,20 @@ Each type is **standalone** — open its file in exported SDK types for the comp
|
|
|
49
49
|
- **Answer:** `string` or `string[]` (`option.id` values)
|
|
50
50
|
- **Options:** `McqOption[]` with `id`, `optionLabel`
|
|
51
51
|
|
|
52
|
+
| Field | Type | API source |
|
|
53
|
+
|-------|------|------------|
|
|
54
|
+
| `selectionMode` | `'single' \| 'multiple'` | multi-select config |
|
|
55
|
+
| `defaultOptionIds?` | `string[]` | `DEFAULT_ANSWER` when enabled |
|
|
56
|
+
| `minSelections?` | `number` | `ANSWER_CONDITION` / `MIN_ANSWER` |
|
|
57
|
+
| `maxSelections?` | `number` | `ANSWER_CONDITION` / `MAX_ANSWER` |
|
|
58
|
+
| `answerConditionErrorMessage?` | `string` | `ANSWER_CONDITION` error message |
|
|
59
|
+
|
|
60
|
+
**Default answers:** On survey load, SDK `buildInitialAnswers` / `applyMcqDefaults` pre-populates `state.answers[questionId]` from `defaultOptionIds` (single: first id; multi: full array). Client binds UI checked state to `state.answers` — do not maintain separate default state. SDK re-applies defaults on validation/submit if answer is still empty.
|
|
61
|
+
|
|
52
62
|
### SDK Integration Checklist (agent)
|
|
53
63
|
|
|
54
64
|
- [ ] Type literal: `QUESTION_TYPE.MCQ`
|
|
55
|
-
- [ ] Answer stored as: `option.id` (string/number) or array when
|
|
65
|
+
- [ ] Answer stored as: `option.id` (string/number) or array when `selectionMode === 'multiple'`
|
|
56
66
|
- [ ] onAction CHANGE payload: `{ questionId, answerValue: optionId | optionId[] }`
|
|
57
67
|
- [ ] Visibility helper: `getVisibleMcqOptionsForAnswers(question, answers, allQuestions, customFieldValues?)`
|
|
58
68
|
- [ ] Custom fields: 4th arg when logic references CRM fields
|
|
@@ -17,8 +17,16 @@
|
|
|
17
17
|
| `midLabel` | ✅ | Absolute positioned in Question.tsx |
|
|
18
18
|
| `maxLabel` | ✅ | Absolute positioned in Question.tsx |
|
|
19
19
|
| `midLabelIndex` | ✅ | Mid label `left` percentage |
|
|
20
|
-
| `reverseScaleOrder` |
|
|
21
|
-
| `buttonVariant` | ❌ | Client
|
|
20
|
+
| `reverseScaleOrder` | ✅ | Reverse option index before selection/color lookup |
|
|
21
|
+
| `buttonVariant` | ❌ | Client uses dual-layer radio layout |
|
|
22
|
+
|
|
23
|
+
## Answer logic on scale points
|
|
24
|
+
|
|
25
|
+
When scale points have answer logic, filter with `getVisibleScaleColumnsForAnswers(question, answers, allQuestions, customFieldValues?)` before render.
|
|
26
|
+
|
|
27
|
+
## Implementation Recipe — reverseScaleOrder
|
|
28
|
+
|
|
29
|
+
When `reverseScaleOrder === true`, map visual index `i` to stored index `options.length - 1 - i` before deriving submit value and badge color.
|
|
22
30
|
|
|
23
31
|
## Answer Mutation
|
|
24
32
|
|
|
@@ -2,44 +2,99 @@
|
|
|
2
2
|
|
|
3
3
|
> **SDK type:** `McqQuestion` — `fetchSurvey/types/mcq.ts`
|
|
4
4
|
> **Component blueprint:** `01-components/02-question.md`
|
|
5
|
+
> **Dispatcher:** inline in `Question.tsx` (see [`templates/Question.tsx`](../templates/Question.tsx))
|
|
5
6
|
|
|
6
7
|
## Config Inventory
|
|
7
8
|
|
|
8
|
-
| Field |
|
|
9
|
-
|
|
10
|
-
| `options[]` |
|
|
11
|
-
| `options[].optionLabel` |
|
|
12
|
-
| `options[].id` |
|
|
13
|
-
| `
|
|
9
|
+
| Field | UI impact |
|
|
10
|
+
|-------|-----------|
|
|
11
|
+
| `options[]` | One card per visible option |
|
|
12
|
+
| `options[].optionLabel` | HTML via `dangerouslySetInnerHTML` |
|
|
13
|
+
| `options[].id` | **Submit value** — answer is `option.id` |
|
|
14
|
+
| `selectionMode` | `'single'` → radio cards; `'multiple'` → checkbox cards + `string[]` answer |
|
|
15
|
+
| `defaultOptionIds?` | Pre-selected on load (SDK seeds `state.answers`) |
|
|
16
|
+
| `minSelections?` / `maxSelections?` | Answer-condition count (SDK validates on NEXT) |
|
|
17
|
+
| `answerConditionErrorMessage?` | Custom validation banner text |
|
|
18
|
+
| `requiredErrorMessage?` | Required-field error text |
|
|
14
19
|
|
|
15
|
-
##
|
|
20
|
+
## Selection mode (not `isMultiSelect`)
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
Use `question.selectionMode` from the SDK — **never** `isMultiSelect`.
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
| `selectionMode` | UI | Answer shape |
|
|
25
|
+
|-----------------|-----|--------------|
|
|
26
|
+
| `'single'` (default) | Radio cards; one selected | `string` (option `id`) |
|
|
27
|
+
| `'multiple'` | Checkbox cards; toggle on click | `string[]` of option ids |
|
|
23
28
|
|
|
24
|
-
|
|
29
|
+
Multi-select toggle pattern:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
const handleToggle = (optionId: string) => {
|
|
33
|
+
const next = selectedIds.includes(optionId)
|
|
34
|
+
? selectedIds.filter(id => id !== optionId)
|
|
35
|
+
: question.maxSelections && selectedIds.length >= question.maxSelections
|
|
36
|
+
? selectedIds // block add at max
|
|
37
|
+
: [...selectedIds, optionId];
|
|
38
|
+
onSelect(next);
|
|
39
|
+
};
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Filter options before render: `getVisibleMcqOptionsForAnswers(question, allAnswers, allQuestions, customFieldValues?)`.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Implementation Recipe — Default answers
|
|
47
|
+
|
|
48
|
+
| Concern | SDK behavior | Client UI contract |
|
|
49
|
+
|---------|--------------|-------------------|
|
|
50
|
+
| **Config** | API `DEFAULT_ANSWER` → `question.defaultOptionIds?: string[]` | Inventory in implementation plan §4b |
|
|
51
|
+
| **Initial load** | `buildInitialAnswers` / `applyMcqDefaults` writes into `state.answers` | Bind checked state to `selectedValue` (`state.answers[question.id]`) |
|
|
52
|
+
| **Single-select** | Default = `defaultOptionIds[0]` | Pre-selected radio when present in `state.answers` |
|
|
53
|
+
| **Multi-select** | Default = full `defaultOptionIds` array | Pre-checked boxes for each id |
|
|
54
|
+
| **User override** | `applyMcqDefaults` never overwrites user selection | Toggle via `onAction(CHANGE)` |
|
|
55
|
+
| **Validation / submit** | SDK re-applies defaults in `validateCurrentPage` and submit if empty | Client does **not** re-apply defaults at NEXT/SUBMIT |
|
|
56
|
+
| **Answer logic** | Hidden defaults may exist in `state.answers` until SDK sanitizes | Show checked state only for **visible** options |
|
|
57
|
+
| **Template fallback** | `Question.tsx` uses `selectedValue ?? defaultOptionIds` defensively | Prefer `state.answers`; fallback is display-only |
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Implementation Recipe — Answer conditions
|
|
62
|
+
|
|
63
|
+
When API `ANSWER_CONDITION` is enabled, SDK maps:
|
|
64
|
+
|
|
65
|
+
- `minSelections` — minimum selected count (multi-select)
|
|
66
|
+
- `maxSelections` — maximum selected count (multi-select)
|
|
67
|
+
- `answerConditionErrorMessage` — shown in `state.validationErrors[questionId]`
|
|
68
|
+
|
|
69
|
+
**Client must:**
|
|
70
|
+
|
|
71
|
+
1. Render `validationError` banner when SDK sets an error (do not duplicate count validation).
|
|
72
|
+
2. Block adding options when `selectedIds.length >= maxSelections` in multi-select UI.
|
|
73
|
+
3. Pass through `onAction(CHANGE)` with updated array — SDK validates on NEXT.
|
|
74
|
+
|
|
75
|
+
Skip logic may use `NUM_OF_SELECTED_OPTIONS` — see [`logic-fields-catalog.md`](../00-integration/logic-fields-catalog.md).
|
|
76
|
+
|
|
77
|
+
---
|
|
25
78
|
|
|
26
79
|
## Answer Mutation
|
|
27
80
|
|
|
28
81
|
```typescript
|
|
29
|
-
|
|
30
|
-
|
|
82
|
+
// Single-select
|
|
83
|
+
onSelect(option.id)
|
|
84
|
+
onAction({ type: 'CHANGE', payload: { questionId, answerValue: string } })
|
|
85
|
+
|
|
86
|
+
// Multi-select
|
|
87
|
+
onSelect(selectedIds) // string[]
|
|
88
|
+
onAction({ type: 'CHANGE', payload: { questionId, answerValue: string[] } })
|
|
31
89
|
```
|
|
32
90
|
|
|
33
91
|
## Agent Checklist
|
|
34
92
|
|
|
35
|
-
- [ ]
|
|
36
|
-
- [ ]
|
|
37
|
-
- [ ] Selected: magenta border + pink
|
|
38
|
-
- [ ]
|
|
39
|
-
- [ ]
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- [ ] Visibility helper: `getVisibleMcqOptionsForAnswers`
|
|
44
|
-
- [ ] Store `option.id` — never option label text
|
|
93
|
+
- [ ] Use `selectionMode`, not `isMultiSelect`
|
|
94
|
+
- [ ] Full-width card options; custom circular radio / checkbox indicators
|
|
95
|
+
- [ ] Selected: magenta border `#e20074` + pink bg `#fdf2f8`
|
|
96
|
+
- [ ] Multi-select: `string[]` answer; block at `maxSelections`
|
|
97
|
+
- [ ] Defaults: bind to `state.answers`; pre-selected visual on load when `defaultOptionIds` set
|
|
98
|
+
- [ ] `getVisibleMcqOptionsForAnswers` with optional `customFieldValues` 4th arg
|
|
99
|
+
- [ ] Store `option.id` — never label text
|
|
45
100
|
- [ ] Matrix row: [`question-type-sdk-matrix.md#mcq`](../00-integration/question-type-sdk-matrix.md#mcq)
|
|
@@ -38,12 +38,42 @@ CSAT_MATRIX
|
|
|
38
38
|
- Alternating row backgrounds
|
|
39
39
|
- Column headers OR `scaleAnchorLabels[]` anchor row with percentage positioning
|
|
40
40
|
|
|
41
|
-
## Carousel
|
|
41
|
+
## Carousel — Implementation Recipe
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
Same rules as CFM carousel (prev/next disabled at bounds, dot click jumps row, no auto-advance). CSAT uses **pink** active dot `#e20074` (not Likert blue).
|
|
44
|
+
|
|
45
|
+
## N/A mutex
|
|
46
|
+
|
|
47
|
+
| Layout | Behavior |
|
|
48
|
+
|--------|----------|
|
|
49
|
+
| Grid N/A column | Selecting N/A clears row; selecting column clears N/A |
|
|
50
|
+
| Vertical checkbox | `hasNotApplicableOption` checkbox below row sets `null` |
|
|
51
|
+
|
|
52
|
+
## Emoji / star / numbered — react-icons mapping
|
|
53
|
+
|
|
54
|
+
Copy [`templates/surveyUiIcons.tsx`](../templates/surveyUiIcons.tsx) or implement `getEmojiForIndex(index, totalColumns)`.
|
|
55
|
+
|
|
56
|
+
**5-point CSAT emoji index → icon key:**
|
|
57
|
+
|
|
58
|
+
| Column index (0-based) | Sentiment | Maps to 11-point scale key |
|
|
59
|
+
|--------------------------|-----------|----------------------------|
|
|
60
|
+
| 0 | Very negative | 3 |
|
|
61
|
+
| 1 | Negative | 4 |
|
|
62
|
+
| 2 | Neutral | 6 |
|
|
63
|
+
| 3 | Positive | 7 |
|
|
64
|
+
| 4 | Very positive | 9 |
|
|
65
|
+
|
|
66
|
+
**Star mode:** filled `FaStar` `#e20074` when selected; `FaRegStar` `#d1d5db` opacity 0.5 when unselected; hover ring `ring-2 ring-[#e20074] ring-offset-2`.
|
|
67
|
+
|
|
68
|
+
**Numbered mode:** 40×40 badge; selected = magenta fill `#e20074` white text; unselected = gray border.
|
|
69
|
+
|
|
70
|
+
**Graphics mode:** `CustomSliderTrack` per row with `ticks={scaleColumns.length}`, `tickLabels={scaleColumns.map(c => c.label)}`, `sliderType="graphics"`.
|
|
71
|
+
|
|
72
|
+
Apply `reverseScaleOrder` by flipping column index before icon lookup.
|
|
73
|
+
|
|
74
|
+
## Scale column labels on slider (Day 19)
|
|
75
|
+
|
|
76
|
+
Pass `tickLabels={visibleScaleColumns.map(col => col.label)}` to `CustomSliderTrack` so tooltips and graphics thumb show mapped labels, not raw numeric indices.
|
|
47
77
|
|
|
48
78
|
## Answer Mutation
|
|
49
79
|
|
|
@@ -46,12 +46,28 @@ CFM_MATRIX
|
|
|
46
46
|
- Zebra rows: even rows `rgba(249,250,251,0.8)`
|
|
47
47
|
- Native inputs: `accentColor: #e20074`, 18×18px
|
|
48
48
|
|
|
49
|
-
## Carousel
|
|
49
|
+
## Carousel — Implementation Recipe
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
| Rule | Behavior |
|
|
52
|
+
|------|----------|
|
|
53
|
+
| Prev button | Disabled when `currentRowIndex === 0` |
|
|
54
|
+
| Next button | Disabled when on last visible row |
|
|
55
|
+
| Dot indicators | Click dot → jump to that row index |
|
|
56
|
+
| On answer | **Do not** auto-advance to next row |
|
|
57
|
+
| Keyboard | Optional Left/Right for prev/next |
|
|
58
|
+
| Active dot (Likert) | `#2563eb`, expanded pill `20×8px` vs `8×8px` inactive |
|
|
53
59
|
|
|
54
|
-
|
|
60
|
+
One statement row visible at a time; column headers below cells when `showColumnHeaders`.
|
|
61
|
+
|
|
62
|
+
## N/A mutex (all layouts)
|
|
63
|
+
|
|
64
|
+
| Action | Result |
|
|
65
|
+
|--------|--------|
|
|
66
|
+
| Select N/A | Clear other column values for row; other inputs opacity 0.4 + disabled |
|
|
67
|
+
| Select column after N/A | Clear N/A first |
|
|
68
|
+
| Multi-select + N/A | N/A sets `[null]` exclusively |
|
|
69
|
+
|
|
70
|
+
## Implementation Recipe
|
|
55
71
|
|
|
56
72
|
Store **`column.id`** via `matrixColumnStoredValue(col)` — see `02-reference/value-derivation.md`.
|
|
57
73
|
|
|
@@ -19,7 +19,11 @@
|
|
|
19
19
|
|
|
20
20
|
## Config Decision Tree
|
|
21
21
|
|
|
22
|
-
Same as `CSAT_MATRIX`
|
|
22
|
+
Same as `CSAT_MATRIX` — reuse `CsatMatrixScale`. See [04-csat.md](04-csat.md) for emoji/star/carousel/N/A/graphics recipes.
|
|
23
|
+
|
|
24
|
+
## Carousel / N/A / scale labels
|
|
25
|
+
|
|
26
|
+
Follow CSAT Implementation Recipes in [04-csat.md](04-csat.md): pink carousel dots, N/A mutex, `tickLabels` from `scaleColumns[].label` on graphics sliders.
|
|
23
27
|
|
|
24
28
|
## Answer Mutation
|
|
25
29
|
|
|
@@ -36,6 +36,23 @@ sliceWidth = tickValues.length > 1 ? 100 / (tickValues.length - 1) : 100
|
|
|
36
36
|
// graphics: plain text label instead of colored badge
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
## CustomSliderTrack per row — scale labels
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
<CustomSliderTrack
|
|
43
|
+
min={row.min}
|
|
44
|
+
max={row.max}
|
|
45
|
+
htmlStep={row.htmlStep}
|
|
46
|
+
value={rowVal as number}
|
|
47
|
+
tickLabels={row.tickLabels ?? scaleColumns.map(c => c.label)}
|
|
48
|
+
ticks={row.ticks}
|
|
49
|
+
sliderType={question.sliderStyle === 'graphics' ? 'graphics' : undefined}
|
|
50
|
+
onChange={v => updateRow(row.id, v)}
|
|
51
|
+
/>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Pass **`scaleColumns[].label`** (or row tick labels) as `tickLabels` — required for correct tooltip text (Day 19 fix).
|
|
55
|
+
|
|
39
56
|
## N/A Behavior
|
|
40
57
|
|
|
41
58
|
- Checked → `rowVal === 'N/A'`; slider disabled, input grayed
|