@explorer02/cfm-survey-sdk 0.1.3 → 0.1.5
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/postinstall.js +30 -18
- package/templates/AGENT.md +10 -1906
- package/templates/docs/01-sdk-core/01-fetch-survey.md +68 -0
- package/templates/docs/01-sdk-core/02-survey-mapper.md +85 -0
- package/templates/docs/01-sdk-core/03-question-mappers.md +114 -0
- package/templates/docs/01-sdk-core/04-pagination.md +72 -0
- package/templates/docs/01-sdk-core/05-validation.md +66 -0
- package/templates/docs/01-sdk-core/06-submit-survey.md +90 -0
- package/templates/docs/01-sdk-core/07-language-handling.md +111 -0
- package/templates/docs/01-sdk-core/08-icons-and-emojis.md +88 -0
- package/templates/docs/01-sdk-core/README.md +53 -0
- package/templates/docs/02-question-types/01-rating.md +52 -0
- package/templates/docs/02-question-types/02-radio.md +26 -0
- package/templates/docs/02-question-types/03-text.md +26 -0
- package/templates/docs/02-question-types/04-csat.md +54 -0
- package/templates/docs/02-question-types/05-rating-scale.md +26 -0
- package/templates/docs/02-question-types/06-slider.md +30 -0
- package/templates/docs/02-question-types/07-matrix-cfm.md +43 -0
- package/templates/docs/02-question-types/08-matrix-csat.md +29 -0
- package/templates/docs/02-question-types/09-matrix-rating.md +28 -0
- package/templates/docs/02-question-types/10-slider-matrix.md +40 -0
- package/templates/docs/02-question-types/11-file-upload.md +34 -0
- package/templates/docs/02-question-types/12-text-and-media.md +35 -0
- package/templates/docs/02-question-types/README.md +74 -0
- package/templates/docs/03-client-components/01-survey-page.md +113 -0
- package/templates/docs/03-client-components/02-question.md +57 -0
- package/templates/docs/03-client-components/03-rating-scale.md +38 -0
- package/templates/docs/03-client-components/04-csat-scale.md +40 -0
- package/templates/docs/03-client-components/05-csat-matrix-scale.md +43 -0
- package/templates/docs/03-client-components/06-likert-matrix-scale.md +38 -0
- package/templates/docs/03-client-components/07-slider-matrix-scale.md +44 -0
- package/templates/docs/03-client-components/08-file-upload-scale.md +33 -0
- package/templates/docs/03-client-components/09-custom-slider-track.md +27 -0
- package/templates/docs/03-client-components/10-header-footer.md +46 -0
- package/templates/docs/03-client-components/11-progress-bar.md +31 -0
- package/templates/docs/03-client-components/12-language-selector.md +41 -0
- package/templates/docs/03-client-components/13-matrix-dropdown.md +42 -0
- package/templates/docs/03-client-components/README.md +63 -0
- package/templates/docs/04-critical-rules/01-import-rules.md +51 -0
- package/templates/docs/04-critical-rules/02-action-dispatching.md +56 -0
- package/templates/docs/04-critical-rules/03-scroll-navigation.md +37 -0
- package/templates/docs/04-critical-rules/04-logo-branding.md +42 -0
- package/templates/docs/04-critical-rules/05-troubleshooting.md +46 -0
- package/templates/docs/04-critical-rules/README.md +29 -0
- package/templates/docs/index.md +129 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Fetch Survey — `useFetchSurvey`
|
|
2
|
+
|
|
3
|
+
> **Source**: `packages/sdk/src/fetchSurvey/useFetchSurvey.ts`
|
|
4
|
+
> **Called by**: `useSurveySDK()` internally — the client never calls this directly.
|
|
5
|
+
|
|
6
|
+
## What It Does
|
|
7
|
+
|
|
8
|
+
1. Builds the API URL from the client's `instanceId` (JWT token)
|
|
9
|
+
2. Makes a `GET` request to the Sprinklr CFM API endpoint
|
|
10
|
+
3. Caches the raw response using `@tanstack/react-query` (singleton QueryClient — no provider needed)
|
|
11
|
+
4. Re-maps the cached response whenever the target language changes (client-side, no re-fetch)
|
|
12
|
+
|
|
13
|
+
## API Endpoint
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
https://custom-p0.feedbook.me/api/feedback/survey/cfm?instanceId={JWT_TOKEN}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The URL is constructed by `buildSurveyUrl()` in `fetchSurvey/utils/url.ts`.
|
|
20
|
+
|
|
21
|
+
## Instance ID
|
|
22
|
+
|
|
23
|
+
The `instanceId` is a **JWT token** string that encodes:
|
|
24
|
+
- `surveyId` — Which survey to load
|
|
25
|
+
- `distributionEntityId` — Which distribution channel
|
|
26
|
+
- `surveyLanguage` — Default language (e.g. `'de'`)
|
|
27
|
+
- `partnerId` — Tenant identifier
|
|
28
|
+
|
|
29
|
+
**⚠️ CRITICAL**: The instanceId comes from the **client's chat prompt**. The agent must ask the client for it before building the survey. Without it, `useFetchSurvey` returns `null` and the survey cannot load.
|
|
30
|
+
|
|
31
|
+
## Return Shape
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
{
|
|
35
|
+
survey: Survey | null, // Fully mapped survey object (see 02-survey-mapper.md)
|
|
36
|
+
session: SurveySession | null, // { instanceId, language } — passed to submit hook
|
|
37
|
+
fetching: boolean, // True while network request is in flight
|
|
38
|
+
fetchError: Error | null, // Network/API error
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Caching Behaviour
|
|
43
|
+
|
|
44
|
+
- Keyed by `['survey', url]` — fetched **once** per instanceId
|
|
45
|
+
- Language changes do NOT re-fetch — they re-map the cached raw response via `useMemo`
|
|
46
|
+
- Uses `cache: 'force-cache'` on the fetch request for browser-level HTTP caching
|
|
47
|
+
|
|
48
|
+
## QueryClient
|
|
49
|
+
|
|
50
|
+
The SDK creates its own internal singleton `QueryClient` in `src/queryClient.ts`.
|
|
51
|
+
The client app does **NOT** need to wrap anything in `<QueryClientProvider>`.
|
|
52
|
+
|
|
53
|
+
## Logging & Telemetry
|
|
54
|
+
|
|
55
|
+
The fetch lifecycle emits structured logs (when `debug: true`):
|
|
56
|
+
- `logFetchStart(url)` — before the request
|
|
57
|
+
- `logFetchPerfSuccess(durationMs, status, url, payloadSize)` — on success
|
|
58
|
+
- `logFetchPerfFailure(durationMs, status, url)` — on failure
|
|
59
|
+
- `trackFetchLatency(...)` — performance telemetry
|
|
60
|
+
|
|
61
|
+
## Error Handling
|
|
62
|
+
|
|
63
|
+
| Scenario | Behaviour |
|
|
64
|
+
|----------|-----------|
|
|
65
|
+
| Network offline | `fetchError` populated, `survey` is `null` |
|
|
66
|
+
| HTTP 4xx/5xx | `fetchError` populated with status message |
|
|
67
|
+
| Invalid instanceId | API returns empty/malformed → `survey` maps to `null` |
|
|
68
|
+
| Successful but empty | `survey` is `null`, no error |
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Survey Mapper — `mapSurvey`
|
|
2
|
+
|
|
3
|
+
> **Source**: `packages/sdk/src/fetchSurvey/utils/mappers/surveyMapper.ts`
|
|
4
|
+
> **Called by**: `useFetchSurvey()` inside `useMemo` — re-runs when language changes.
|
|
5
|
+
|
|
6
|
+
## What It Does
|
|
7
|
+
|
|
8
|
+
Transforms the raw API response (`ApiResponse`) into a clean, typed `Survey` object:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
ApiResponse (raw network JSON)
|
|
12
|
+
│
|
|
13
|
+
├── extractQuestions() → Flat list of ApiQuestion[]
|
|
14
|
+
├── resolveSurveyLanguage() → Determine active language code
|
|
15
|
+
├── extractSurveyLanguages()→ Build SurveyLanguage[] for language switcher
|
|
16
|
+
└── mapSurveyPages() → Pages → Questions → per-type mappers
|
|
17
|
+
│
|
|
18
|
+
└── mapQuestion() per question (see 03-question-mappers.md)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Output: `Survey` Type
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
type Survey = {
|
|
25
|
+
id: string; // Survey identifier
|
|
26
|
+
language: string; // Active language code (e.g. 'en', 'de')
|
|
27
|
+
languages: SurveyLanguage[]; // All available languages for the switcher
|
|
28
|
+
pages: SurveyPageData[]; // Ordered list of pages
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type SurveyPageData = {
|
|
32
|
+
id: string; // Page identifier
|
|
33
|
+
title?: string; // Optional page heading
|
|
34
|
+
questions: SurveyQuestion[]; // Questions on this page
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type SurveyLanguage = {
|
|
38
|
+
code: string; // BCP-47 code (e.g. 'de')
|
|
39
|
+
name: string; // Display name (e.g. 'Deutsch')
|
|
40
|
+
};
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Key Functions
|
|
44
|
+
|
|
45
|
+
### `extractQuestions(api)`
|
|
46
|
+
Traverses `api.cfmSurveyDTO.surveyPages[].questionHolders[].question` to produce a flat array of raw API questions.
|
|
47
|
+
|
|
48
|
+
### `resolveSurveyLanguage(api, language?)`
|
|
49
|
+
Priority: `language` parameter → `api.surveyLanguage` → `api.cfmSurveyDTO.baseLanguage` → `'en'`
|
|
50
|
+
|
|
51
|
+
### `extractSession(api)`
|
|
52
|
+
Returns `{ instanceId, language }` or `null` if no valid instanceId exists. This session object is passed to the submit hook.
|
|
53
|
+
|
|
54
|
+
### `mapSurveyPages(api, language, placeholders)`
|
|
55
|
+
Iterates over `api.cfmSurveyDTO.surveyPages`, calling `mapQuestion()` for each question within each page's `questionHolders`. Returns `SurveyPageData[]`.
|
|
56
|
+
|
|
57
|
+
## API Response Structure (Simplified)
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
type ApiResponse = {
|
|
61
|
+
instanceId: string;
|
|
62
|
+
surveyLanguage: string;
|
|
63
|
+
cfmSurveyDTO: {
|
|
64
|
+
id: string;
|
|
65
|
+
baseLanguage: string;
|
|
66
|
+
surveyPages: Array<{
|
|
67
|
+
id: string;
|
|
68
|
+
pageTitle?: string;
|
|
69
|
+
questionHolders: Array<{
|
|
70
|
+
question: ApiQuestion;
|
|
71
|
+
}>;
|
|
72
|
+
}>;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Language Re-mapping
|
|
78
|
+
|
|
79
|
+
When the user switches language via the `LanguageSelector`:
|
|
80
|
+
1. `selectedLanguage` state changes in `SurveyPage.tsx`
|
|
81
|
+
2. This flows into `useSurveySDK({ options: { language: selectedLanguage } })`
|
|
82
|
+
3. `useFetchSurvey` does **NOT re-fetch** — the raw `ApiResponse` is cached
|
|
83
|
+
4. `useMemo` re-runs `mapSurvey(apiResponse, newLanguage, placeholders)`
|
|
84
|
+
5. Each question's text, options, and labels are resolved from `translations[language]`
|
|
85
|
+
6. The entire UI re-renders with translated content
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Question Mappers — `mapQuestion`
|
|
2
|
+
|
|
3
|
+
> **Source**: `packages/sdk/src/fetchSurvey/utils/mappers/questionMappers/index.ts`
|
|
4
|
+
> **Called by**: `mapSurveyPages()` for each question in each page.
|
|
5
|
+
|
|
6
|
+
## What It Does
|
|
7
|
+
|
|
8
|
+
Takes a raw `ApiQuestion` and produces a fully typed `SurveyQuestion` variant. This is the core routing logic that determines what question type the client component will render.
|
|
9
|
+
|
|
10
|
+
## Dispatch Pipeline
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
ApiQuestion
|
|
14
|
+
│
|
|
15
|
+
├── resolveBase(raw, configs, language, placeholders)
|
|
16
|
+
│ → { id, text, description, required, requiredErrorMessage, containerMedia* }
|
|
17
|
+
│
|
|
18
|
+
├── normalizeType(raw.answerType)
|
|
19
|
+
│ → 'rating' | 'text' | 'matrix' | 'rating_matrix' | 'slider_matrix'
|
|
20
|
+
│ | 'text_and_media' | 'file_upload' | 'radio' (default)
|
|
21
|
+
│
|
|
22
|
+
└── switch(type) → per-type mapper
|
|
23
|
+
├── 'rating' → mapRatingQuestion() [handles NPS, CSAT, Slider, RatingScale]
|
|
24
|
+
├── 'text' → mapTextQuestion()
|
|
25
|
+
├── 'matrix' → mapMatrixQuestion() [handles CFM, CSAT, RATING matrix]
|
|
26
|
+
├── 'rating_matrix' → mapMatrixQuestion() [same handler, different subType]
|
|
27
|
+
├── 'slider_matrix' → mapSliderMatrixQuestion()
|
|
28
|
+
├── 'text_and_media' → mapTextAndMediaQuestion()
|
|
29
|
+
├── 'file_upload' → mapFileUploadQuestion()
|
|
30
|
+
└── default → mapRadioQuestion() [MCQ and any unrecognised type]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## `normalizeType(answerType)` — Type Resolution
|
|
34
|
+
|
|
35
|
+
| API `answerType` | Normalized Type | Notes |
|
|
36
|
+
|-----------------|----------------|-------|
|
|
37
|
+
| `'SCALE'` | `'rating'` | Further sub-typed inside ratingMapper |
|
|
38
|
+
| `'RATING'` | `'rating'` | Legacy alias |
|
|
39
|
+
| `'RATING_MATRIX'` | `'rating_matrix'` | Grid with NPS-style columns |
|
|
40
|
+
| `'TEXTFIELD'` | `'text'` | Short or long text input |
|
|
41
|
+
| `'MCQ'` | `'radio'` | Multiple choice (single select) |
|
|
42
|
+
| `'CFM_MATRIX'` | `'matrix'` | Likert-style grid |
|
|
43
|
+
| `'CSAT_MATRIX'` | `'matrix'` | Satisfaction grid with emoji/star |
|
|
44
|
+
| `'SLIDER_MATRIX'` | `'slider_matrix'` | Grid of range sliders |
|
|
45
|
+
| `'TEXT_AND_MEDIA'` | `'text_and_media'` | Display-only media card |
|
|
46
|
+
| `'FILE_UPLOAD'` | `'file_upload'` | File attachment question |
|
|
47
|
+
| Any other | `'radio'` | Safe fallback |
|
|
48
|
+
|
|
49
|
+
## `resolveBase()` — Shared Fields
|
|
50
|
+
|
|
51
|
+
Every question variant inherits these base fields:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
type QuestionBase = {
|
|
55
|
+
id: string; // ⚠️ MUST be used as DOM element id
|
|
56
|
+
text: string; // May contain HTML — use dangerouslySetInnerHTML
|
|
57
|
+
description?: string; // Supplementary instruction text (HTML)
|
|
58
|
+
required?: boolean; // Whether answer is mandatory
|
|
59
|
+
requiredErrorMessage?: string; // Custom validation error text
|
|
60
|
+
questionNumber?: number; // 1-indexed ordinal position
|
|
61
|
+
containerMediaUrl?: string; // Image/video shown alongside question
|
|
62
|
+
containerMediaMimeType?: string; // MIME type for container media
|
|
63
|
+
containerMediaAlignment?: string;// Layout position of container media
|
|
64
|
+
containerMediaSize?: number; // Width percentage of container media
|
|
65
|
+
};
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Text Resolution Priority
|
|
69
|
+
```
|
|
70
|
+
language translation (richText) → language translation (plainText)
|
|
71
|
+
→ default richText → default plainText → ''
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Placeholder Replacement
|
|
75
|
+
The `resolveBase` function replaces `{{TOKEN}}` patterns in question text and description:
|
|
76
|
+
```typescript
|
|
77
|
+
text.replace(/\{\{(\w+)\}\}/g, (match, key) =>
|
|
78
|
+
placeholders[key] !== undefined ? placeholders[key] : match
|
|
79
|
+
);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Rating Mapper Sub-Dispatch
|
|
83
|
+
|
|
84
|
+
The `mapRatingQuestion()` handler further sub-types based on `questionConfigs`:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
SCALE answerType
|
|
88
|
+
│
|
|
89
|
+
├── SCALE_TYPE=CSAT (or ≤5 options) → type: 'csat'
|
|
90
|
+
├── SCALE_TYPE=STAR/EMOJI/FIVE_POINT → type: 'rating_scale'
|
|
91
|
+
├── INPUT_TYPE=SLIDER → type: 'slider'
|
|
92
|
+
└── default (NPS/TEN_POINT) → type: 'rating'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
This means the client component's `Question.tsx` needs to handle **all these refined types**, not just the raw `'rating'` type.
|
|
96
|
+
|
|
97
|
+
## Final Output: `SurveyQuestion` Union
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
type SurveyQuestion =
|
|
101
|
+
| RatingQuestion // type: 'rating'
|
|
102
|
+
| RadioQuestion // type: 'radio'
|
|
103
|
+
| TextQuestion // type: 'text'
|
|
104
|
+
| MatrixQuestion // type: 'matrix'
|
|
105
|
+
| RatingMatrixQuestion // type: 'rating_matrix'
|
|
106
|
+
| SliderMatrixQuestion // type: 'slider_matrix'
|
|
107
|
+
| TextAndMediaQuestion // type: 'text_and_media'
|
|
108
|
+
| CsatQuestion // type: 'csat'
|
|
109
|
+
| SliderQuestion // type: 'slider'
|
|
110
|
+
| RatingScaleQuestion // type: 'rating_scale'
|
|
111
|
+
| FileUploadQuestion; // type: 'file_upload'
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Each type is documented in detail in `02-question-types/`.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Pagination — `useSurveyPagination`
|
|
2
|
+
|
|
3
|
+
> **Source**: `packages/sdk/src/surveyPagination/useSurveyPagination.ts`
|
|
4
|
+
> **Called by**: `useSurveySDK()` internally.
|
|
5
|
+
|
|
6
|
+
## What It Does
|
|
7
|
+
|
|
8
|
+
Manages the multi-page survey state: current page index, user answers, validation errors, page navigation, and wires into submission and progress tracking.
|
|
9
|
+
|
|
10
|
+
## State Shape
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
{
|
|
14
|
+
currentPageIndex: number, // Zero-based active page
|
|
15
|
+
values: Record<string, AnswerValue>, // All answers keyed by question ID
|
|
16
|
+
validationErrors: Record<string, string>, // Error messages keyed by question ID
|
|
17
|
+
currentQuestions: SurveyQuestion[], // Questions on the active page
|
|
18
|
+
progressPercentage: number, // 0–100 completion percentage
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Navigation Actions
|
|
23
|
+
|
|
24
|
+
### `onNext()` (dispatched by `onAction({ type: 'NEXT' })`)
|
|
25
|
+
1. Validates all questions on the current page
|
|
26
|
+
2. If validation fails → populates `validationErrors` → scrolls to first error
|
|
27
|
+
3. If validation passes → clears errors → increments `currentPageIndex`
|
|
28
|
+
4. If already on the last page → auto-redirects to `onSubmit()`
|
|
29
|
+
|
|
30
|
+
### `onBack()` (dispatched by `onAction({ type: 'PREVIOUS' })`)
|
|
31
|
+
1. Decrements `currentPageIndex` (minimum 0)
|
|
32
|
+
2. Scrolls to the top of the new page
|
|
33
|
+
3. Logs backtrack analytics
|
|
34
|
+
|
|
35
|
+
### `onChange(questionId, value)` (dispatched by `onAction({ type: 'CHANGE', payload })`)
|
|
36
|
+
1. Updates `values[questionId] = value`
|
|
37
|
+
2. If the question had a validation error, clears it immediately
|
|
38
|
+
|
|
39
|
+
### `onSubmit()` (dispatched by `onAction({ type: 'SUBMIT' })` or auto-triggered)
|
|
40
|
+
1. Validates ALL pages (not just current)
|
|
41
|
+
2. If any page has errors → navigates to the error page → scrolls to first error
|
|
42
|
+
3. If all valid → calls `submit(values)` → sends response to server
|
|
43
|
+
|
|
44
|
+
## Progress Tracking
|
|
45
|
+
|
|
46
|
+
`useSurveyProgress` computes `progressPercentage`:
|
|
47
|
+
- Counts total answerable questions across ALL pages (excludes `text_and_media`)
|
|
48
|
+
- Counts questions that have a non-empty value in `values`
|
|
49
|
+
- Formula: `(answered / total) * 100`
|
|
50
|
+
|
|
51
|
+
## Internal Refs (Analytics)
|
|
52
|
+
|
|
53
|
+
The hook maintains a `useRef` for analytics data that persists across renders:
|
|
54
|
+
- `pageStartTime` — timestamp when current page was entered
|
|
55
|
+
- `sessionStartTime` — timestamp when survey was first loaded
|
|
56
|
+
- `maxVisitedPageIndex` — furthest page the user has reached
|
|
57
|
+
- `validationErrorsCount` — total validation failures during the session
|
|
58
|
+
- `backtrackCount` — number of times user navigated backwards
|
|
59
|
+
|
|
60
|
+
These are used for telemetry and do NOT affect the UI.
|
|
61
|
+
|
|
62
|
+
## Auto-Submit on Last Page
|
|
63
|
+
|
|
64
|
+
When `onAction({ type: 'NEXT' })` is dispatched on the last page, the SDK's `useSurveySDK` dispatcher automatically converts it to `{ type: 'SUBMIT' }`:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
if (action.type === 'NEXT' && currentPageIndex === survey.pages.length - 1) {
|
|
68
|
+
action = { type: 'SUBMIT' };
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This means the client's "Next" button on the last page automatically becomes "Submit" in behaviour.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Validation — `validateQuestions` & `validateAllPages`
|
|
2
|
+
|
|
3
|
+
> **Source**: `packages/sdk/src/surveyPagination/utils/validation.ts`
|
|
4
|
+
> **Called by**: `useOnNext()` and `useOnSubmit()` internally.
|
|
5
|
+
|
|
6
|
+
## What It Does
|
|
7
|
+
|
|
8
|
+
Validates required questions before allowing page navigation or survey submission. Populates `state.validationErrors` with per-question error messages.
|
|
9
|
+
|
|
10
|
+
## Validation Rules
|
|
11
|
+
|
|
12
|
+
### Standard Questions (rating, radio, text, csat, rating_scale, slider)
|
|
13
|
+
- **Required check**: `value !== undefined && value !== ''`
|
|
14
|
+
- `null` is treated as a **valid** answer (represents the N/A option)
|
|
15
|
+
|
|
16
|
+
### Matrix Questions (matrix, rating_matrix, slider_matrix)
|
|
17
|
+
- Every row must have a value: `question.rows.every(row => answerMap[row.id] !== undefined && answerMap[row.id] !== '')`
|
|
18
|
+
- `null` per row is valid (N/A option)
|
|
19
|
+
|
|
20
|
+
### Display-Only Questions (text_and_media)
|
|
21
|
+
- **Always skipped** — `isDisplayOnly()` returns `true`
|
|
22
|
+
- Never counted in validation or progress
|
|
23
|
+
|
|
24
|
+
## Error Messages
|
|
25
|
+
|
|
26
|
+
| Source | Priority |
|
|
27
|
+
|--------|----------|
|
|
28
|
+
| `question.requiredErrorMessage` (from API) | **Highest** — custom per-question message |
|
|
29
|
+
| Language-aware default | Fallback |
|
|
30
|
+
|
|
31
|
+
Default messages:
|
|
32
|
+
- German (`de`): `"Beantworten Sie bitte diese Frage bevor Sie fortfahren."`
|
|
33
|
+
- All other: `"Please answer this question before you proceed."`
|
|
34
|
+
|
|
35
|
+
## Scroll-to-Error
|
|
36
|
+
|
|
37
|
+
When validation fails, the SDK:
|
|
38
|
+
1. Identifies the first question with an error
|
|
39
|
+
2. Calls `document.getElementById(questionId)` to find the DOM element
|
|
40
|
+
3. Calls `.scrollIntoView({ behavior: 'smooth', block: 'center' })` to scroll to it
|
|
41
|
+
|
|
42
|
+
**⚠️ CRITICAL**: This is why every question wrapper element MUST have `id={question.id}`.
|
|
43
|
+
|
|
44
|
+
## `validateQuestions(questions, values, language)`
|
|
45
|
+
|
|
46
|
+
Validates a single page's questions. Returns `Record<string, string>` of errors (empty if all valid).
|
|
47
|
+
|
|
48
|
+
## `validateAllPages(pages, values, language)`
|
|
49
|
+
|
|
50
|
+
Validates ALL pages at once (used before submission). Returns:
|
|
51
|
+
```typescript
|
|
52
|
+
{
|
|
53
|
+
errors: Record<string, string>, // All errors across all pages
|
|
54
|
+
errorPageIndex: number, // First page with errors
|
|
55
|
+
firstErrorId: string, // First question ID with error
|
|
56
|
+
} | null // null = all valid
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
When this returns non-null, the SDK:
|
|
60
|
+
1. Navigates to `errorPageIndex`
|
|
61
|
+
2. Sets all errors into `validationErrors` state
|
|
62
|
+
3. Scrolls to `firstErrorId`
|
|
63
|
+
|
|
64
|
+
## `isQuestionAnswered(question, values)`
|
|
65
|
+
|
|
66
|
+
Exported utility that checks if a single question has been answered. Used by both validation and progress tracking.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Submit Survey — `useSubmitResponse` & `buildSubmitRequestBody`
|
|
2
|
+
|
|
3
|
+
> **Sources**:
|
|
4
|
+
> - `packages/sdk/src/submitSurvey/useSubmitResponse.ts`
|
|
5
|
+
> - `packages/sdk/src/submitSurvey/utils/formatters.ts`
|
|
6
|
+
|
|
7
|
+
## What It Does
|
|
8
|
+
|
|
9
|
+
Formats user answers into the Sprinklr CFM API payload structure and sends them via POST.
|
|
10
|
+
|
|
11
|
+
## Submission Endpoint
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
POST https://custom-p0.feedbook.me/api/feedback/survey/cfm/response
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Payload Format — `SubmitRequestBody`
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
{
|
|
21
|
+
questionToAnswers: Record<string, string[]>, // Standard + matrix row answers
|
|
22
|
+
quesIdVsAttachmentDetails: Record<string, any[]>, // File upload attachments
|
|
23
|
+
optionLevelAnswers: Record<string, string[]>, // (reserved)
|
|
24
|
+
conjointTasks: unknown[], // (reserved — always [])
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Per-Type Serialization
|
|
29
|
+
|
|
30
|
+
### Standard Questions (rating, radio, text, csat, rating_scale, slider)
|
|
31
|
+
```
|
|
32
|
+
questionToAnswers[questionId] = [String(value)]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Matrix Questions (matrix, rating_matrix, slider_matrix)
|
|
36
|
+
Each row becomes a separate entry with a combined key:
|
|
37
|
+
```
|
|
38
|
+
questionToAnswers[`${questionId}_${rowId}`] = [String(rowValue)]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### File Upload
|
|
42
|
+
```
|
|
43
|
+
quesIdVsAttachmentDetails[questionId] = [
|
|
44
|
+
{ id: "...", name: "file.pdf", mediaUrl: "https://..." },
|
|
45
|
+
...
|
|
46
|
+
]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Text and Media (display-only)
|
|
50
|
+
Completely skipped — no data submitted.
|
|
51
|
+
|
|
52
|
+
## Answer Value Types
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
type AnswerValue = string | number | null | MatrixAnswerMap | any[];
|
|
56
|
+
|
|
57
|
+
type MatrixAnswerMap = Record<string, string | number | null | Array<string | number | null>>;
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
| Question Type | Answer Shape | Example |
|
|
61
|
+
|--------------|-------------|---------|
|
|
62
|
+
| `rating` | `number` | `7` |
|
|
63
|
+
| `radio` | `string \| number` | `"option_1"` |
|
|
64
|
+
| `text` | `string` | `"Great service"` |
|
|
65
|
+
| `csat` | `number \| null` | `4` or `null` (N/A) |
|
|
66
|
+
| `rating_scale` | `number` | `3` |
|
|
67
|
+
| `slider` | `number` | `65` |
|
|
68
|
+
| `matrix` | `MatrixAnswerMap` | `{ "row1": "col2", "row2": null }` |
|
|
69
|
+
| `slider_matrix` | `MatrixAnswerMap` | `{ "row1": 75, "row2": 30 }` |
|
|
70
|
+
| `file_upload` | `UploadedFile[]` | `[{ id, name, mediaUrl }]` |
|
|
71
|
+
| `text_and_media` | (none) | Not submitted |
|
|
72
|
+
|
|
73
|
+
## Submit Result
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
type SubmitResult = {
|
|
77
|
+
success: boolean;
|
|
78
|
+
responseId: string; // Generated database ID for the response
|
|
79
|
+
};
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Accessible via `submitSurveyResults.data` in the component.
|
|
83
|
+
|
|
84
|
+
## Error Handling
|
|
85
|
+
|
|
86
|
+
| Scenario | Behaviour |
|
|
87
|
+
|----------|-----------|
|
|
88
|
+
| Network failure | `submitSurveyResults.error` populated |
|
|
89
|
+
| Server rejection | `submitSurveyResults.error` populated with status |
|
|
90
|
+
| During submission | `submitSurveyResults.isLoading = true` — disable submit button |
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Language Handling — Translations & Placeholders
|
|
2
|
+
|
|
3
|
+
> **Source**: `packages/sdk/src/fetchSurvey/utils/extractSurveyLanguages.ts`
|
|
4
|
+
|
|
5
|
+
## How Multi-Language Works
|
|
6
|
+
|
|
7
|
+
1. The raw API response contains `translations` objects on each question and answer detail
|
|
8
|
+
2. `extractSurveyLanguages()` scans all questions to discover available language codes
|
|
9
|
+
3. `mapSurvey()` resolves text fields using the active language's translations
|
|
10
|
+
4. Language switching is **client-side only** — no API re-fetch
|
|
11
|
+
|
|
12
|
+
## Language Discovery
|
|
13
|
+
|
|
14
|
+
The SDK traverses all questions in the survey and collects unique language codes from `question.translations` keys:
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// Pseudocode
|
|
18
|
+
languagesSet.add(baseLanguage); // e.g. 'de'
|
|
19
|
+
for each question:
|
|
20
|
+
for each key in question.translations:
|
|
21
|
+
languagesSet.add(key); // e.g. 'en', 'fr'
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Display Names
|
|
25
|
+
|
|
26
|
+
Built-in language code → display name mapping:
|
|
27
|
+
|
|
28
|
+
| Code | Display Name |
|
|
29
|
+
|------|-------------|
|
|
30
|
+
| `en` | English |
|
|
31
|
+
| `de` | Deutsch |
|
|
32
|
+
| `hi` | Hindi |
|
|
33
|
+
| `es` | Español |
|
|
34
|
+
| `fr` | Français |
|
|
35
|
+
| `it` | Italiano |
|
|
36
|
+
| `ja` | 日本語 |
|
|
37
|
+
| `ko` | 한국어 |
|
|
38
|
+
| `zh` | 中文 |
|
|
39
|
+
| `bn` | Bengali |
|
|
40
|
+
| `gu` | Gujarati |
|
|
41
|
+
| `mr` | Marathi |
|
|
42
|
+
| `pa` | Punjabi |
|
|
43
|
+
| `ta` | Tamil |
|
|
44
|
+
| `te` | Telugu |
|
|
45
|
+
| `ur` | Urdu |
|
|
46
|
+
|
|
47
|
+
Unknown codes fall back to `code.toUpperCase()`.
|
|
48
|
+
|
|
49
|
+
## Translation Resolution Priority
|
|
50
|
+
|
|
51
|
+
For question text:
|
|
52
|
+
```
|
|
53
|
+
translations[language].questionRichText
|
|
54
|
+
→ translations[language].questionText
|
|
55
|
+
→ raw.questionRichText
|
|
56
|
+
→ raw.questionText
|
|
57
|
+
→ ''
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
For answer option labels:
|
|
61
|
+
```
|
|
62
|
+
translations[language].answerText → raw.answerText → ''
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
For validation error messages:
|
|
66
|
+
```
|
|
67
|
+
config.translations[language].value[0] → config.value[0] → default message
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Placeholders
|
|
71
|
+
|
|
72
|
+
Tokens like `{{FIRST_NAME}}` in question text are replaced at map time:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
const SURVEY_PLACEHOLDERS = {
|
|
76
|
+
FIRST_NAME: 'Customer',
|
|
77
|
+
LAST_NAME: 'Smith',
|
|
78
|
+
EMAIL_ADDRESS: 'customer@example.com',
|
|
79
|
+
};
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**⚠️ CRITICAL**: Define `SURVEY_PLACEHOLDERS` as a **constant outside the component** to prevent infinite re-render loops:
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
// ✅ CORRECT — stable reference
|
|
86
|
+
const SURVEY_PLACEHOLDERS = { FIRST_NAME: 'John' };
|
|
87
|
+
export default function SurveyPage() { ... }
|
|
88
|
+
|
|
89
|
+
// ❌ WRONG — new object every render → infinite re-fetch
|
|
90
|
+
export default function SurveyPage() {
|
|
91
|
+
useSurveySDK({ options: { placeholders: { FIRST_NAME: 'John' } } });
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Language Switching in UI
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
const [selectedLanguage, setSelectedLanguage] = useState<string | undefined>("");
|
|
99
|
+
|
|
100
|
+
// Pass to SDK
|
|
101
|
+
const { ... } = useSurveySDK({ options: { language: selectedLanguage } });
|
|
102
|
+
|
|
103
|
+
// Render selector
|
|
104
|
+
<LanguageSelector
|
|
105
|
+
languages={survey.languages}
|
|
106
|
+
selectedLanguage={survey.language}
|
|
107
|
+
onChange={setSelectedLanguage}
|
|
108
|
+
/>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
When `setSelectedLanguage` fires, the SDK re-maps the cached survey data to the new language — no network request.
|