mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-22 11:29:22 -05:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f291de1835 | |||
| 5c4fd7cb0a |
@@ -25,8 +25,16 @@ https://app.formbricks.com/s/clin3dxja02k8l80hpwmx4bjy?question_id_1=answer_1&qu
|
|||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
1. To prefill survey questions, add query parameters to the survey URL using the format `questionId=answer`.
|
1. To prefill survey questions, add query parameters to the survey URL using the format `questionId=answer`.
|
||||||
2. The answer must match the question’s expected type to pass [validation](/xm-and-surveys/surveys/link-surveys/data-prefilling#validation).
|
2. For choice-based questions, you can use either **option IDs** (recommended) or **option labels** (backward compatible).
|
||||||
3. The answer needs to be [URL encoded](https://www.urlencoder.org/) (if it contains spaces or special characters)
|
3. The answer must match the question's expected type to pass [validation](/xm-and-surveys/surveys/link-surveys/data-prefilling#validation).
|
||||||
|
4. The answer needs to be [URL encoded](https://www.urlencoder.org/) (if it contains spaces or special characters)
|
||||||
|
|
||||||
|
### Option IDs vs Option Labels
|
||||||
|
|
||||||
|
- **Option IDs (Recommended):** Use the unique ID of each option. These don't change with translations and are more reliable. Example: `?country=choice-us`
|
||||||
|
- **Option Labels (Backward Compatible):** Use the exact text of the option. Example: `?country=United%20States`
|
||||||
|
|
||||||
|
If you provide a value that matches an option ID, it will be used. Otherwise, the system will try to match it as an option label.
|
||||||
|
|
||||||
|
|
||||||
### Skip prefilled questions
|
### Skip prefilled questions
|
||||||
@@ -77,9 +85,15 @@ https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?single_select_question_id
|
|||||||
|
|
||||||
### Multi Select Question (Checkbox)
|
### Multi Select Question (Checkbox)
|
||||||
|
|
||||||
Selects three options 'Sun, Palms and Beach' in the multi select question. The strings have to be identical to the options in your question:
|
Selects multiple options in the multi select question using comma-separated values. You can use either option IDs or labels:
|
||||||
|
|
||||||
```sh Multi-select Question
|
**Using Option IDs (Recommended):**
|
||||||
|
```sh Multi-select Question (by ID)
|
||||||
|
https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?multi_select_question_id=sport%2Cmusic%2Ctravel
|
||||||
|
```
|
||||||
|
|
||||||
|
**Using Option Labels (Backward Compatible):**
|
||||||
|
```sh Multi-select Question (by Label)
|
||||||
https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?multi_select_question_id=Sun%2CPalms%2CBeach
|
https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?multi_select_question_id=Sun%2CPalms%2CBeach
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -113,12 +127,22 @@ https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?consent_question_id=accep
|
|||||||
|
|
||||||
### Picture Selection Question
|
### Picture Selection Question
|
||||||
|
|
||||||
Adds index of the selected image(s) as the answer to the Picture Selection question. The index starts from 1
|
Selects image options in the Picture Selection question using comma-separated option IDs:
|
||||||
|
|
||||||
```txt Picture Selection Question.
|
```txt Picture Selection Question
|
||||||
https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?pictureSelection_question_id=1%2C2%2C3
|
https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?pictureSelection_question_id=choice-1%2Cchoice-2%2Cchoice-3
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Ranking Question
|
||||||
|
|
||||||
|
Orders items in a Ranking question using comma-separated option IDs. The order matters and determines the rank:
|
||||||
|
|
||||||
|
```txt Ranking Question (order matters)
|
||||||
|
https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?ranking_question_id=item-3%2Citem-1%2Citem-2
|
||||||
|
```
|
||||||
|
|
||||||
|
<Note>The order of comma-separated values determines the ranking order.</Note>
|
||||||
|
|
||||||
<Note>All other question types, you currently cannot prefill via the URL.</Note>
|
<Note>All other question types, you currently cannot prefill via the URL.</Note>
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
@@ -130,10 +154,17 @@ The URL validation works as follows:
|
|||||||
- For Rating or NPS questions, the response is parsed as a number and verified if it's accepted by the schema.
|
- For Rating or NPS questions, the response is parsed as a number and verified if it's accepted by the schema.
|
||||||
- For CTA type questions, the valid values are "clicked" (main CTA) and "dismissed" (skip CTA).
|
- For CTA type questions, the valid values are "clicked" (main CTA) and "dismissed" (skip CTA).
|
||||||
- For Consent type questions, the valid values are "accepted" (consent given) and "dismissed" (consent not given).
|
- For Consent type questions, the valid values are "accepted" (consent given) and "dismissed" (consent not given).
|
||||||
- For Picture Selection type questions, the response is parsed as an array of numbers and verified if it's accepted by the schema.
|
- For Picture Selection, Multi-select, and Ranking questions, the response can be comma-separated option IDs or labels.
|
||||||
|
- For choice-based questions (Single-select, Multi-select, Ranking, Picture Selection), the system will first try to match as an option ID, then fallback to matching as an option label.
|
||||||
- All other question types are strings.
|
- All other question types are strings.
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
If an answer is invalid, the prefilling will be ignored and the question is
|
If an answer is invalid, the prefilling will be ignored and the question is
|
||||||
presented as if not prefilled.
|
presented as if not prefilled.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
|
## Finding Option IDs
|
||||||
|
|
||||||
|
Option IDs are unique identifiers for each choice option in your survey. You can find them in the **Advanced Settings** at the bottom of each question card in the Survey Editor. For choice-based questions (Single-select, Multi-select, Ranking, Picture Selection), the option ID is displayed next to each option.
|
||||||
|
|
||||||
|
You can update option IDs to any string you like **before you publish a survey.** After you publish a survey, you cannot change the IDs anymore.
|
||||||
|
|||||||
@@ -43,8 +43,6 @@ export function AddressElement({
|
|||||||
return Array.isArray(value) ? value : ["", "", "", "", "", ""];
|
return Array.isArray(value) ? value : ["", "", "", "", "", ""];
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
const isCurrent = element.id === currentElementId;
|
|
||||||
|
|
||||||
const fields = useMemo(
|
const fields = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@@ -166,7 +164,7 @@ export function AddressElement({
|
|||||||
handleChange(field.id, e.currentTarget.value);
|
handleChange(field.id, e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
ref={index === 0 ? addressRef : null}
|
ref={index === 0 ? addressRef : null}
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={0}
|
||||||
aria-label={field.label}
|
aria-label={field.label}
|
||||||
dir={!safeValue[index] ? dir : "auto"}
|
dir={!safeValue[index] ? dir : "auto"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export function ConsentElement({
|
|||||||
}: Readonly<ConsentElementProps>) {
|
}: Readonly<ConsentElementProps>) {
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
||||||
const isCurrent = element.id === currentElementId;
|
|
||||||
|
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
||||||
|
|
||||||
@@ -66,7 +65,7 @@ export function ConsentElement({
|
|||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
ref={consentRef}
|
ref={consentRef}
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={0}
|
||||||
id={`${element.id}-label`}
|
id={`${element.id}-label`}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
// Accessibility: if spacebar was pressed pass this down to the input
|
// Accessibility: if spacebar was pressed pass this down to the input
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export function ContactInfoElement({
|
|||||||
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
||||||
const isCurrent = element.id === currentElementId;
|
|
||||||
const safeValue = useMemo(() => {
|
const safeValue = useMemo(() => {
|
||||||
return Array.isArray(value) ? value : ["", "", "", "", ""];
|
return Array.isArray(value) ? value : ["", "", "", "", ""];
|
||||||
}, [value]);
|
}, [value]);
|
||||||
@@ -149,7 +148,7 @@ export function ContactInfoElement({
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
handleChange(field.id, e.currentTarget.value);
|
handleChange(field.id, e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={0}
|
||||||
aria-label={field.label}
|
aria-label={field.label}
|
||||||
dir={!safeValue[index] ? dir : "auto"}
|
dir={!safeValue[index] ? dir : "auto"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function CTAElement({
|
|||||||
<button
|
<button
|
||||||
dir="auto"
|
dir="auto"
|
||||||
type="button"
|
type="button"
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={0}
|
||||||
onClick={handleExternalButtonClick}
|
onClick={handleExternalButtonClick}
|
||||||
className="fb-text-heading focus:fb-ring-focus fb-flex fb-items-center fb-rounded-md fb-px-3 fb-py-3 fb-text-base fb-font-medium fb-leading-4 hover:fb-opacity-90 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2">
|
className="fb-text-heading focus:fb-ring-focus fb-flex fb-items-center fb-rounded-md fb-px-3 fb-py-3 fb-text-base fb-font-medium fb-leading-4 hover:fb-opacity-90 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2">
|
||||||
{getLocalizedValue(element.ctaButtonLabel, languageCode)}
|
{getLocalizedValue(element.ctaButtonLabel, languageCode)}
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ export function DateElement({
|
|||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
||||||
const isCurrent = element.id === currentElementId;
|
|
||||||
const [datePickerOpen, setDatePickerOpen] = useState(false);
|
const [datePickerOpen, setDatePickerOpen] = useState(false);
|
||||||
const [selectedDate, setSelectedDate] = useState<Date | undefined>(value ? new Date(value) : undefined);
|
const [selectedDate, setSelectedDate] = useState<Date | undefined>(value ? new Date(value) : undefined);
|
||||||
const [hideInvalid, setHideInvalid] = useState(!selectedDate);
|
const [hideInvalid, setHideInvalid] = useState(!selectedDate);
|
||||||
@@ -161,7 +160,7 @@ export function DateElement({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDatePickerOpen(true);
|
setDatePickerOpen(true);
|
||||||
}}
|
}}
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={0}
|
||||||
type="button"
|
type="button"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === " ") setDatePickerOpen(true);
|
if (e.key === " ") setDatePickerOpen(true);
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ export function MatrixElement({
|
|||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
||||||
const isCurrent = element.id === currentElementId;
|
|
||||||
const rowShuffleIdx = useMemo(() => {
|
const rowShuffleIdx = useMemo(() => {
|
||||||
if (element.shuffleOption !== "none") {
|
if (element.shuffleOption !== "none") {
|
||||||
return getShuffledRowIndices(element.rows.length, element.shuffleOption);
|
return getShuffledRowIndices(element.rows.length, element.shuffleOption);
|
||||||
@@ -127,7 +126,7 @@ export function MatrixElement({
|
|||||||
{element.columns.map((column, columnIndex) => (
|
{element.columns.map((column, columnIndex) => (
|
||||||
<td
|
<td
|
||||||
key={`column-${columnIndex.toString()}`}
|
key={`column-${columnIndex.toString()}`}
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={0}
|
||||||
className={`fb-outline-brand fb-px-4 fb-py-2 fb-text-slate-800 ${columnIndex === element.columns.length - 1 ? "fb-rounded-r-custom" : ""}`}
|
className={`fb-outline-brand fb-px-4 fb-py-2 fb-text-slate-800 ${columnIndex === element.columns.length - 1 ? "fb-rounded-r-custom" : ""}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleSelect(
|
handleSelect(
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ export function MultipleChoiceMultiElement({
|
|||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
||||||
const isCurrent = element.id === currentElementId;
|
|
||||||
const shuffledChoicesIds = useMemo(() => {
|
const shuffledChoicesIds = useMemo(() => {
|
||||||
if (element.shuffleOption) {
|
if (element.shuffleOption) {
|
||||||
return getShuffledChoicesIds(element.choices, element.shuffleOption);
|
return getShuffledChoicesIds(element.choices, element.shuffleOption);
|
||||||
@@ -212,7 +211,7 @@ export function MultipleChoiceMultiElement({
|
|||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
key={choice.id}
|
key={choice.id}
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={0}
|
||||||
className={labelClassName}
|
className={labelClassName}
|
||||||
onKeyDown={handleKeyDown(choice.id)}
|
onKeyDown={handleKeyDown(choice.id)}
|
||||||
autoFocus={idx === 0 && autoFocusEnabled}>
|
autoFocus={idx === 0 && autoFocusEnabled}>
|
||||||
@@ -260,15 +259,12 @@ export function MultipleChoiceMultiElement({
|
|||||||
: "Please specify";
|
: "Please specify";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label tabIndex={0} className={labelClassName} onKeyDown={handleKeyDown(otherOption.id)}>
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
|
||||||
className={labelClassName}
|
|
||||||
onKeyDown={handleKeyDown(otherOption.id)}>
|
|
||||||
<span className="fb-flex fb-items-center fb-text-sm">
|
<span className="fb-flex fb-items-center fb-text-sm">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
dir={dir}
|
dir={dir}
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={-1}
|
||||||
id={otherOption.id}
|
id={otherOption.id}
|
||||||
name={element.id}
|
name={element.id}
|
||||||
value={otherLabel}
|
value={otherLabel}
|
||||||
@@ -289,7 +285,7 @@ export function MultipleChoiceMultiElement({
|
|||||||
id={`${otherOption.id}-specify`}
|
id={`${otherOption.id}-specify`}
|
||||||
maxLength={250}
|
maxLength={250}
|
||||||
name={element.id}
|
name={element.id}
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={0}
|
||||||
value={otherValue}
|
value={otherValue}
|
||||||
pattern=".*\S+.*"
|
pattern=".*\S+.*"
|
||||||
onChange={(e) => setOtherValue(e.currentTarget.value)}
|
onChange={(e) => setOtherValue(e.currentTarget.value)}
|
||||||
@@ -313,10 +309,7 @@ export function MultipleChoiceMultiElement({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label tabIndex={0} className={labelClassName} onKeyDown={handleKeyDown(noneOption.id)}>
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
|
||||||
className={labelClassName}
|
|
||||||
onKeyDown={handleKeyDown(noneOption.id)}>
|
|
||||||
<span className="fb-flex fb-items-center fb-text-sm">
|
<span className="fb-flex fb-items-center fb-text-sm">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ export function MultipleChoiceSingleElement({
|
|||||||
const otherSpecify = useRef<HTMLInputElement | null>(null);
|
const otherSpecify = useRef<HTMLInputElement | null>(null);
|
||||||
const choicesContainerRef = useRef<HTMLDivElement | null>(null);
|
const choicesContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
||||||
const isCurrent = element.id === currentElementId;
|
|
||||||
const shuffledChoicesIds = useMemo(() => {
|
const shuffledChoicesIds = useMemo(() => {
|
||||||
if (element.shuffleOption) {
|
if (element.shuffleOption) {
|
||||||
return getShuffledChoicesIds(element.choices, element.shuffleOption);
|
return getShuffledChoicesIds(element.choices, element.shuffleOption);
|
||||||
@@ -68,22 +67,10 @@ export function MultipleChoiceSingleElement({
|
|||||||
const noneOption = useMemo(() => element.choices.find((choice) => choice.id === "none"), [element.choices]);
|
const noneOption = useMemo(() => element.choices.find((choice) => choice.id === "none"), [element.choices]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!value) {
|
|
||||||
const prefillAnswer = new URLSearchParams(window.location.search).get(element.id);
|
|
||||||
if (
|
|
||||||
prefillAnswer &&
|
|
||||||
otherOption &&
|
|
||||||
prefillAnswer === getLocalizedValue(otherOption.label, languageCode)
|
|
||||||
) {
|
|
||||||
setOtherSelected(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isOtherSelected =
|
const isOtherSelected =
|
||||||
value !== undefined && !elementChoices.some((choice) => choice?.label[languageCode] === value);
|
value !== undefined && !elementChoices.some((choice) => choice?.label[languageCode] === value);
|
||||||
setOtherSelected(isOtherSelected);
|
setOtherSelected(isOtherSelected);
|
||||||
}, [languageCode, otherOption, element.id, elementChoices, value]);
|
}, [languageCode, elementChoices, value]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Scroll to the bottom of choices container and focus on 'otherSpecify' input when 'otherSelected' is true
|
// Scroll to the bottom of choices container and focus on 'otherSpecify' input when 'otherSelected' is true
|
||||||
@@ -158,7 +145,7 @@ export function MultipleChoiceSingleElement({
|
|||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
key={choice.id}
|
key={choice.id}
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={0}
|
||||||
className={labelClassName}
|
className={labelClassName}
|
||||||
onKeyDown={handleKeyDown(choice.id)}
|
onKeyDown={handleKeyDown(choice.id)}
|
||||||
autoFocus={idx === 0 && autoFocusEnabled}>
|
autoFocus={idx === 0 && autoFocusEnabled}>
|
||||||
@@ -197,7 +184,7 @@ export function MultipleChoiceSingleElement({
|
|||||||
: "Please specify";
|
: "Please specify";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label tabIndex={isCurrent ? 0 : -1} className={labelClassName} onKeyDown={handleOtherKeyDown}>
|
<label tabIndex={0} className={labelClassName} onKeyDown={handleOtherKeyDown}>
|
||||||
<span className="fb-flex fb-items-center fb-text-sm">
|
<span className="fb-flex fb-items-center fb-text-sm">
|
||||||
<input
|
<input
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
@@ -245,10 +232,7 @@ export function MultipleChoiceSingleElement({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label tabIndex={0} className={labelClassName} onKeyDown={handleKeyDown(noneOption.id)}>
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
|
||||||
className={labelClassName}
|
|
||||||
onKeyDown={handleKeyDown(noneOption.id)}>
|
|
||||||
<span className="fb-flex fb-items-center fb-text-sm">
|
<span className="fb-flex fb-items-center fb-text-sm">
|
||||||
<input
|
<input
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export function NPSElement({
|
|||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const [hoveredNumber, setHoveredNumber] = useState(-1);
|
const [hoveredNumber, setHoveredNumber] = useState(-1);
|
||||||
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
||||||
const isCurrent = element.id === currentElementId;
|
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
||||||
|
|
||||||
const handleClick = (number: number) => {
|
const handleClick = (number: number) => {
|
||||||
@@ -74,7 +73,7 @@ export function NPSElement({
|
|||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
key={number}
|
key={number}
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={0}
|
||||||
onMouseOver={() => {
|
onMouseOver={() => {
|
||||||
setHoveredNumber(number);
|
setHoveredNumber(number);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export function OpenTextElement({
|
|||||||
<input
|
<input
|
||||||
ref={inputRef as RefObject<HTMLInputElement>}
|
ref={inputRef as RefObject<HTMLInputElement>}
|
||||||
autoFocus={isCurrent ? autoFocusEnabled : undefined}
|
autoFocus={isCurrent ? autoFocusEnabled : undefined}
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={0}
|
||||||
name={element.id}
|
name={element.id}
|
||||||
id={element.id}
|
id={element.id}
|
||||||
placeholder={getLocalizedValue(element.placeholder, languageCode)}
|
placeholder={getLocalizedValue(element.placeholder, languageCode)}
|
||||||
@@ -195,7 +195,7 @@ export function OpenTextElement({
|
|||||||
rows={3}
|
rows={3}
|
||||||
autoFocus={isCurrent ? autoFocusEnabled : undefined}
|
autoFocus={isCurrent ? autoFocusEnabled : undefined}
|
||||||
name={element.id}
|
name={element.id}
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={0}
|
||||||
aria-label="textarea"
|
aria-label="textarea"
|
||||||
id={element.id}
|
id={element.id}
|
||||||
placeholder={getLocalizedValue(element.placeholder, languageCode, true)}
|
placeholder={getLocalizedValue(element.placeholder, languageCode, true)}
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export function PictureSelectionElement({
|
|||||||
<div className="fb-relative" key={choice.id}>
|
<div className="fb-relative" key={choice.id}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={0}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onClick={() => handleChange(choice.id)}
|
onClick={() => handleChange(choice.id)}
|
||||||
className={getButtonClassName(choice.id)}>
|
className={getButtonClassName(choice.id)}>
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export function RankingElement({
|
|||||||
)}>
|
)}>
|
||||||
<button
|
<button
|
||||||
autoFocus={idx === 0 && autoFocusEnabled}
|
autoFocus={idx === 0 && autoFocusEnabled}
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={0}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === " ") {
|
if (e.key === " ") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ export function RatingElement({
|
|||||||
const [hoveredNumber, setHoveredNumber] = useState(0);
|
const [hoveredNumber, setHoveredNumber] = useState(0);
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
||||||
const isCurrent = element.id === currentElementId;
|
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
||||||
|
|
||||||
const handleSelect = (number: number) => {
|
const handleSelect = (number: number) => {
|
||||||
@@ -163,7 +162,7 @@ export function RatingElement({
|
|||||||
const renderNumberScale = (number: number, totalLength: number) => {
|
const renderNumberScale = (number: number, totalLength: number) => {
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={0}
|
||||||
onKeyDown={handleKeyDown(number)}
|
onKeyDown={handleKeyDown(number)}
|
||||||
className={getNumberLabelClassName(number, totalLength)}>
|
className={getNumberLabelClassName(number, totalLength)}>
|
||||||
{element.isColorCodingEnabled && (
|
{element.isColorCodingEnabled && (
|
||||||
@@ -180,7 +179,7 @@ export function RatingElement({
|
|||||||
const renderStarScale = (number: number) => {
|
const renderStarScale = (number: number) => {
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={0}
|
||||||
onKeyDown={handleKeyDown(number)}
|
onKeyDown={handleKeyDown(number)}
|
||||||
className={getStarLabelClassName(number)}
|
className={getStarLabelClassName(number)}
|
||||||
onFocus={handleFocus(number)}
|
onFocus={handleFocus(number)}
|
||||||
@@ -201,7 +200,7 @@ export function RatingElement({
|
|||||||
const renderSmileyScale = (number: number, idx: number) => {
|
const renderSmileyScale = (number: number, idx: number) => {
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
tabIndex={isCurrent ? 0 : -1}
|
tabIndex={0}
|
||||||
className={getSmileyLabelClassName(number)}
|
className={getSmileyLabelClassName(number)}
|
||||||
onKeyDown={handleKeyDown(number)}
|
onKeyDown={handleKeyDown(number)}
|
||||||
onFocus={handleFocus(number)}
|
onFocus={handleFocus(number)}
|
||||||
|
|||||||
@@ -81,32 +81,32 @@ export function BlockConditional({
|
|||||||
ttcCollectorRef.current[elementId] = elementTtc;
|
ttcCollectorRef.current[elementId] = elementTtc;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle skipPrefilled at block level
|
// Handle prefilled data at block level
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (skipPrefilled && prefilledResponseData) {
|
if (prefilledResponseData) {
|
||||||
// Check if ALL elements in this block have prefilled values
|
// Populate ALL available prefilled values for this block
|
||||||
const allElementsPrefilled = block.elements.every(
|
const prefilledData: TResponseData = {};
|
||||||
(element) => prefilledResponseData[element.id] !== undefined
|
const prefilledTtc: TResponseTtc = {};
|
||||||
);
|
|
||||||
|
|
||||||
if (allElementsPrefilled) {
|
block.elements.forEach((element) => {
|
||||||
// Auto-populate all prefilled values
|
if (prefilledResponseData[element.id] !== undefined) {
|
||||||
const prefilledData: TResponseData = {};
|
|
||||||
const prefilledTtc: TResponseTtc = {};
|
|
||||||
|
|
||||||
block.elements.forEach((element) => {
|
|
||||||
prefilledData[element.id] = prefilledResponseData[element.id];
|
prefilledData[element.id] = prefilledResponseData[element.id];
|
||||||
prefilledTtc[element.id] = 0; // 0 TTC for prefilled/skipped questions
|
prefilledTtc[element.id] = 0; // 0 TTC for prefilled questions
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Update state with prefilled data
|
// ALWAYS populate what we have
|
||||||
|
if (Object.keys(prefilledData).length > 0) {
|
||||||
onChange(prefilledData);
|
onChange(prefilledData);
|
||||||
setTtc({ ...ttc, ...prefilledTtc });
|
setTtc({ ...ttc, ...prefilledTtc });
|
||||||
|
|
||||||
// Auto-submit the entire block (skip to next)
|
// Only auto-skip if skipPrefilled=true AND all elements are filled
|
||||||
setTimeout(() => {
|
const allElementsPrefilled = Object.keys(prefilledData).length === block.elements.length;
|
||||||
onSubmit(prefilledData, prefilledTtc);
|
if (skipPrefilled && allElementsPrefilled) {
|
||||||
}, 0);
|
setTimeout(() => {
|
||||||
|
onSubmit(prefilledData, prefilledTtc);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Only run once when block mounts
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- Only run once when block mounts
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { AutoCloseWrapper } from "@/components/wrappers/auto-close-wrapper";
|
|||||||
import { StackedCardsContainer } from "@/components/wrappers/stacked-cards-container";
|
import { StackedCardsContainer } from "@/components/wrappers/stacked-cards-container";
|
||||||
import { ApiClient } from "@/lib/api-client";
|
import { ApiClient } from "@/lib/api-client";
|
||||||
import { evaluateLogic, performActions } from "@/lib/logic";
|
import { evaluateLogic, performActions } from "@/lib/logic";
|
||||||
|
import { parsePrefillFromURL } from "@/lib/prefill";
|
||||||
import { parseRecallInformation } from "@/lib/recall";
|
import { parseRecallInformation } from "@/lib/recall";
|
||||||
import { ResponseQueue } from "@/lib/response-queue";
|
import { ResponseQueue } from "@/lib/response-queue";
|
||||||
import { SurveyState } from "@/lib/survey-state";
|
import { SurveyState } from "@/lib/survey-state";
|
||||||
@@ -55,7 +56,6 @@ export function Survey({
|
|||||||
onResponseCreated,
|
onResponseCreated,
|
||||||
onOpenExternalURL,
|
onOpenExternalURL,
|
||||||
isRedirectDisabled = false,
|
isRedirectDisabled = false,
|
||||||
prefillResponseData,
|
|
||||||
skipPrefilled,
|
skipPrefilled,
|
||||||
languageCode,
|
languageCode,
|
||||||
getSetIsError,
|
getSetIsError,
|
||||||
@@ -203,6 +203,14 @@ export function Survey({
|
|||||||
return styling.cardArrangement?.appSurveys ?? "straight";
|
return styling.cardArrangement?.appSurveys ?? "straight";
|
||||||
}, [localSurvey.type, styling.cardArrangement?.linkSurveys, styling.cardArrangement?.appSurveys]);
|
}, [localSurvey.type, styling.cardArrangement?.linkSurveys, styling.cardArrangement?.appSurveys]);
|
||||||
|
|
||||||
|
// Parse prefill data from URL
|
||||||
|
const effectivePrefillData = useMemo(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
return parsePrefillFromURL(localSurvey, selectedLanguage);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [localSurvey, selectedLanguage]);
|
||||||
|
|
||||||
// Current block tracking (replaces currentQuestionIndex)
|
// Current block tracking (replaces currentQuestionIndex)
|
||||||
const currentBlockIndex = localSurvey.blocks.findIndex((b) => b.id === blockId);
|
const currentBlockIndex = localSurvey.blocks.findIndex((b) => b.id === blockId);
|
||||||
const currentBlock = localSurvey.blocks[currentBlockIndex];
|
const currentBlock = localSurvey.blocks[currentBlockIndex];
|
||||||
@@ -811,7 +819,7 @@ export function Survey({
|
|||||||
onFileUpload={onFileUpload}
|
onFileUpload={onFileUpload}
|
||||||
isFirstBlock={block.id === localSurvey.blocks[0]?.id}
|
isFirstBlock={block.id === localSurvey.blocks[0]?.id}
|
||||||
skipPrefilled={skipPrefilled}
|
skipPrefilled={skipPrefilled}
|
||||||
prefilledResponseData={offset === 0 ? prefillResponseData : undefined}
|
prefilledResponseData={offset === 0 ? effectivePrefillData : undefined}
|
||||||
isLastBlock={block.id === localSurvey.blocks[localSurvey.blocks.length - 1].id}
|
isLastBlock={block.id === localSurvey.blocks[localSurvey.blocks.length - 1].id}
|
||||||
languageCode={selectedLanguage}
|
languageCode={selectedLanguage}
|
||||||
autoFocusEnabled={autoFocusEnabled}
|
autoFocusEnabled={autoFocusEnabled}
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { beforeEach, describe, expect, test } from "vitest";
|
||||||
|
import type { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||||
|
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||||
|
import { parsePrefillFromURL } from "./prefill";
|
||||||
|
|
||||||
|
describe("parsePrefillFromURL", () => {
|
||||||
|
let mockSurvey: TJsEnvironmentStateSurvey;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset URL search params
|
||||||
|
delete (window as any).location;
|
||||||
|
(window as any).location = { search: "" };
|
||||||
|
|
||||||
|
mockSurvey = {
|
||||||
|
id: "survey-1",
|
||||||
|
name: "Test Survey",
|
||||||
|
type: "link",
|
||||||
|
welcomeCard: { enabled: false },
|
||||||
|
endings: [],
|
||||||
|
variables: [],
|
||||||
|
questions: [],
|
||||||
|
hiddenFields: [],
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
id: "block-1",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
id: "q1",
|
||||||
|
type: TSurveyElementTypeEnum.OpenText,
|
||||||
|
headline: { default: "What is your name?" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "q2",
|
||||||
|
type: TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||||
|
headline: { default: "Choose one" },
|
||||||
|
choices: [
|
||||||
|
{ id: "choice-us", label: { default: "United States" } },
|
||||||
|
{ id: "choice-uk", label: { default: "United Kingdom" } },
|
||||||
|
{ id: "choice-ca", label: { default: "Canada" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "q3",
|
||||||
|
type: TSurveyElementTypeEnum.MultipleChoiceMulti,
|
||||||
|
headline: { default: "Choose multiple" },
|
||||||
|
choices: [
|
||||||
|
{ id: "sport", label: { default: "Sports" } },
|
||||||
|
{ id: "music", label: { default: "Music" } },
|
||||||
|
{ id: "travel", label: { default: "Travel" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should parse simple text parameter", () => {
|
||||||
|
(window as any).location.search = "?q1=John";
|
||||||
|
|
||||||
|
const result = parsePrefillFromURL(mockSurvey, "default");
|
||||||
|
|
||||||
|
expect(result).toEqual({ q1: "John" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle URL encoded values", () => {
|
||||||
|
(window as any).location.search = "?q1=John%20Doe";
|
||||||
|
|
||||||
|
const result = parsePrefillFromURL(mockSurvey, "default");
|
||||||
|
|
||||||
|
expect(result).toEqual({ q1: "John Doe" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should resolve single choice by ID", () => {
|
||||||
|
(window as any).location.search = "?q2=choice-us";
|
||||||
|
|
||||||
|
const result = parsePrefillFromURL(mockSurvey, "default");
|
||||||
|
|
||||||
|
expect(result).toEqual({ q2: "United States" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should fallback to label matching for single choice", () => {
|
||||||
|
(window as any).location.search = "?q2=United%20States";
|
||||||
|
|
||||||
|
const result = parsePrefillFromURL(mockSurvey, "default");
|
||||||
|
|
||||||
|
expect(result).toEqual({ q2: "United States" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should resolve multiple choices by ID", () => {
|
||||||
|
(window as any).location.search = "?q3=sport,music,travel";
|
||||||
|
|
||||||
|
const result = parsePrefillFromURL(mockSurvey, "default");
|
||||||
|
|
||||||
|
expect(result).toEqual({ q3: ["Sports", "Music", "Travel"] });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle multiple parameters", () => {
|
||||||
|
(window as any).location.search = "?q1=John&q2=choice-uk&q3=sport,music";
|
||||||
|
|
||||||
|
const result = parsePrefillFromURL(mockSurvey, "default");
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
q1: "John",
|
||||||
|
q2: "United Kingdom",
|
||||||
|
q3: ["Sports", "Music"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return undefined when no matching parameters", () => {
|
||||||
|
(window as any).location.search = "?unrelated=value";
|
||||||
|
|
||||||
|
const result = parsePrefillFromURL(mockSurvey, "default");
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should ignore invalid choice values", () => {
|
||||||
|
(window as any).location.search = "?q2=invalid-choice&q1=John";
|
||||||
|
|
||||||
|
const result = parsePrefillFromURL(mockSurvey, "default");
|
||||||
|
|
||||||
|
expect(result).toEqual({ q1: "John" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle empty string parameters", () => {
|
||||||
|
(window as any).location.search = "?q1=&q2=choice-us";
|
||||||
|
|
||||||
|
const result = parsePrefillFromURL(mockSurvey, "default");
|
||||||
|
|
||||||
|
expect(result).toEqual({ q2: "United States" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle mixed valid and invalid values in multi-choice", () => {
|
||||||
|
(window as any).location.search = "?q3=sport,invalid,music";
|
||||||
|
|
||||||
|
const result = parsePrefillFromURL(mockSurvey, "default");
|
||||||
|
|
||||||
|
expect(result).toEqual({ q3: ["Sports", "Music"] });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return undefined if window is not defined", () => {
|
||||||
|
const originalWindow = global.window;
|
||||||
|
// @ts-expect-error - testing undefined window
|
||||||
|
delete global.window;
|
||||||
|
|
||||||
|
const result = parsePrefillFromURL(mockSurvey, "default");
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
|
||||||
|
global.window = originalWindow;
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import type { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||||
|
import type { TResponseData } from "@formbricks/types/responses";
|
||||||
|
import {
|
||||||
|
TSurveyElement,
|
||||||
|
TSurveyElementChoice,
|
||||||
|
TSurveyElementTypeEnum,
|
||||||
|
TSurveyPictureChoice,
|
||||||
|
} from "@formbricks/types/surveys/elements";
|
||||||
|
import { getLocalizedValue } from "@/lib/i18n";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse URL query parameters and return prefill data for the survey
|
||||||
|
* Supports option IDs and labels for choice-based questions
|
||||||
|
* Multi-value fields use comma-separated syntax
|
||||||
|
*/
|
||||||
|
export function parsePrefillFromURL(
|
||||||
|
survey: TJsEnvironmentStateSurvey,
|
||||||
|
languageCode: string
|
||||||
|
): TResponseData | undefined {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
|
const prefillData: TResponseData = {};
|
||||||
|
|
||||||
|
// Get all elements from all blocks
|
||||||
|
const allElements: TSurveyElement[] = [];
|
||||||
|
survey.blocks.forEach((block) => {
|
||||||
|
allElements.push(...block.elements);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For each element, check if URL has a matching parameter
|
||||||
|
allElements.forEach((element) => {
|
||||||
|
const urlValue = searchParams.get(element.id);
|
||||||
|
|
||||||
|
if (urlValue !== null && urlValue !== "") {
|
||||||
|
// Resolve the value based on element type
|
||||||
|
const resolvedValue = resolveChoiceValue(element, urlValue, languageCode);
|
||||||
|
|
||||||
|
if (resolvedValue !== undefined) {
|
||||||
|
prefillData[element.id] = resolvedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return undefined if no prefill data found
|
||||||
|
return Object.keys(prefillData).length > 0 ? prefillData : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a URL parameter value to the correct format for an element
|
||||||
|
* For choice-based questions, tries to match option ID first, then label
|
||||||
|
* Handles comma-separated values for multi-value fields
|
||||||
|
*/
|
||||||
|
function resolveChoiceValue(
|
||||||
|
element: TSurveyElement,
|
||||||
|
value: string,
|
||||||
|
languageCode: string
|
||||||
|
): string | string[] | undefined {
|
||||||
|
// Handle choice-based questions
|
||||||
|
if (
|
||||||
|
element.type === TSurveyElementTypeEnum.MultipleChoiceSingle ||
|
||||||
|
element.type === TSurveyElementTypeEnum.MultipleChoiceMulti ||
|
||||||
|
element.type === TSurveyElementTypeEnum.Ranking ||
|
||||||
|
element.type === TSurveyElementTypeEnum.PictureSelection
|
||||||
|
) {
|
||||||
|
// Check if this is a multi-value field
|
||||||
|
const isMultiValue =
|
||||||
|
element.type === TSurveyElementTypeEnum.MultipleChoiceMulti ||
|
||||||
|
element.type === TSurveyElementTypeEnum.Ranking;
|
||||||
|
|
||||||
|
if (isMultiValue) {
|
||||||
|
// Split by comma and resolve each value
|
||||||
|
const values = value.split(",").map((v) => v.trim());
|
||||||
|
const resolvedValues: string[] = [];
|
||||||
|
|
||||||
|
for (const v of values) {
|
||||||
|
const resolved = matchChoice(element.choices, v, languageCode);
|
||||||
|
if (resolved !== undefined) {
|
||||||
|
resolvedValues.push(resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedValues.length > 0 ? resolvedValues : undefined;
|
||||||
|
} else {
|
||||||
|
// Single choice - return as string
|
||||||
|
return matchChoice(element.choices, value, languageCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-choice elements, return value as-is
|
||||||
|
// (text, number, date, etc.)
|
||||||
|
return value || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match a value against choices (either by ID or label)
|
||||||
|
* First tries exact ID match, then tries label match for backward compatibility
|
||||||
|
*/
|
||||||
|
function matchChoice(
|
||||||
|
choices: (TSurveyElementChoice | TSurveyPictureChoice)[],
|
||||||
|
value: string,
|
||||||
|
languageCode: string
|
||||||
|
): string | undefined {
|
||||||
|
// 1. Try exact ID match
|
||||||
|
const byId = choices.find((choice) => choice.id === value);
|
||||||
|
if (byId) {
|
||||||
|
// For regular choices, return the localized label
|
||||||
|
if ("label" in byId) {
|
||||||
|
return getLocalizedValue(byId.label, languageCode);
|
||||||
|
}
|
||||||
|
// For picture choices, return the ID (they don't have labels)
|
||||||
|
return byId.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try label match (backward compatibility) - only for regular choices
|
||||||
|
const byLabel = choices.find(
|
||||||
|
(choice) => "label" in choice && getLocalizedValue(choice.label, languageCode) === value
|
||||||
|
);
|
||||||
|
if (byLabel) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No match found
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user