@cxpinsight/react-native 0.4.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +90 -0
- package/lib/CXPinsight.d.ts +66 -3
- package/lib/CXPinsight.d.ts.map +1 -1
- package/lib/CXPinsight.js +130 -3
- package/lib/CXPinsight.js.map +1 -1
- package/lib/CXPinsightProvider.d.ts.map +1 -1
- package/lib/CXPinsightProvider.js +48 -10
- package/lib/CXPinsightProvider.js.map +1 -1
- package/lib/consent/ConsentBanner.d.ts +18 -0
- package/lib/consent/ConsentBanner.d.ts.map +1 -0
- package/lib/consent/ConsentBanner.js +90 -0
- package/lib/consent/ConsentBanner.js.map +1 -0
- package/lib/core/attribution.d.ts +54 -0
- package/lib/core/attribution.d.ts.map +1 -0
- package/lib/core/attribution.js +112 -0
- package/lib/core/attribution.js.map +1 -0
- package/lib/core/consent.d.ts +16 -1
- package/lib/core/consent.d.ts.map +1 -1
- package/lib/core/consent.js +79 -32
- package/lib/core/consent.js.map +1 -1
- package/lib/core/experiments.d.ts +59 -0
- package/lib/core/experiments.d.ts.map +1 -0
- package/lib/core/experiments.js +201 -0
- package/lib/core/experiments.js.map +1 -0
- package/lib/core/triggerEngine.d.ts +5 -3
- package/lib/core/triggerEngine.d.ts.map +1 -1
- package/lib/core/triggerEngine.js +16 -2
- package/lib/core/triggerEngine.js.map +1 -1
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +5 -1
- package/lib/index.js.map +1 -1
- package/lib/survey/definitionCache.d.ts +15 -0
- package/lib/survey/definitionCache.d.ts.map +1 -0
- package/lib/survey/definitionCache.js +80 -0
- package/lib/survey/definitionCache.js.map +1 -0
- package/lib/survey/pagesFromV1.d.ts.map +1 -1
- package/lib/survey/pagesFromV1.js +30 -76
- package/lib/survey/pagesFromV1.js.map +1 -1
- package/lib/survey/validate.d.ts.map +1 -1
- package/lib/survey/validate.js +13 -163
- package/lib/survey/validate.js.map +1 -1
- package/lib/survey/voiceParsers.d.ts +12 -42
- package/lib/survey/voiceParsers.d.ts.map +1 -1
- package/lib/survey/voiceParsers.js +60 -284
- package/lib/survey/voiceParsers.js.map +1 -1
- package/lib/types.d.ts +18 -1
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"definitionCache.d.ts","sourceRoot":"","sources":["../../src/survey/definitionCache.ts"],"names":[],"mappings":"AAeA;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,QAA0B,CAAC;AAOlD,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,2DAA2D;AAC3D,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,MAAmB,GACvB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAKzC;AAED,kFAAkF;AAClF,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC,CAG5D"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.MAX_AGE_MS = void 0;
|
|
37
|
+
exports.cacheDefinition = cacheDefinition;
|
|
38
|
+
exports.getCachedDefinition = getCachedDefinition;
|
|
39
|
+
exports.clearCachedDefinitions = clearCachedDefinitions;
|
|
40
|
+
/**
|
|
41
|
+
* Cached survey definitions, so a trigger that fires offline can still show.
|
|
42
|
+
*
|
|
43
|
+
* A native app is offline routinely — a lift, a plane, a bad cell — and the
|
|
44
|
+
* survey definition arrives over the network at the moment the trigger fires.
|
|
45
|
+
* Without a cache, an invitation that comes due offline simply does not appear.
|
|
46
|
+
*
|
|
47
|
+
* Used ONLY when the fetch fails. A successful fetch always wins, so a survey
|
|
48
|
+
* edited in the dashboard reaches respondents on their next online launch
|
|
49
|
+
* rather than being pinned to whatever was cached.
|
|
50
|
+
*/
|
|
51
|
+
const storage = __importStar(require("../core/storage"));
|
|
52
|
+
const PREFIX = '@cxpinsight:surveydef:';
|
|
53
|
+
/**
|
|
54
|
+
* How stale a cached definition may be before it is refused.
|
|
55
|
+
*
|
|
56
|
+
* Not unlimited. A definition cached months ago can be missing questions that
|
|
57
|
+
* were added since, and answers collected against it are recorded against a
|
|
58
|
+
* survey that no longer looks like that — which is harder to notice, and worse,
|
|
59
|
+
* than a survey that failed to appear.
|
|
60
|
+
*/
|
|
61
|
+
exports.MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
62
|
+
async function cacheDefinition(surveyId, definition) {
|
|
63
|
+
await storage.set(PREFIX + surveyId, { cachedAt: Date.now(), definition });
|
|
64
|
+
}
|
|
65
|
+
/** A cached definition, or null when absent or too old. */
|
|
66
|
+
async function getCachedDefinition(surveyId, now = Date.now()) {
|
|
67
|
+
const entry = await storage.get(PREFIX + surveyId);
|
|
68
|
+
if (!entry?.definition)
|
|
69
|
+
return null;
|
|
70
|
+
if (now - entry.cachedAt > exports.MAX_AGE_MS)
|
|
71
|
+
return null;
|
|
72
|
+
return entry.definition;
|
|
73
|
+
}
|
|
74
|
+
/** Used by reset(): a cached definition outlives the identity that fetched it. */
|
|
75
|
+
async function clearCachedDefinitions() {
|
|
76
|
+
const keys = await storage.getKeysWithPrefix(PREFIX);
|
|
77
|
+
if (keys.length)
|
|
78
|
+
await storage.multiRemove(keys);
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=definitionCache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"definitionCache.js","sourceRoot":"","sources":["../../src/survey/definitionCache.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;GAUG;AACH,MAAY,OAAO,4CAAwB;AAE3C,MAAM,MAAM,GAAG,wBAAwB,CAAC;AAExC;;;;;;;GAOG;AACU,QAAA,UAAU,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAO3C,KAAK,0BACV,QAAgB,EAChB,UAAmC;IAEnC,MAAM,OAAO,CAAC,GAAG,CAAC,MAAM,GAAG,QAAQ,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,UAAU,EAAW,CAAC,CAAC;AACtF,CAAC;AAED,2DAA2D;AACpD,KAAK,8BACV,QAAgB,EAChB,GAAG,GAAW,IAAI,CAAC,GAAG,EAAE;IAExB,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAAQ,MAAM,GAAG,QAAQ,CAAC,CAAC;IAC1D,IAAI,CAAC,KAAK,EAAE,UAAU;QAAE,OAAO,IAAI,CAAC;IACpC,IAAI,GAAG,GAAG,KAAK,CAAC,QAAQ,GAAG,QAAA,UAAU;QAAE,OAAO,IAAI,CAAC;IACnD,OAAO,KAAK,CAAC,UAAU,CAAC;AAC1B,CAAC;AAED,kFAAkF;AAC3E,KAAK;IACV,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACrD,IAAI,IAAI,CAAC,MAAM;QAAE,MAAM,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;AACnD,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pagesFromV1.d.ts","sourceRoot":"","sources":["../../src/survey/pagesFromV1.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pagesFromV1.d.ts","sourceRoot":"","sources":["../../src/survey/pagesFromV1.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEhF,wBAAgB,WAAW,CAAC,EAAE,EAAE;IAC9B,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC;IAC7B,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;CAC5B,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAUzF"}
|
|
@@ -1,83 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// v1.1 → in-modal pagination.
|
|
3
|
-
// using sections[].questionIds[] as the overlay, then falls back to
|
|
4
|
-
// type `pageBreak` markers, then to a single page. Renderer-level
|
|
5
|
-
// rules (spec §22 §26): a section with pageBreak:true begins a new
|
|
6
|
-
// page; pageBreak type questions are omitted from the rendered list.
|
|
2
|
+
// v1.1 → in-modal pagination.
|
|
7
3
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
4
|
+
// The rules now live in @cxpinsight/survey-spec. They used to live here AND in
|
|
5
|
+
// the React renderer, and the two disagreed on 6 of 17 fixtures — including
|
|
6
|
+
// PAGE ORDER, so a survey with a loose question before a section was asked in a
|
|
7
|
+
// different order on each channel while the answers were recorded as though it
|
|
8
|
+
// were the same instrument.
|
|
9
|
+
//
|
|
10
|
+
// Two behaviours change on mobile as a result, both toward the shared rule:
|
|
11
|
+
//
|
|
12
|
+
// • Page order follows QUESTION order. This file walked sections[] and
|
|
13
|
+
// appended unclaimed questions at the end, so section order in the
|
|
14
|
+
// definition — which the builder does not treat as meaningful — could
|
|
15
|
+
// reorder the survey.
|
|
16
|
+
//
|
|
17
|
+
// • Section showWhen / hideWhen are evaluated. This file checked only
|
|
18
|
+
// `hidden`, so a section conditioned away still split pages here.
|
|
19
|
+
//
|
|
20
|
+
// Typeform/conversation is unchanged here: one page per question, which is what
|
|
21
|
+
// this file always did. It was briefly a parameter while that was an open
|
|
22
|
+
// question between the channels; it is now the single behaviour, and React
|
|
23
|
+
// moved onto it.
|
|
24
|
+
//
|
|
25
|
+
// This file is now a shape adapter: the spec returns `{ items, questions }`,
|
|
26
|
+
// the modal wants `{ elements }`.
|
|
16
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
28
|
exports.pagesFromV1 = pagesFromV1;
|
|
29
|
+
const survey_spec_1 = require("@cxpinsight/survey-spec");
|
|
18
30
|
function pagesFromV1(v1, flow, presentationMode) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// section that mixes flows only shows the current flow's questions.
|
|
25
|
-
const questions = flow === null
|
|
26
|
-
? rawQuestions
|
|
27
|
-
: rawQuestions.filter((q) => (q.flow || undefined) === (flow || undefined));
|
|
28
|
-
if (questions.length === 0)
|
|
29
|
-
return [];
|
|
30
|
-
// Typeform / conversation presentation modes: one question per page.
|
|
31
|
-
// The web widget renders this way by default (its question-at-a-time
|
|
32
|
-
// navigation loops over the flat list) but mobile paginates via the
|
|
33
|
-
// sections/pageBreak structure below — without this short-circuit the
|
|
34
|
-
// whole survey collapses into a single scrollable page. Author's
|
|
35
|
-
// section grouping is ignored in these modes because the builder itself
|
|
36
|
-
// treats each question as its own page (add-page button is hidden and
|
|
37
|
-
// section headers become intro pages).
|
|
38
|
-
const isTypeform = presentationMode === 'typeform' || presentationMode === 'conversation';
|
|
39
|
-
if (isTypeform) {
|
|
40
|
-
return questions
|
|
41
|
-
.filter((q) => q.type !== 'pageBreak')
|
|
42
|
-
.map((q) => ({ elements: [q] }));
|
|
43
|
-
}
|
|
44
|
-
const sections = Array.isArray(v1.sections) ? v1.sections.filter((s) => !s.hidden) : [];
|
|
45
|
-
if (sections.length > 0) {
|
|
46
|
-
const byId = new Map(questions.map((q) => [q.id, q]));
|
|
47
|
-
const claimed = new Set();
|
|
48
|
-
const pages = [];
|
|
49
|
-
for (const s of sections) {
|
|
50
|
-
const ids = Array.isArray(s.questionIds) ? s.questionIds : [];
|
|
51
|
-
const els = [];
|
|
52
|
-
for (const id of ids) {
|
|
53
|
-
const q = byId.get(id);
|
|
54
|
-
if (!q || q.type === 'pageBreak')
|
|
55
|
-
continue;
|
|
56
|
-
claimed.add(id);
|
|
57
|
-
els.push(q);
|
|
58
|
-
}
|
|
59
|
-
if (els.length > 0)
|
|
60
|
-
pages.push({ elements: els });
|
|
61
|
-
}
|
|
62
|
-
const leftover = questions.filter((q) => !claimed.has(q.id) && q.type !== 'pageBreak');
|
|
63
|
-
if (leftover.length > 0)
|
|
64
|
-
pages.push({ elements: leftover });
|
|
65
|
-
return pages;
|
|
66
|
-
}
|
|
67
|
-
const pages = [];
|
|
68
|
-
let current = [];
|
|
69
|
-
for (const q of questions) {
|
|
70
|
-
if (q.type === 'pageBreak') {
|
|
71
|
-
if (current.length > 0) {
|
|
72
|
-
pages.push({ elements: current });
|
|
73
|
-
current = [];
|
|
74
|
-
}
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
current.push(q);
|
|
78
|
-
}
|
|
79
|
-
if (current.length > 0)
|
|
80
|
-
pages.push({ elements: current });
|
|
81
|
-
return pages;
|
|
31
|
+
// No evaluation context here: the modal computes pages before any answer
|
|
32
|
+
// exists, so a section condition referencing an answer resolves false — the
|
|
33
|
+
// same as it did on React, which is what parity requires.
|
|
34
|
+
const pages = (0, survey_spec_1.splitIntoPages)(v1, { answers: {}, variables: {} }, { flow, presentationMode });
|
|
35
|
+
return pages.map((p) => ({ elements: p.questions }));
|
|
82
36
|
}
|
|
83
37
|
//# sourceMappingURL=pagesFromV1.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pagesFromV1.js","sourceRoot":"","sources":["../../src/survey/pagesFromV1.ts"],"names":[],"mappings":";AAAA,
|
|
1
|
+
{"version":3,"file":"pagesFromV1.js","sourceRoot":"","sources":["../../src/survey/pagesFromV1.ts"],"names":[],"mappings":";AAAA,8BAA8B;AAC9B,EAAE;AACF,+EAA+E;AAC/E,4EAA4E;AAC5E,gFAAgF;AAChF,+EAA+E;AAC/E,4BAA4B;AAC5B,EAAE;AACF,4EAA4E;AAC5E,EAAE;AACF,yEAAyE;AACzE,uEAAuE;AACvE,0EAA0E;AAC1E,0BAA0B;AAC1B,EAAE;AACF,wEAAwE;AACxE,sEAAsE;AACtE,EAAE;AACF,gFAAgF;AAChF,0EAA0E;AAC1E,2EAA2E;AAC3E,iBAAiB;AACjB,EAAE;AACF,6EAA6E;AAC7E,kCAAkC;;;AAElC,yDAAyD;AAGzD,qBAA4B,EAG3B,EAAE,IAAgC,EAAE,gBAAyB;IAC5D,yEAAyE;IACzE,4EAA4E;IAC5E,0DAA0D;IAC1D,MAAM,KAAK,GAAG,IAAA,4BAAc,EAC1B,EAAE,EACF,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,EAC9B,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAC3B,CAAC;IACF,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;AACvD,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/survey/validate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/survey/validate.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C,wBAAgB,cAAc,CAAC,QAAQ,EAAE,cAAc,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAEtF"}
|
package/lib/survey/validate.js
CHANGED
|
@@ -1,169 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// Answer validation — spec §8 + type-specific rules. Kept in lockstep
|
|
3
|
-
// with apps/frontend/src/components/survey/renderer/lib/validate.ts,
|
|
4
|
-
// with two mobile simplifications:
|
|
5
|
-
//
|
|
6
|
-
// - Mobile question components emit primitive values, not the
|
|
7
|
-
// compound {text,attachments} / {choice,otherText} shapes that
|
|
8
|
-
// the web renderer uses for allowAttachments + "Other". Whenever
|
|
9
|
-
// the value shape simplifies, the mobile helper's per-type path
|
|
10
|
-
// shrinks too.
|
|
11
|
-
// - inputType regexes stay identical so a text field flagged as
|
|
12
|
-
// invalid on web reads as invalid on mobile and vice versa.
|
|
13
|
-
//
|
|
14
|
-
// Order per spec §8: required → type-specific format → rules[] array.
|
|
15
|
-
// Empty non-required answers are valid — a blank optional email
|
|
16
|
-
// shouldn't complain about missing '@'.
|
|
17
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
3
|
exports.validateAnswer = validateAnswer;
|
|
19
|
-
|
|
20
|
-
|
|
4
|
+
// Answer validation now lives in @cxpinsight/survey-spec.
|
|
5
|
+
//
|
|
6
|
+
// This file was the superset in type routing (email / number / csat / ces) and
|
|
7
|
+
// the subset in emptiness: its isEmpty only understood primitives, so a
|
|
8
|
+
// compound value like `{ choice: undefined }` counted as answered. The shared
|
|
9
|
+
// version takes the dispatcher from here and the type-aware isEmpty from the
|
|
10
|
+
// React renderer.
|
|
11
|
+
//
|
|
12
|
+
// One behaviour changes on mobile: the required message is now "This field is
|
|
13
|
+
// required" rather than "This question requires an answer", so the wording is
|
|
14
|
+
// the same wherever a respondent meets it.
|
|
15
|
+
const survey_spec_1 = require("@cxpinsight/survey-spec");
|
|
21
16
|
function validateAnswer(question, value) {
|
|
22
|
-
|
|
23
|
-
if ((question.required || question.isRequired) && empty) {
|
|
24
|
-
return 'This question requires an answer';
|
|
25
|
-
}
|
|
26
|
-
if (empty)
|
|
27
|
-
return null; // optional + blank → OK
|
|
28
|
-
const typeErr = validateByType(question, value);
|
|
29
|
-
if (typeErr)
|
|
30
|
-
return typeErr;
|
|
31
|
-
return validateRules(question, value);
|
|
32
|
-
}
|
|
33
|
-
function isEmpty(v) {
|
|
34
|
-
if (v === undefined || v === null)
|
|
35
|
-
return true;
|
|
36
|
-
if (typeof v === 'string')
|
|
37
|
-
return v.trim().length === 0;
|
|
38
|
-
if (Array.isArray(v))
|
|
39
|
-
return v.length === 0;
|
|
40
|
-
if (typeof v === 'number')
|
|
41
|
-
return Number.isNaN(v);
|
|
42
|
-
return false; // 0, false, non-empty obj → not empty
|
|
43
|
-
}
|
|
44
|
-
function validateByType(question, value) {
|
|
45
|
-
switch (question.type) {
|
|
46
|
-
case 'text':
|
|
47
|
-
case 'comment':
|
|
48
|
-
case 'email':
|
|
49
|
-
case 'number':
|
|
50
|
-
return validateTextish(question, value);
|
|
51
|
-
case 'checkbox':
|
|
52
|
-
return validateCheckbox(question, value);
|
|
53
|
-
case 'rating':
|
|
54
|
-
case 'csat':
|
|
55
|
-
case 'ces':
|
|
56
|
-
case 'nps':
|
|
57
|
-
return validateNumericRange(question, value);
|
|
58
|
-
default:
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
function validateTextish(question, value) {
|
|
63
|
-
const trimmed = String(value ?? '').trim();
|
|
64
|
-
const inputType = question.inputType || (question.type === 'email' ? 'email' : question.type === 'number' ? 'number' : undefined);
|
|
65
|
-
if (inputType === 'email' && !EMAIL_RE.test(trimmed)) {
|
|
66
|
-
return 'Please enter a valid email address (e.g., you@example.com)';
|
|
67
|
-
}
|
|
68
|
-
if (inputType === 'url' && !URL_RE.test(trimmed)) {
|
|
69
|
-
return 'Please enter a full URL, including https://';
|
|
70
|
-
}
|
|
71
|
-
if (inputType === 'number' && Number.isNaN(Number(trimmed))) {
|
|
72
|
-
return 'Please enter a number';
|
|
73
|
-
}
|
|
74
|
-
if (inputType === 'tel' && !/^[\d\s+\-()]{3,}$/.test(trimmed)) {
|
|
75
|
-
return 'Please enter a valid phone number';
|
|
76
|
-
}
|
|
77
|
-
const maxLength = typeof question.maxLength === 'number' ? question.maxLength : undefined;
|
|
78
|
-
if (maxLength !== undefined && trimmed.length > maxLength) {
|
|
79
|
-
return `Please keep this under ${maxLength} characters`;
|
|
80
|
-
}
|
|
81
|
-
// minLength was only honoured through the rules[] array, never as a direct
|
|
82
|
-
// question property — so a question with minLength set in the builder was
|
|
83
|
-
// enforced on links and silently ignored here. Same survey, different rules,
|
|
84
|
-
// depending on how the respondent arrived.
|
|
85
|
-
const minLength = typeof question.minLength === 'number'
|
|
86
|
-
? question.minLength
|
|
87
|
-
: undefined;
|
|
88
|
-
if (minLength !== undefined && trimmed.length < minLength) {
|
|
89
|
-
return `Please enter at least ${minLength} characters`;
|
|
90
|
-
}
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
function validateCheckbox(question, value) {
|
|
94
|
-
const arr = Array.isArray(value) ? value : [];
|
|
95
|
-
const count = arr.length;
|
|
96
|
-
const minCount = typeof question.minCount === 'number' ? question.minCount : undefined;
|
|
97
|
-
const maxCount = typeof question.maxCount === 'number' ? question.maxCount : undefined;
|
|
98
|
-
if (minCount !== undefined && count < minCount)
|
|
99
|
-
return `Please select at least ${minCount}`;
|
|
100
|
-
if (maxCount !== undefined && count > maxCount)
|
|
101
|
-
return `Please select at most ${maxCount}`;
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
function validateNumericRange(question, value) {
|
|
105
|
-
if (typeof value !== 'number')
|
|
106
|
-
return null;
|
|
107
|
-
const min = typeof question.minValue === 'number' ? question.minValue : undefined;
|
|
108
|
-
const max = typeof question.maxValue === 'number' ? question.maxValue : undefined;
|
|
109
|
-
if (min !== undefined && value < min)
|
|
110
|
-
return `Must be at least ${min}`;
|
|
111
|
-
if (max !== undefined && value > max)
|
|
112
|
-
return `Must be at most ${max}`;
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
function validateRules(question, value) {
|
|
116
|
-
const rules = Array.isArray(question.rules)
|
|
117
|
-
? question.rules
|
|
118
|
-
: [];
|
|
119
|
-
if (rules.length === 0)
|
|
120
|
-
return null;
|
|
121
|
-
const str = typeof value === 'string' ? value : String(value ?? '');
|
|
122
|
-
for (const r of rules) {
|
|
123
|
-
const err = applyRule(r, str);
|
|
124
|
-
if (err)
|
|
125
|
-
return r.message || err;
|
|
126
|
-
}
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
function applyRule(rule, str) {
|
|
130
|
-
switch (rule.type) {
|
|
131
|
-
case 'minLength':
|
|
132
|
-
if (typeof rule.value === 'number' && str.length < rule.value)
|
|
133
|
-
return `At least ${rule.value} characters`;
|
|
134
|
-
return null;
|
|
135
|
-
case 'maxLength':
|
|
136
|
-
if (typeof rule.value === 'number' && str.length > rule.value)
|
|
137
|
-
return `At most ${rule.value} characters`;
|
|
138
|
-
return null;
|
|
139
|
-
case 'min':
|
|
140
|
-
if (typeof rule.value === 'number' && Number(str) < rule.value)
|
|
141
|
-
return `Must be at least ${rule.value}`;
|
|
142
|
-
return null;
|
|
143
|
-
case 'max':
|
|
144
|
-
if (typeof rule.value === 'number' && Number(str) > rule.value)
|
|
145
|
-
return `Must be at most ${rule.value}`;
|
|
146
|
-
return null;
|
|
147
|
-
case 'email':
|
|
148
|
-
if (!EMAIL_RE.test(str))
|
|
149
|
-
return 'Please enter a valid email address';
|
|
150
|
-
return null;
|
|
151
|
-
case 'url':
|
|
152
|
-
if (!URL_RE.test(str))
|
|
153
|
-
return 'Please enter a valid URL';
|
|
154
|
-
return null;
|
|
155
|
-
case 'pattern': {
|
|
156
|
-
if (typeof rule.value !== 'string')
|
|
157
|
-
return null;
|
|
158
|
-
try {
|
|
159
|
-
const re = new RegExp(rule.value);
|
|
160
|
-
return re.test(str) ? null : 'Format doesn\'t match';
|
|
161
|
-
}
|
|
162
|
-
catch {
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
default: return null;
|
|
167
|
-
}
|
|
17
|
+
return (0, survey_spec_1.validateAnswer)(question, value);
|
|
168
18
|
}
|
|
169
19
|
//# sourceMappingURL=validate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/survey/validate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/survey/validate.ts"],"names":[],"mappings":";;;AAAA,0DAA0D;AAC1D,EAAE;AACF,+EAA+E;AAC/E,wEAAwE;AACxE,8EAA8E;AAC9E,6EAA6E;AAC7E,kBAAkB;AAClB,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,2CAA2C;AAC3C,yDAA+E;AAG/E,wBAA+B,QAAwB,EAAE,KAAc;IACrE,OAAO,IAAA,4BAAkB,EAAC,QAAoD,EAAE,KAAK,CAAC,CAAC;AACzF,CAAC"}
|
|
@@ -1,47 +1,17 @@
|
|
|
1
|
+
import { type VoiceQuestion } from '@cxpinsight/survey-spec';
|
|
2
|
+
export { auditVoiceQuestions, voiceDisclaimer, skipNoteText, scaffoldFor, buildPromptText, } from '@cxpinsight/survey-spec';
|
|
3
|
+
/**
|
|
4
|
+
* Mobile's four levels. The spec distinguishes 'assist' (readable, answer it
|
|
5
|
+
* on screen) from 'none' (voice cannot produce this at all); this renderer
|
|
6
|
+
* treats both the same way — read it, hand off — so they collapse to
|
|
7
|
+
* 'partial', which is exactly what it returned before.
|
|
8
|
+
*/
|
|
1
9
|
export type VoiceSupport = 'full' | 'matrix' | 'read' | 'partial';
|
|
2
|
-
interface VQuestion {
|
|
3
|
-
type?: string;
|
|
4
|
-
title?: string;
|
|
5
|
-
text?: string;
|
|
6
|
-
label?: string;
|
|
7
|
-
options?: unknown;
|
|
8
|
-
choices?: unknown;
|
|
9
|
-
yesLabel?: string;
|
|
10
|
-
noLabel?: string;
|
|
11
|
-
minValue?: number;
|
|
12
|
-
maxValue?: number;
|
|
13
|
-
minLabel?: string;
|
|
14
|
-
maxLabel?: string;
|
|
15
|
-
rows?: unknown;
|
|
16
|
-
columns?: unknown;
|
|
17
|
-
}
|
|
18
|
-
export declare function parseAnswer(q: VQuestion, transcript: string): {
|
|
19
|
-
value: unknown;
|
|
20
|
-
parsed: boolean;
|
|
21
|
-
};
|
|
22
10
|
export declare function voiceSupport(type: string): VoiceSupport;
|
|
23
11
|
export declare function voiceCanHandle(type: string): boolean;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
export declare function auditVoiceQuestions(questions: AuditQ[]): {
|
|
30
|
-
blocking: AuditQ[];
|
|
31
|
-
skippable: AuditQ[];
|
|
32
|
-
};
|
|
33
|
-
export declare function voiceDisclaimer(n: number, lang?: string): string;
|
|
34
|
-
export declare function skipNoteText(lang?: string): string;
|
|
35
|
-
export declare function scaffoldFor(lang: string): {
|
|
36
|
-
scale: (min: number, max: number, minL?: string, maxL?: string) => string;
|
|
37
|
-
binary: (y: string, n: string) => string;
|
|
38
|
-
options: (o: string[]) => string;
|
|
39
|
-
multi: (o: string[]) => string;
|
|
40
|
-
ranking: (o: string[]) => string;
|
|
41
|
-
date: () => string;
|
|
42
|
-
matrixStem: (stem: string) => string;
|
|
43
|
-
matrixRow: (row: string, cols: string[]) => string;
|
|
12
|
+
/** Match a spoken answer, then wrap it in the shape this renderer stores. */
|
|
13
|
+
export declare function parseAnswer(q: VoiceQuestion, transcript: string): {
|
|
14
|
+
value: unknown;
|
|
15
|
+
parsed: boolean;
|
|
44
16
|
};
|
|
45
|
-
export declare function buildPromptText(q: VQuestion, lang: string): string;
|
|
46
|
-
export {};
|
|
47
17
|
//# sourceMappingURL=voiceParsers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voiceParsers.d.ts","sourceRoot":"","sources":["../../src/survey/voiceParsers.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"voiceParsers.d.ts","sourceRoot":"","sources":["../../src/survey/voiceParsers.ts"],"names":[],"mappings":"AAkBA,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,mBAAmB,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,eAAe,GACjF,MAAM,yBAAyB,CAAC;AAEjC;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;AAElE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAGvD;AAGD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAA6C;AAIlG,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,CAAC,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CA6CrG"}
|