mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-21 03:03:25 -05:00
Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e1e5ce6270 | |||
| 86cc8fb8ff | |||
| ee56cc10e7 | |||
| 7c47299775 | |||
| 92f4f04f7c | |||
| e072a0e889 | |||
| 4723a428e7 | |||
| d7692a1b76 | |||
| a08f2db40c | |||
| 48eb4fe705 | |||
| 26a2d50d45 | |||
| da5d9e27e1 | |||
| 8e2934d7bb | |||
| 799b86801d | |||
| 9d77b808d0 | |||
| 007d996870 | |||
| 9f59d7a967 | |||
| b9c647ef62 | |||
| 7770d43f9a | |||
| 615aa6aaad | |||
| f57ca755f4 | |||
| fd37b978c0 | |||
| d40ce9ce84 | |||
| 21c63bc400 | |||
| 77722aa638 | |||
| 2a9897370e | |||
| 5e85347bf5 | |||
| e4a9d28b4b | |||
| b79703f87e | |||
| 567cc4b893 | |||
| d9b37496fc | |||
| 87a06c846a | |||
| c5e02a597d | |||
| 1fec0ca7a6 | |||
| ae165eac87 | |||
| 7de5fdc383 | |||
| 3e61d31041 | |||
| d7e537f699 | |||
| 1e6c7609b6 | |||
| 59438d9afe | |||
| e234ed78cf | |||
| 74b168d727 | |||
| 2ed2da61cd | |||
| 729f269d4e | |||
| 41776d0001 | |||
| a2a6870a21 | |||
| 3ab62968e5 | |||
| d4f7f0f35d | |||
| a10cd0cb47 | |||
| 45100673f1 | |||
| 35f53769a5 | |||
| 22ad78a187 | |||
| 67076c4b4c | |||
| 74bfeb132e | |||
| 562b4047ae | |||
| 4791018546 | |||
| be1e546729 | |||
| 5bad0da477 | |||
| 9c776c5e4e | |||
| c50b46f715 | |||
| ce0a0573be | |||
| 3e27143ab1 | |||
| 018e2883ff | |||
| 85fb7ca956 | |||
| 2258699156 | |||
| b1a7b929bd | |||
| fded9a3bad | |||
| 4d84468269 | |||
| e6e010e801 | |||
| 8ced882406 | |||
| f7d462cc7f | |||
| f3d679d087 | |||
| c79a600efc | |||
| 7a8da3b84b | |||
| 4b2d48397d | |||
| 3ea81dc7c1 | |||
| d9b6b550a9 | |||
| 56a6ba08ba | |||
| 1ba55ff66c | |||
| 0cf621d76c | |||
| 3dc615fdc0 | |||
| 7157b17901 | |||
| 82c26941e4 | |||
| 591d5fa3d4 | |||
| 211bca1bd8 | |||
| 5a20839c5b | |||
| 85743bd3d0 | |||
| 335ec02361 | |||
| 7918523957 | |||
| 3b5fe4cb94 | |||
| 6bbd5ec7ef | |||
| c9542dcf79 | |||
| 4277a9dc34 | |||
| b1da63e47d | |||
| 8c05154a86 | |||
| 45122de652 | |||
| 2180bf98ba | |||
| 2d4a94721b | |||
| b2b97c8bed | |||
| f349f7199d | |||
| e7d8803a13 | |||
| 53a9b218bc | |||
| c618e7d473 | |||
| 3d0f703ae1 | |||
| 33eadaaa7b | |||
| 452617529c | |||
| 5951eea618 | |||
| e314feb416 | |||
| 0910b0f1a7 | |||
| 10ba42eb31 | |||
| 04f1e17e23 | |||
| 4642cc60c9 | |||
| 49fa5c587c | |||
| 4f9b48b5e5 | |||
| 80789327d0 | |||
| 38108a32d1 | |||
| ce4b64da0e | |||
| 9790b071d7 | |||
| 1f5ba0e60e | |||
| b502bbc91e | |||
| 6772ac7c20 |
+17
-9
@@ -32,14 +32,22 @@ const mockProject: TProject = {
|
||||
};
|
||||
const mockTemplate: TXMTemplate = {
|
||||
name: "$[projectName] Survey",
|
||||
questions: [
|
||||
blocks: [
|
||||
{
|
||||
id: "q1",
|
||||
inputType: "text",
|
||||
type: "email" as any,
|
||||
headline: { default: "$[projectName] Question" },
|
||||
required: false,
|
||||
charLimit: { enabled: true, min: 400, max: 1000 },
|
||||
id: "block1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
{
|
||||
id: "q1",
|
||||
type: "openText" as const,
|
||||
inputType: "text" as const,
|
||||
headline: { default: "$[projectName] Question" },
|
||||
subheader: { default: "" },
|
||||
required: false,
|
||||
placeholder: { default: "" },
|
||||
charLimit: 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
endings: [
|
||||
@@ -66,9 +74,9 @@ describe("replacePresetPlaceholders", () => {
|
||||
expect(result.name).toBe("Test Project Survey");
|
||||
});
|
||||
|
||||
test("replaces projectName placeholder in question headline", () => {
|
||||
test("replaces projectName placeholder in element headline", () => {
|
||||
const result = replacePresetPlaceholders(mockTemplate, mockProject);
|
||||
expect(result.questions[0].headline.default).toBe("Test Project Question");
|
||||
expect(result.blocks[0].elements[0].headline.default).toBe("Test Project Question");
|
||||
});
|
||||
|
||||
test("returns a new object without mutating the original template", () => {
|
||||
|
||||
+10
-7
@@ -1,13 +1,16 @@
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TSurveyBlock } from "@formbricks/types/surveys/blocks";
|
||||
import { TXMTemplate } from "@formbricks/types/templates";
|
||||
import { replaceQuestionPresetPlaceholders } from "@/lib/utils/templates";
|
||||
import { replaceElementPresetPlaceholders } from "@/lib/utils/templates";
|
||||
|
||||
// replace all occurences of projectName with the actual project name in the current template
|
||||
export const replacePresetPlaceholders = (template: TXMTemplate, project: TProject) => {
|
||||
export const replacePresetPlaceholders = (template: TXMTemplate, project: TProject): TXMTemplate => {
|
||||
const survey = structuredClone(template);
|
||||
survey.name = survey.name.replace("$[projectName]", project.name);
|
||||
survey.questions = survey.questions.map((question) => {
|
||||
return replaceQuestionPresetPlaceholders(question, project);
|
||||
});
|
||||
return { ...template, ...survey };
|
||||
|
||||
const modifiedBlocks = survey.blocks.map((block: TSurveyBlock) => ({
|
||||
...block,
|
||||
elements: block.elements.map((element) => replaceElementPresetPlaceholders(element, project)),
|
||||
}));
|
||||
|
||||
return { ...survey, name: survey.name.replace("$[projectName]", project.name), blocks: modifiedBlocks };
|
||||
};
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ describe("xm-templates", () => {
|
||||
expect(result).toEqual({
|
||||
name: "",
|
||||
endings: expect.any(Array),
|
||||
questions: [],
|
||||
blocks: [],
|
||||
styling: {
|
||||
overwriteThemeStyling: true,
|
||||
},
|
||||
|
||||
+227
-209
@@ -3,19 +3,21 @@ import { TFunction } from "i18next";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TXMTemplate } from "@formbricks/types/templates";
|
||||
import {
|
||||
buildCTAQuestion,
|
||||
buildNPSQuestion,
|
||||
buildOpenTextQuestion,
|
||||
buildRatingQuestion,
|
||||
getDefaultEndingCard,
|
||||
} from "@/app/lib/survey-builder";
|
||||
buildBlock,
|
||||
buildCTAElement,
|
||||
buildNPSElement,
|
||||
buildOpenTextElement,
|
||||
buildRatingElement,
|
||||
createBlockJumpLogic,
|
||||
} from "@/app/lib/survey-block-builder";
|
||||
import { getDefaultEndingCard } from "@/app/lib/survey-builder";
|
||||
|
||||
export const getXMSurveyDefault = (t: TFunction): TXMTemplate => {
|
||||
try {
|
||||
return {
|
||||
name: "",
|
||||
endings: [getDefaultEndingCard([], t)],
|
||||
questions: [],
|
||||
blocks: [],
|
||||
styling: {
|
||||
overwriteThemeStyling: true,
|
||||
},
|
||||
@@ -30,25 +32,40 @@ const npsSurvey = (t: TFunction): TXMTemplate => {
|
||||
return {
|
||||
...getXMSurveyDefault(t),
|
||||
name: t("templates.nps_survey_name"),
|
||||
questions: [
|
||||
buildNPSQuestion({
|
||||
headline: t("templates.nps_survey_question_1_headline"),
|
||||
required: true,
|
||||
lowerLabel: t("templates.nps_survey_question_1_lower_label"),
|
||||
upperLabel: t("templates.nps_survey_question_1_upper_label"),
|
||||
isColorCodingEnabled: true,
|
||||
blocks: [
|
||||
buildBlock({
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
buildNPSElement({
|
||||
headline: t("templates.nps_survey_question_1_headline"),
|
||||
required: true,
|
||||
lowerLabel: t("templates.nps_survey_question_1_lower_label"),
|
||||
upperLabel: t("templates.nps_survey_question_1_upper_label"),
|
||||
isColorCodingEnabled: true,
|
||||
}),
|
||||
],
|
||||
t,
|
||||
}),
|
||||
buildOpenTextQuestion({
|
||||
headline: t("templates.nps_survey_question_2_headline"),
|
||||
required: false,
|
||||
inputType: "text",
|
||||
buildBlock({
|
||||
name: "Block 2",
|
||||
elements: [
|
||||
buildOpenTextElement({
|
||||
headline: t("templates.nps_survey_question_2_headline"),
|
||||
required: false,
|
||||
inputType: "text",
|
||||
}),
|
||||
],
|
||||
t,
|
||||
}),
|
||||
buildOpenTextQuestion({
|
||||
headline: t("templates.nps_survey_question_3_headline"),
|
||||
required: false,
|
||||
inputType: "text",
|
||||
buildBlock({
|
||||
name: "Block 3",
|
||||
elements: [
|
||||
buildOpenTextElement({
|
||||
headline: t("templates.nps_survey_question_3_headline"),
|
||||
required: false,
|
||||
inputType: "text",
|
||||
}),
|
||||
],
|
||||
t,
|
||||
}),
|
||||
],
|
||||
@@ -56,15 +73,27 @@ const npsSurvey = (t: TFunction): TXMTemplate => {
|
||||
};
|
||||
|
||||
const starRatingSurvey = (t: TFunction): TXMTemplate => {
|
||||
const reusableQuestionIds = [createId(), createId(), createId()];
|
||||
const reusableElementIds = [createId(), createId(), createId()];
|
||||
const block3Id = createId(); // Pre-generate Block 3 ID for logic reference
|
||||
const defaultSurvey = getXMSurveyDefault(t);
|
||||
|
||||
return {
|
||||
...defaultSurvey,
|
||||
name: t("templates.star_rating_survey_name"),
|
||||
questions: [
|
||||
buildRatingQuestion({
|
||||
id: reusableQuestionIds[0],
|
||||
blocks: [
|
||||
buildBlock({
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
buildRatingElement({
|
||||
id: reusableElementIds[0],
|
||||
range: 5,
|
||||
scale: "number",
|
||||
headline: t("templates.star_rating_survey_question_1_headline"),
|
||||
required: true,
|
||||
lowerLabel: t("templates.star_rating_survey_question_1_lower_label"),
|
||||
upperLabel: t("templates.star_rating_survey_question_1_upper_label"),
|
||||
}),
|
||||
],
|
||||
logic: [
|
||||
{
|
||||
id: createId(),
|
||||
@@ -75,8 +104,8 @@ const starRatingSurvey = (t: TFunction): TXMTemplate => {
|
||||
{
|
||||
id: createId(),
|
||||
leftOperand: {
|
||||
value: reusableQuestionIds[0],
|
||||
type: "question",
|
||||
value: reusableElementIds[0],
|
||||
type: "element",
|
||||
},
|
||||
operator: "isLessThanOrEqual",
|
||||
rightOperand: {
|
||||
@@ -89,64 +118,44 @@ const starRatingSurvey = (t: TFunction): TXMTemplate => {
|
||||
actions: [
|
||||
{
|
||||
id: createId(),
|
||||
objective: "jumpToQuestion",
|
||||
target: reusableQuestionIds[2],
|
||||
objective: "jumpToBlock",
|
||||
target: block3Id,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
range: 5,
|
||||
scale: "number",
|
||||
headline: t("templates.star_rating_survey_question_1_headline"),
|
||||
required: true,
|
||||
lowerLabel: t("templates.star_rating_survey_question_1_lower_label"),
|
||||
upperLabel: t("templates.star_rating_survey_question_1_upper_label"),
|
||||
t,
|
||||
}),
|
||||
buildCTAQuestion({
|
||||
id: reusableQuestionIds[1],
|
||||
subheader: t("templates.star_rating_survey_question_2_html"),
|
||||
logic: [
|
||||
{
|
||||
id: createId(),
|
||||
conditions: {
|
||||
id: createId(),
|
||||
connector: "and",
|
||||
conditions: [
|
||||
{
|
||||
id: createId(),
|
||||
leftOperand: {
|
||||
value: reusableQuestionIds[1],
|
||||
type: "question",
|
||||
},
|
||||
operator: "isClicked",
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: createId(),
|
||||
objective: "jumpToQuestion",
|
||||
target: defaultSurvey.endings[0].id,
|
||||
},
|
||||
],
|
||||
},
|
||||
buildBlock({
|
||||
name: "Block 2",
|
||||
elements: [
|
||||
buildCTAElement({
|
||||
id: reusableElementIds[1],
|
||||
subheader: t("templates.star_rating_survey_question_2_html"),
|
||||
headline: t("templates.star_rating_survey_question_2_headline"),
|
||||
required: false,
|
||||
buttonUrl: "https://formbricks.com/github",
|
||||
buttonExternal: true,
|
||||
ctaButtonLabel: t("templates.star_rating_survey_question_2_button_label"),
|
||||
}),
|
||||
],
|
||||
headline: t("templates.star_rating_survey_question_2_headline"),
|
||||
required: true,
|
||||
buttonUrl: "https://formbricks.com/github",
|
||||
buttonLabel: t("templates.star_rating_survey_question_2_button_label"),
|
||||
buttonExternal: true,
|
||||
logic: [createBlockJumpLogic(reusableElementIds[1], defaultSurvey.endings[0].id, "isClicked")],
|
||||
t,
|
||||
}),
|
||||
buildOpenTextQuestion({
|
||||
id: reusableQuestionIds[2],
|
||||
headline: t("templates.star_rating_survey_question_3_headline"),
|
||||
required: true,
|
||||
subheader: t("templates.star_rating_survey_question_3_subheader"),
|
||||
buildBlock({
|
||||
id: block3Id,
|
||||
name: "Block 3",
|
||||
elements: [
|
||||
buildOpenTextElement({
|
||||
id: reusableElementIds[2],
|
||||
headline: t("templates.star_rating_survey_question_3_headline"),
|
||||
required: true,
|
||||
subheader: t("templates.star_rating_survey_question_3_subheader"),
|
||||
placeholder: t("templates.star_rating_survey_question_3_placeholder"),
|
||||
inputType: "text",
|
||||
}),
|
||||
],
|
||||
buttonLabel: t("templates.star_rating_survey_question_3_button_label"),
|
||||
placeholder: t("templates.star_rating_survey_question_3_placeholder"),
|
||||
inputType: "text",
|
||||
t,
|
||||
}),
|
||||
],
|
||||
@@ -154,15 +163,27 @@ const starRatingSurvey = (t: TFunction): TXMTemplate => {
|
||||
};
|
||||
|
||||
const csatSurvey = (t: TFunction): TXMTemplate => {
|
||||
const reusableQuestionIds = [createId(), createId(), createId()];
|
||||
const reusableElementIds = [createId(), createId(), createId()];
|
||||
const block3Id = createId(); // Pre-generate Block 3 ID for logic reference
|
||||
const defaultSurvey = getXMSurveyDefault(t);
|
||||
|
||||
return {
|
||||
...defaultSurvey,
|
||||
name: t("templates.csat_survey_name"),
|
||||
questions: [
|
||||
buildRatingQuestion({
|
||||
id: reusableQuestionIds[0],
|
||||
blocks: [
|
||||
buildBlock({
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
buildRatingElement({
|
||||
id: reusableElementIds[0],
|
||||
range: 5,
|
||||
scale: "smiley",
|
||||
headline: t("templates.csat_survey_question_1_headline"),
|
||||
required: true,
|
||||
lowerLabel: t("templates.csat_survey_question_1_lower_label"),
|
||||
upperLabel: t("templates.csat_survey_question_1_upper_label"),
|
||||
}),
|
||||
],
|
||||
logic: [
|
||||
{
|
||||
id: createId(),
|
||||
@@ -173,8 +194,8 @@ const csatSurvey = (t: TFunction): TXMTemplate => {
|
||||
{
|
||||
id: createId(),
|
||||
leftOperand: {
|
||||
value: reusableQuestionIds[0],
|
||||
type: "question",
|
||||
value: reusableElementIds[0],
|
||||
type: "element",
|
||||
},
|
||||
operator: "isLessThanOrEqual",
|
||||
rightOperand: {
|
||||
@@ -187,60 +208,40 @@ const csatSurvey = (t: TFunction): TXMTemplate => {
|
||||
actions: [
|
||||
{
|
||||
id: createId(),
|
||||
objective: "jumpToQuestion",
|
||||
target: reusableQuestionIds[2],
|
||||
objective: "jumpToBlock",
|
||||
target: block3Id,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
range: 5,
|
||||
scale: "smiley",
|
||||
headline: t("templates.csat_survey_question_1_headline"),
|
||||
required: true,
|
||||
lowerLabel: t("templates.csat_survey_question_1_lower_label"),
|
||||
upperLabel: t("templates.csat_survey_question_1_upper_label"),
|
||||
t,
|
||||
}),
|
||||
buildOpenTextQuestion({
|
||||
id: reusableQuestionIds[1],
|
||||
logic: [
|
||||
{
|
||||
id: createId(),
|
||||
conditions: {
|
||||
id: createId(),
|
||||
connector: "and",
|
||||
conditions: [
|
||||
{
|
||||
id: createId(),
|
||||
leftOperand: {
|
||||
value: reusableQuestionIds[1],
|
||||
type: "question",
|
||||
},
|
||||
operator: "isSubmitted",
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: createId(),
|
||||
objective: "jumpToQuestion",
|
||||
target: defaultSurvey.endings[0].id,
|
||||
},
|
||||
],
|
||||
},
|
||||
buildBlock({
|
||||
name: "Block 2",
|
||||
elements: [
|
||||
buildOpenTextElement({
|
||||
id: reusableElementIds[1],
|
||||
headline: t("templates.csat_survey_question_2_headline"),
|
||||
required: false,
|
||||
placeholder: t("templates.csat_survey_question_2_placeholder"),
|
||||
inputType: "text",
|
||||
}),
|
||||
],
|
||||
headline: t("templates.csat_survey_question_2_headline"),
|
||||
required: false,
|
||||
placeholder: t("templates.csat_survey_question_2_placeholder"),
|
||||
inputType: "text",
|
||||
logic: [createBlockJumpLogic(reusableElementIds[1], defaultSurvey.endings[0].id, "isSubmitted")],
|
||||
t,
|
||||
}),
|
||||
buildOpenTextQuestion({
|
||||
id: reusableQuestionIds[2],
|
||||
headline: t("templates.csat_survey_question_3_headline"),
|
||||
required: false,
|
||||
placeholder: t("templates.csat_survey_question_3_placeholder"),
|
||||
inputType: "text",
|
||||
buildBlock({
|
||||
id: block3Id,
|
||||
name: "Block 3",
|
||||
elements: [
|
||||
buildOpenTextElement({
|
||||
id: reusableElementIds[2],
|
||||
headline: t("templates.csat_survey_question_3_headline"),
|
||||
required: false,
|
||||
placeholder: t("templates.csat_survey_question_3_placeholder"),
|
||||
inputType: "text",
|
||||
}),
|
||||
],
|
||||
t,
|
||||
}),
|
||||
],
|
||||
@@ -251,21 +252,31 @@ const cessSurvey = (t: TFunction): TXMTemplate => {
|
||||
return {
|
||||
...getXMSurveyDefault(t),
|
||||
name: t("templates.cess_survey_name"),
|
||||
questions: [
|
||||
buildRatingQuestion({
|
||||
range: 5,
|
||||
scale: "number",
|
||||
headline: t("templates.cess_survey_question_1_headline"),
|
||||
required: true,
|
||||
lowerLabel: t("templates.cess_survey_question_1_lower_label"),
|
||||
upperLabel: t("templates.cess_survey_question_1_upper_label"),
|
||||
blocks: [
|
||||
buildBlock({
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
buildRatingElement({
|
||||
range: 5,
|
||||
scale: "number",
|
||||
headline: t("templates.cess_survey_question_1_headline"),
|
||||
required: true,
|
||||
lowerLabel: t("templates.cess_survey_question_1_lower_label"),
|
||||
upperLabel: t("templates.cess_survey_question_1_upper_label"),
|
||||
}),
|
||||
],
|
||||
t,
|
||||
}),
|
||||
buildOpenTextQuestion({
|
||||
headline: t("templates.cess_survey_question_2_headline"),
|
||||
required: true,
|
||||
placeholder: t("templates.cess_survey_question_2_placeholder"),
|
||||
inputType: "text",
|
||||
buildBlock({
|
||||
name: "Block 2",
|
||||
elements: [
|
||||
buildOpenTextElement({
|
||||
headline: t("templates.cess_survey_question_2_headline"),
|
||||
required: true,
|
||||
placeholder: t("templates.cess_survey_question_2_placeholder"),
|
||||
inputType: "text",
|
||||
}),
|
||||
],
|
||||
t,
|
||||
}),
|
||||
],
|
||||
@@ -273,15 +284,27 @@ const cessSurvey = (t: TFunction): TXMTemplate => {
|
||||
};
|
||||
|
||||
const smileysRatingSurvey = (t: TFunction): TXMTemplate => {
|
||||
const reusableQuestionIds = [createId(), createId(), createId()];
|
||||
const reusableElementIds = [createId(), createId(), createId()];
|
||||
const block3Id = createId(); // Pre-generate Block 3 ID for logic reference
|
||||
const defaultSurvey = getXMSurveyDefault(t);
|
||||
|
||||
return {
|
||||
...defaultSurvey,
|
||||
name: t("templates.smileys_survey_name"),
|
||||
questions: [
|
||||
buildRatingQuestion({
|
||||
id: reusableQuestionIds[0],
|
||||
blocks: [
|
||||
buildBlock({
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
buildRatingElement({
|
||||
id: reusableElementIds[0],
|
||||
range: 5,
|
||||
scale: "smiley",
|
||||
headline: t("templates.smileys_survey_question_1_headline"),
|
||||
required: true,
|
||||
lowerLabel: t("templates.smileys_survey_question_1_lower_label"),
|
||||
upperLabel: t("templates.smileys_survey_question_1_upper_label"),
|
||||
}),
|
||||
],
|
||||
logic: [
|
||||
{
|
||||
id: createId(),
|
||||
@@ -292,8 +315,8 @@ const smileysRatingSurvey = (t: TFunction): TXMTemplate => {
|
||||
{
|
||||
id: createId(),
|
||||
leftOperand: {
|
||||
value: reusableQuestionIds[0],
|
||||
type: "question",
|
||||
value: reusableElementIds[0],
|
||||
type: "element",
|
||||
},
|
||||
operator: "isLessThanOrEqual",
|
||||
rightOperand: {
|
||||
@@ -306,64 +329,44 @@ const smileysRatingSurvey = (t: TFunction): TXMTemplate => {
|
||||
actions: [
|
||||
{
|
||||
id: createId(),
|
||||
objective: "jumpToQuestion",
|
||||
target: reusableQuestionIds[2],
|
||||
objective: "jumpToBlock",
|
||||
target: block3Id,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
range: 5,
|
||||
scale: "smiley",
|
||||
headline: t("templates.smileys_survey_question_1_headline"),
|
||||
required: true,
|
||||
lowerLabel: t("templates.smileys_survey_question_1_lower_label"),
|
||||
upperLabel: t("templates.smileys_survey_question_1_upper_label"),
|
||||
t,
|
||||
}),
|
||||
buildCTAQuestion({
|
||||
id: reusableQuestionIds[1],
|
||||
subheader: t("templates.smileys_survey_question_2_html"),
|
||||
logic: [
|
||||
{
|
||||
id: createId(),
|
||||
conditions: {
|
||||
id: createId(),
|
||||
connector: "and",
|
||||
conditions: [
|
||||
{
|
||||
id: createId(),
|
||||
leftOperand: {
|
||||
value: reusableQuestionIds[1],
|
||||
type: "question",
|
||||
},
|
||||
operator: "isClicked",
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: createId(),
|
||||
objective: "jumpToQuestion",
|
||||
target: defaultSurvey.endings[0].id,
|
||||
},
|
||||
],
|
||||
},
|
||||
buildBlock({
|
||||
name: "Block 2",
|
||||
elements: [
|
||||
buildCTAElement({
|
||||
id: reusableElementIds[1],
|
||||
subheader: t("templates.smileys_survey_question_2_html"),
|
||||
headline: t("templates.smileys_survey_question_2_headline"),
|
||||
required: false,
|
||||
buttonUrl: "https://formbricks.com/github",
|
||||
buttonExternal: true,
|
||||
ctaButtonLabel: t("templates.smileys_survey_question_2_button_label"),
|
||||
}),
|
||||
],
|
||||
headline: t("templates.smileys_survey_question_2_headline"),
|
||||
required: true,
|
||||
buttonUrl: "https://formbricks.com/github",
|
||||
buttonLabel: t("templates.smileys_survey_question_2_button_label"),
|
||||
buttonExternal: true,
|
||||
logic: [createBlockJumpLogic(reusableElementIds[1], defaultSurvey.endings[0].id, "isClicked")],
|
||||
t,
|
||||
}),
|
||||
buildOpenTextQuestion({
|
||||
id: reusableQuestionIds[2],
|
||||
headline: t("templates.smileys_survey_question_3_headline"),
|
||||
required: true,
|
||||
subheader: t("templates.smileys_survey_question_3_subheader"),
|
||||
buildBlock({
|
||||
id: block3Id,
|
||||
name: "Block 3",
|
||||
elements: [
|
||||
buildOpenTextElement({
|
||||
id: reusableElementIds[2],
|
||||
headline: t("templates.smileys_survey_question_3_headline"),
|
||||
required: true,
|
||||
subheader: t("templates.smileys_survey_question_3_subheader"),
|
||||
placeholder: t("templates.smileys_survey_question_3_placeholder"),
|
||||
inputType: "text",
|
||||
}),
|
||||
],
|
||||
buttonLabel: t("templates.smileys_survey_question_3_button_label"),
|
||||
placeholder: t("templates.smileys_survey_question_3_placeholder"),
|
||||
inputType: "text",
|
||||
t,
|
||||
}),
|
||||
],
|
||||
@@ -374,25 +377,40 @@ const enpsSurvey = (t: TFunction): TXMTemplate => {
|
||||
return {
|
||||
...getXMSurveyDefault(t),
|
||||
name: t("templates.enps_survey_name"),
|
||||
questions: [
|
||||
buildNPSQuestion({
|
||||
headline: t("templates.enps_survey_question_1_headline"),
|
||||
required: false,
|
||||
lowerLabel: t("templates.enps_survey_question_1_lower_label"),
|
||||
upperLabel: t("templates.enps_survey_question_1_upper_label"),
|
||||
isColorCodingEnabled: true,
|
||||
blocks: [
|
||||
buildBlock({
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
buildNPSElement({
|
||||
headline: t("templates.enps_survey_question_1_headline"),
|
||||
required: false,
|
||||
lowerLabel: t("templates.enps_survey_question_1_lower_label"),
|
||||
upperLabel: t("templates.enps_survey_question_1_upper_label"),
|
||||
isColorCodingEnabled: true,
|
||||
}),
|
||||
],
|
||||
t,
|
||||
}),
|
||||
buildOpenTextQuestion({
|
||||
headline: t("templates.enps_survey_question_2_headline"),
|
||||
required: false,
|
||||
inputType: "text",
|
||||
buildBlock({
|
||||
name: "Block 2",
|
||||
elements: [
|
||||
buildOpenTextElement({
|
||||
headline: t("templates.enps_survey_question_2_headline"),
|
||||
required: false,
|
||||
inputType: "text",
|
||||
}),
|
||||
],
|
||||
t,
|
||||
}),
|
||||
buildOpenTextQuestion({
|
||||
headline: t("templates.enps_survey_question_3_headline"),
|
||||
required: false,
|
||||
inputType: "text",
|
||||
buildBlock({
|
||||
name: "Block 3",
|
||||
elements: [
|
||||
buildOpenTextElement({
|
||||
headline: t("templates.enps_survey_question_3_headline"),
|
||||
required: false,
|
||||
inputType: "text",
|
||||
}),
|
||||
],
|
||||
t,
|
||||
}),
|
||||
],
|
||||
|
||||
+62
-32
@@ -3,7 +3,7 @@
|
||||
import { TFunction } from "i18next";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Control, Controller, useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -14,14 +14,15 @@ import {
|
||||
TIntegrationAirtableInput,
|
||||
TIntegrationAirtableTables,
|
||||
} from "@formbricks/types/integration/airtable";
|
||||
import { TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
|
||||
import { BaseSelectDropdown } from "@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/BaseSelectDropdown";
|
||||
import { fetchTables } from "@/app/(app)/environments/[environmentId]/project/integrations/airtable/lib/airtable";
|
||||
import AirtableLogo from "@/images/airtableLogo.svg";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
||||
import { recallToHeadline } from "@/lib/utils/recall";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -45,6 +46,45 @@ import {
|
||||
} from "@/modules/ui/components/select";
|
||||
import { IntegrationModalInputs } from "../lib/types";
|
||||
|
||||
const ElementCheckbox = ({
|
||||
element,
|
||||
selectedSurvey,
|
||||
field,
|
||||
}: {
|
||||
element: TSurveyElement;
|
||||
selectedSurvey: TSurvey;
|
||||
field: {
|
||||
value: string[] | undefined;
|
||||
onChange: (value: string[]) => void;
|
||||
};
|
||||
}) => {
|
||||
const handleCheckedChange = (checked: boolean) => {
|
||||
if (checked) {
|
||||
field.onChange([...(field.value || []), element.id]);
|
||||
} else {
|
||||
field.onChange(field.value?.filter((value) => value !== element.id) || []);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={element.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
type="button"
|
||||
id={element.id}
|
||||
value={element.id}
|
||||
className="bg-white"
|
||||
checked={field.value?.includes(element.id)}
|
||||
onCheckedChange={handleCheckedChange}
|
||||
/>
|
||||
<span className="ml-2">
|
||||
{getTextContent(recallToHeadline(element.headline, selectedSurvey, false, "default")["default"])}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type EditModeProps =
|
||||
| { isEditMode: false; defaultData?: never }
|
||||
| { isEditMode: true; defaultData: IntegrationModalInputs & { index: number } };
|
||||
@@ -68,9 +108,10 @@ const NoBaseFoundError = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderQuestionSelection = ({
|
||||
const renderElementSelection = ({
|
||||
t,
|
||||
selectedSurvey,
|
||||
elements,
|
||||
control,
|
||||
includeVariables,
|
||||
setIncludeVariables,
|
||||
@@ -83,6 +124,7 @@ const renderQuestionSelection = ({
|
||||
}: {
|
||||
t: TFunction;
|
||||
selectedSurvey: TSurvey;
|
||||
elements: TSurveyElement[];
|
||||
control: Control<IntegrationModalInputs>;
|
||||
includeVariables: boolean;
|
||||
setIncludeVariables: (value: boolean) => void;
|
||||
@@ -99,31 +141,13 @@ const renderQuestionSelection = ({
|
||||
<Label htmlFor="Surveys">{t("common.questions")}</Label>
|
||||
<div className="mt-1 max-h-[15vh] overflow-y-auto rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{replaceHeadlineRecall(selectedSurvey, "default")?.questions.map((question) => (
|
||||
{elements.map((element) => (
|
||||
<Controller
|
||||
key={question.id}
|
||||
key={element.id}
|
||||
control={control}
|
||||
name={"questions"}
|
||||
name={"elements"}
|
||||
render={({ field }) => (
|
||||
<div className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
type="button"
|
||||
id={question.id}
|
||||
value={question.id}
|
||||
className="bg-white"
|
||||
checked={field.value?.includes(question.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([...field.value, question.id])
|
||||
: field.onChange(field.value?.filter((value) => value !== question.id));
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">
|
||||
{getTextContent(getLocalizedValue(question.headline, "default"))}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<ElementCheckbox element={element} selectedSurvey={selectedSurvey} field={field} />
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
@@ -194,6 +218,11 @@ export const AddIntegrationModal = ({
|
||||
};
|
||||
|
||||
const selectedSurvey = surveys.find((item) => item.id === survey);
|
||||
const elements = useMemo(
|
||||
() => (selectedSurvey ? getElementsFromBlocks(selectedSurvey.blocks) : []),
|
||||
[selectedSurvey]
|
||||
);
|
||||
|
||||
const submitHandler = async (data: IntegrationModalInputs) => {
|
||||
try {
|
||||
if (!data.base || data.base === "") {
|
||||
@@ -208,7 +237,7 @@ export const AddIntegrationModal = ({
|
||||
throw new Error(t("environments.integrations.please_select_a_survey_error"));
|
||||
}
|
||||
|
||||
if (data.questions.length === 0) {
|
||||
if (data.elements.length === 0) {
|
||||
throw new Error(t("environments.integrations.select_at_least_one_question_error"));
|
||||
}
|
||||
|
||||
@@ -216,9 +245,9 @@ export const AddIntegrationModal = ({
|
||||
const integrationData: TIntegrationAirtableConfigData = {
|
||||
surveyId: selectedSurvey.id,
|
||||
surveyName: selectedSurvey.name,
|
||||
questionIds: data.questions,
|
||||
questions:
|
||||
data.questions.length === selectedSurvey.questions.length
|
||||
elementIds: data.elements,
|
||||
elements:
|
||||
data.elements.length === elements.length
|
||||
? t("common.all_questions")
|
||||
: t("common.selected_questions"),
|
||||
createdAt: new Date(),
|
||||
@@ -366,7 +395,7 @@ export const AddIntegrationModal = ({
|
||||
required
|
||||
onValueChange={(val) => {
|
||||
field.onChange(val);
|
||||
setValue("questions", []);
|
||||
setValue("elements", []);
|
||||
}}
|
||||
defaultValue={defaultData?.survey}>
|
||||
<SelectTrigger>
|
||||
@@ -392,9 +421,10 @@ export const AddIntegrationModal = ({
|
||||
|
||||
{survey &&
|
||||
selectedSurvey &&
|
||||
renderQuestionSelection({
|
||||
renderElementSelection({
|
||||
t,
|
||||
selectedSurvey,
|
||||
elements: elements,
|
||||
control,
|
||||
includeVariables,
|
||||
setIncludeVariables,
|
||||
|
||||
+2
-2
@@ -108,7 +108,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
||||
onClick={() => {
|
||||
setDefaultValues({
|
||||
base: data.baseId,
|
||||
questions: data.questionIds,
|
||||
elements: data.elementIds,
|
||||
survey: data.surveyId,
|
||||
table: data.tableId,
|
||||
includeVariables: !!data.includeVariables,
|
||||
@@ -121,7 +121,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
||||
}}>
|
||||
<div className="col-span-2 text-center">{data.surveyName}</div>
|
||||
<div className="col-span-2 text-center">{data.tableName}</div>
|
||||
<div className="col-span-2 text-center">{data.questions}</div>
|
||||
<div className="col-span-2 text-center">{data.elements}</div>
|
||||
<div className="col-span-2 text-center">
|
||||
{timeSince(data.createdAt.toString(), props.locale)}
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ export type IntegrationModalInputs = {
|
||||
base: string;
|
||||
table: string;
|
||||
survey: string;
|
||||
questions: string[];
|
||||
elements: string[];
|
||||
includeVariables: boolean;
|
||||
includeHiddenFields: boolean;
|
||||
includeMetadata: boolean;
|
||||
|
||||
+27
-18
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -20,9 +20,9 @@ import {
|
||||
isValidGoogleSheetsUrl,
|
||||
} from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/lib/util";
|
||||
import GoogleSheetLogo from "@/images/googleSheetsLogo.png";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
||||
import { recallToHeadline } from "@/lib/utils/recall";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Checkbox } from "@/modules/ui/components/checkbox";
|
||||
@@ -62,12 +62,12 @@ export const AddIntegrationModal = ({
|
||||
spreadsheetName: "",
|
||||
surveyId: "",
|
||||
surveyName: "",
|
||||
questionIds: [""],
|
||||
questions: "",
|
||||
elementIds: [""],
|
||||
elements: "",
|
||||
createdAt: new Date(),
|
||||
};
|
||||
const { handleSubmit } = useForm();
|
||||
const [selectedQuestions, setSelectedQuestions] = useState<string[]>([]);
|
||||
const [selectedElements, setSelectedElements] = useState<string[]>([]);
|
||||
const [isLinkingSheet, setIsLinkingSheet] = useState(false);
|
||||
const [selectedSurvey, setSelectedSurvey] = useState<TSurvey | null>(null);
|
||||
const [spreadsheetUrl, setSpreadsheetUrl] = useState("");
|
||||
@@ -86,12 +86,17 @@ export const AddIntegrationModal = ({
|
||||
},
|
||||
};
|
||||
|
||||
const surveyElements = useMemo(
|
||||
() => (selectedSurvey ? getElementsFromBlocks(selectedSurvey.blocks) : []),
|
||||
[selectedSurvey]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSurvey && !selectedIntegration) {
|
||||
const questionIds = selectedSurvey.questions.map((question) => question.id);
|
||||
setSelectedQuestions(questionIds);
|
||||
const elementIds = surveyElements.map((element) => element.id);
|
||||
setSelectedElements(elementIds);
|
||||
}
|
||||
}, [selectedIntegration, selectedSurvey]);
|
||||
}, [surveyElements, selectedIntegration, selectedSurvey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedIntegration) {
|
||||
@@ -101,7 +106,7 @@ export const AddIntegrationModal = ({
|
||||
return survey.id === selectedIntegration.surveyId;
|
||||
})!
|
||||
);
|
||||
setSelectedQuestions(selectedIntegration.questionIds);
|
||||
setSelectedElements(selectedIntegration.elementIds);
|
||||
setIncludeVariables(!!selectedIntegration.includeVariables);
|
||||
setIncludeHiddenFields(!!selectedIntegration.includeHiddenFields);
|
||||
setIncludeMetadata(!!selectedIntegration.includeMetadata);
|
||||
@@ -121,7 +126,7 @@ export const AddIntegrationModal = ({
|
||||
if (!selectedSurvey) {
|
||||
throw new Error(t("environments.integrations.please_select_a_survey_error"));
|
||||
}
|
||||
if (selectedQuestions.length === 0) {
|
||||
if (selectedElements.length === 0) {
|
||||
throw new Error(t("environments.integrations.select_at_least_one_question_error"));
|
||||
}
|
||||
const spreadsheetId = extractSpreadsheetIdFromUrl(spreadsheetUrl);
|
||||
@@ -143,9 +148,9 @@ export const AddIntegrationModal = ({
|
||||
integrationData.spreadsheetName = spreadsheetName;
|
||||
integrationData.surveyId = selectedSurvey.id;
|
||||
integrationData.surveyName = selectedSurvey.name;
|
||||
integrationData.questionIds = selectedQuestions;
|
||||
integrationData.questions =
|
||||
selectedQuestions.length === selectedSurvey?.questions.length
|
||||
integrationData.elementIds = selectedElements;
|
||||
integrationData.elements =
|
||||
selectedElements.length === surveyElements.length
|
||||
? t("common.all_questions")
|
||||
: t("common.selected_questions");
|
||||
integrationData.createdAt = new Date();
|
||||
@@ -176,7 +181,7 @@ export const AddIntegrationModal = ({
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (questionId: TSurveyQuestionId) => {
|
||||
setSelectedQuestions((prevValues) =>
|
||||
setSelectedElements((prevValues) =>
|
||||
prevValues.includes(questionId)
|
||||
? prevValues.filter((value) => value !== questionId)
|
||||
: [...prevValues, questionId]
|
||||
@@ -263,7 +268,7 @@ export const AddIntegrationModal = ({
|
||||
<Label htmlFor="Surveys">{t("common.questions")}</Label>
|
||||
<div className="mt-1 max-h-[15vh] overflow-y-auto overflow-x-hidden rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{replaceHeadlineRecall(selectedSurvey, "default")?.questions.map((question) => (
|
||||
{surveyElements.map((question) => (
|
||||
<div key={question.id} className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
@@ -271,13 +276,17 @@ export const AddIntegrationModal = ({
|
||||
id={question.id}
|
||||
value={question.id}
|
||||
className="bg-white"
|
||||
checked={selectedQuestions.includes(question.id)}
|
||||
checked={selectedElements.includes(question.id)}
|
||||
onCheckedChange={() => {
|
||||
handleCheckboxChange(question.id);
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2 w-[30rem] truncate">
|
||||
{getTextContent(getLocalizedValue(question.headline, "default"))}
|
||||
{getTextContent(
|
||||
recallToHeadline(question.headline, selectedSurvey, false, "default")[
|
||||
"default"
|
||||
]
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -110,7 +110,7 @@ export const ManageIntegration = ({
|
||||
}}>
|
||||
<div className="col-span-2 text-center">{data.surveyName}</div>
|
||||
<div className="col-span-2 text-center">{data.spreadsheetName}</div>
|
||||
<div className="col-span-2 text-center">{data.questions}</div>
|
||||
<div className="col-span-2 text-center">{data.elements}</div>
|
||||
<div className="col-span-2 text-center">{timeSince(data.createdAt.toString(), locale)}</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
+98
-81
@@ -12,7 +12,8 @@ import {
|
||||
TIntegrationNotionConfigData,
|
||||
TIntegrationNotionDatabase,
|
||||
} from "@formbricks/types/integration/notion";
|
||||
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
|
||||
import {
|
||||
@@ -21,10 +22,10 @@ import {
|
||||
UNSUPPORTED_TYPES_BY_NOTION,
|
||||
} from "@/app/(app)/environments/[environmentId]/project/integrations/notion/constants";
|
||||
import NotionLogo from "@/images/notion.png";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
||||
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
||||
import { getQuestionTypes } from "@/modules/survey/lib/questions";
|
||||
import { recallToHeadline } from "@/lib/utils/recall";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { getElementTypes } from "@/modules/survey/lib/elements";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -38,6 +39,59 @@ import {
|
||||
import { DropdownSelector } from "@/modules/ui/components/dropdown-selector";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
|
||||
const MappingErrorMessage = ({
|
||||
error,
|
||||
col,
|
||||
elem,
|
||||
t,
|
||||
}: {
|
||||
error: { type: string; msg?: React.ReactNode | string } | null | undefined;
|
||||
col: { id: string; name: string; type: string };
|
||||
elem: { id: string; name: string; type: string };
|
||||
t: ReturnType<typeof useTranslation>["t"];
|
||||
}) => {
|
||||
const showErrorMsg = useMemo(() => {
|
||||
switch (error?.type) {
|
||||
case ERRORS.UNSUPPORTED_TYPE:
|
||||
return (
|
||||
<>
|
||||
-{" "}
|
||||
{t("environments.integrations.notion.col_name_of_type_is_not_supported", {
|
||||
col_name: col.name,
|
||||
type: col.type,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
case ERRORS.MAPPING:
|
||||
const element = getElementTypes(t).find((et) => et.id === elem.type);
|
||||
if (!element) return null;
|
||||
return (
|
||||
<>
|
||||
{t("environments.integrations.notion.que_name_of_type_cant_be_mapped_to", {
|
||||
que_name: elem.name,
|
||||
question_label: element.label,
|
||||
col_name: col.name,
|
||||
col_type: col.type,
|
||||
mapped_type: TYPE_MAPPING[element.id].join(" ,"),
|
||||
})}
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error, col, elem, t]);
|
||||
|
||||
if (!error) return null;
|
||||
|
||||
return (
|
||||
<div className="my-4 w-full rounded-lg bg-red-100 p-4 text-sm text-red-800">
|
||||
<span className="mb-2 block">{error.type}</span>
|
||||
{showErrorMsg}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface AddIntegrationModalProps {
|
||||
environmentId: string;
|
||||
surveys: TSurvey[];
|
||||
@@ -64,7 +118,7 @@ export const AddIntegrationModal = ({
|
||||
const [mapping, setMapping] = useState<
|
||||
{
|
||||
column: { id: string; name: string; type: string };
|
||||
question: { id: string; name: string; type: string };
|
||||
element: { id: string; name: string; type: string };
|
||||
error?: {
|
||||
type: string;
|
||||
msg: React.ReactNode | string;
|
||||
@@ -73,7 +127,7 @@ export const AddIntegrationModal = ({
|
||||
>([
|
||||
{
|
||||
column: { id: "", name: "", type: "" },
|
||||
question: { id: "", name: "", type: "" },
|
||||
element: { id: "", name: "", type: "" },
|
||||
},
|
||||
]);
|
||||
const [isDeleting, setIsDeleting] = useState<boolean>(false);
|
||||
@@ -86,12 +140,17 @@ export const AddIntegrationModal = ({
|
||||
mapping: [
|
||||
{
|
||||
column: { id: "", name: "", type: "" },
|
||||
question: { id: "", name: "", type: "" },
|
||||
element: { id: "", name: "", type: "" },
|
||||
},
|
||||
],
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
const elements = useMemo(
|
||||
() => (selectedSurvey ? getElementsFromBlocks(selectedSurvey.blocks) : []),
|
||||
[selectedSurvey]
|
||||
);
|
||||
|
||||
const notionIntegrationData: TIntegrationInput = {
|
||||
type: "notion",
|
||||
config: {
|
||||
@@ -119,12 +178,12 @@ export const AddIntegrationModal = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedDatabase?.id]);
|
||||
|
||||
const questionItems = useMemo(() => {
|
||||
const questions = selectedSurvey
|
||||
? replaceHeadlineRecall(selectedSurvey, "default")?.questions.map((q) => ({
|
||||
id: q.id,
|
||||
name: getTextContent(getLocalizedValue(q.headline, "default")),
|
||||
type: q.type,
|
||||
const elementItems = useMemo(() => {
|
||||
const mappedElements = selectedSurvey
|
||||
? elements.map((el) => ({
|
||||
id: el.id,
|
||||
name: getTextContent(recallToHeadline(el.headline, selectedSurvey, false, "default")["default"]),
|
||||
type: el.type,
|
||||
}))
|
||||
: [];
|
||||
|
||||
@@ -132,31 +191,31 @@ export const AddIntegrationModal = ({
|
||||
selectedSurvey?.variables.map((variable) => ({
|
||||
id: variable.id,
|
||||
name: variable.name,
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
})) || [];
|
||||
|
||||
const hiddenFields =
|
||||
selectedSurvey?.hiddenFields.fieldIds?.map((fId) => ({
|
||||
id: fId,
|
||||
name: `${t("common.hidden_field")} : ${fId}`,
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
})) || [];
|
||||
const Metadata = [
|
||||
{
|
||||
id: "metadata",
|
||||
name: t("common.metadata"),
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
},
|
||||
];
|
||||
const createdAt = [
|
||||
{
|
||||
id: "createdAt",
|
||||
name: t("common.created_at"),
|
||||
type: TSurveyQuestionTypeEnum.Date,
|
||||
type: TSurveyElementTypeEnum.Date,
|
||||
},
|
||||
];
|
||||
|
||||
return [...questions, ...variables, ...hiddenFields, ...Metadata, ...createdAt];
|
||||
return [...mappedElements, ...variables, ...hiddenFields, ...Metadata, ...createdAt];
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedSurvey?.id]);
|
||||
|
||||
@@ -190,7 +249,7 @@ export const AddIntegrationModal = ({
|
||||
throw new Error(t("environments.integrations.please_select_a_survey_error"));
|
||||
}
|
||||
|
||||
if (mapping.length === 1 && (!mapping[0].question.id || !mapping[0].column.id)) {
|
||||
if (mapping.length === 1 && (!mapping[0].element.id || !mapping[0].column.id)) {
|
||||
throw new Error(t("environments.integrations.notion.please_select_at_least_one_mapping"));
|
||||
}
|
||||
|
||||
@@ -199,8 +258,8 @@ export const AddIntegrationModal = ({
|
||||
}
|
||||
|
||||
if (
|
||||
mapping.filter((m) => m.column.id && !m.question.id).length >= 1 ||
|
||||
mapping.filter((m) => m.question.id && !m.column.id).length >= 1
|
||||
mapping.filter((m) => m.column.id && !m.element.id).length >= 1 ||
|
||||
mapping.filter((m) => m.element.id && !m.column.id).length >= 1
|
||||
) {
|
||||
throw new Error(
|
||||
t("environments.integrations.notion.please_complete_mapping_fields_with_notion_property")
|
||||
@@ -261,23 +320,23 @@ export const AddIntegrationModal = ({
|
||||
setSelectedDatabase(null);
|
||||
setSelectedSurvey(null);
|
||||
};
|
||||
const getFilteredQuestionItems = (selectedIdx) => {
|
||||
const selectedQuestionIds = mapping.filter((_, idx) => idx !== selectedIdx).map((m) => m.question.id);
|
||||
const getFilteredElementItems = (selectedIdx) => {
|
||||
const selectedElementIds = mapping.filter((_, idx) => idx !== selectedIdx).map((m) => m.element.id);
|
||||
|
||||
return questionItems.filter((q) => !selectedQuestionIds.includes(q.id));
|
||||
return elementItems.filter((el) => !selectedElementIds.includes(el.id));
|
||||
};
|
||||
|
||||
const createCopy = (item) => structuredClone(item);
|
||||
|
||||
const MappingRow = ({ idx }: { idx: number }) => {
|
||||
const filteredQuestionItems = getFilteredQuestionItems(idx);
|
||||
const filteredElementItems = getFilteredElementItems(idx);
|
||||
|
||||
const addRow = () => {
|
||||
setMapping((prev) => [
|
||||
...prev,
|
||||
{
|
||||
column: { id: "", name: "", type: "" },
|
||||
question: { id: "", name: "", type: "" },
|
||||
element: { id: "", name: "", type: "" },
|
||||
},
|
||||
]);
|
||||
};
|
||||
@@ -288,49 +347,6 @@ export const AddIntegrationModal = ({
|
||||
});
|
||||
};
|
||||
|
||||
const ErrorMsg = ({ error, col, ques }) => {
|
||||
const showErrorMsg = useMemo(() => {
|
||||
switch (error?.type) {
|
||||
case ERRORS.UNSUPPORTED_TYPE:
|
||||
return (
|
||||
<>
|
||||
-{" "}
|
||||
{t("environments.integrations.notion.col_name_of_type_is_not_supported", {
|
||||
col_name: col.name,
|
||||
type: col.type,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
case ERRORS.MAPPING:
|
||||
const question = getQuestionTypes(t).find((qt) => qt.id === ques.type);
|
||||
if (!question) return null;
|
||||
return (
|
||||
<>
|
||||
{t("environments.integrations.notion.que_name_of_type_cant_be_mapped_to", {
|
||||
que_name: ques.name,
|
||||
question_label: question.label,
|
||||
col_name: col.name,
|
||||
col_type: col.type,
|
||||
mapped_type: TYPE_MAPPING[question.id].join(" ,"),
|
||||
})}
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [error]);
|
||||
|
||||
if (!error) return null;
|
||||
|
||||
return (
|
||||
<div className="my-4 w-full rounded-lg bg-red-100 p-4 text-sm text-red-800">
|
||||
<span className="mb-2 block">{error.type}</span>
|
||||
{showErrorMsg}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getFilteredDbItems = () => {
|
||||
const colMapping = mapping.map((m) => m.column.id);
|
||||
return dbItems.filter((item) => !colMapping.includes(item.id));
|
||||
@@ -338,19 +354,20 @@ export const AddIntegrationModal = ({
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<ErrorMsg
|
||||
<MappingErrorMessage
|
||||
key={idx}
|
||||
error={mapping[idx]?.error}
|
||||
col={mapping[idx].column}
|
||||
ques={mapping[idx].question}
|
||||
elem={mapping[idx].element}
|
||||
t={t}
|
||||
/>
|
||||
<div className="flex w-full items-center space-x-2">
|
||||
<div className="flex w-full items-center">
|
||||
<div className="max-w-full flex-1">
|
||||
<DropdownSelector
|
||||
placeholder={t("environments.integrations.notion.select_a_survey_question")}
|
||||
items={filteredQuestionItems}
|
||||
selectedItem={mapping?.[idx]?.question}
|
||||
items={filteredElementItems}
|
||||
selectedItem={mapping?.[idx]?.element}
|
||||
setSelectedItem={(item) => {
|
||||
setMapping((prev) => {
|
||||
const copy = createCopy(prev);
|
||||
@@ -362,7 +379,7 @@ export const AddIntegrationModal = ({
|
||||
error: {
|
||||
type: ERRORS.UNSUPPORTED_TYPE,
|
||||
},
|
||||
question: item,
|
||||
element: item,
|
||||
};
|
||||
return copy;
|
||||
}
|
||||
@@ -374,7 +391,7 @@ export const AddIntegrationModal = ({
|
||||
error: {
|
||||
type: ERRORS.MAPPING,
|
||||
},
|
||||
question: item,
|
||||
element: item,
|
||||
};
|
||||
return copy;
|
||||
}
|
||||
@@ -382,13 +399,13 @@ export const AddIntegrationModal = ({
|
||||
|
||||
copy[idx] = {
|
||||
...copy[idx],
|
||||
question: item,
|
||||
element: item,
|
||||
error: null,
|
||||
};
|
||||
return copy;
|
||||
});
|
||||
}}
|
||||
disabled={questionItems.length === 0}
|
||||
disabled={elementItems.length === 0}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-px w-4 border-t border-t-slate-300" />
|
||||
@@ -400,9 +417,9 @@ export const AddIntegrationModal = ({
|
||||
setSelectedItem={(item) => {
|
||||
setMapping((prev) => {
|
||||
const copy = createCopy(prev);
|
||||
const ques = copy[idx].question;
|
||||
if (ques.id) {
|
||||
const isValidQuesType = TYPE_MAPPING[ques.type].includes(item.type);
|
||||
const elem = copy[idx].element;
|
||||
if (elem.id) {
|
||||
const isValidElemType = TYPE_MAPPING[elem.type].includes(item.type);
|
||||
|
||||
if (UNSUPPORTED_TYPES_BY_NOTION.includes(item.type)) {
|
||||
copy[idx] = {
|
||||
@@ -415,7 +432,7 @@ export const AddIntegrationModal = ({
|
||||
return copy;
|
||||
}
|
||||
|
||||
if (!isValidQuesType) {
|
||||
if (!isValidElemType) {
|
||||
copy[idx] = {
|
||||
...copy[idx],
|
||||
error: {
|
||||
|
||||
+34
-25
@@ -13,12 +13,12 @@ import {
|
||||
TIntegrationSlackConfigData,
|
||||
TIntegrationSlackInput,
|
||||
} from "@formbricks/types/integration/slack";
|
||||
import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
|
||||
import SlackLogo from "@/images/slacklogo.png";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
||||
import { recallToHeadline } from "@/lib/utils/recall";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Checkbox } from "@/modules/ui/components/checkbox";
|
||||
@@ -55,7 +55,7 @@ export const AddChannelMappingModal = ({
|
||||
}: AddChannelMappingModalProps) => {
|
||||
const { handleSubmit } = useForm();
|
||||
const { t } = useTranslation();
|
||||
const [selectedQuestions, setSelectedQuestions] = useState<string[]>([]);
|
||||
const [selectedElements, setSelectedElements] = useState<string[]>([]);
|
||||
const [isLinkingChannel, setIsLinkingChannel] = useState(false);
|
||||
const [selectedSurvey, setSelectedSurvey] = useState<TSurvey | null>(null);
|
||||
const [selectedChannel, setSelectedChannel] = useState<TIntegrationItem | null>(null);
|
||||
@@ -73,14 +73,19 @@ export const AddChannelMappingModal = ({
|
||||
},
|
||||
};
|
||||
|
||||
const surveyElements = useMemo(
|
||||
() => (selectedSurvey ? getElementsFromBlocks(selectedSurvey.blocks) : []),
|
||||
[selectedSurvey]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSurvey) {
|
||||
const questionIds = selectedSurvey.questions.map((question) => question.id);
|
||||
const elementIds = surveyElements.map((element) => element.id);
|
||||
if (!selectedIntegration) {
|
||||
setSelectedQuestions(questionIds);
|
||||
setSelectedElements(elementIds);
|
||||
}
|
||||
}
|
||||
}, [selectedIntegration, selectedSurvey]);
|
||||
}, [surveyElements, selectedIntegration, selectedSurvey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedIntegration) {
|
||||
@@ -93,7 +98,7 @@ export const AddChannelMappingModal = ({
|
||||
return survey.id === selectedIntegration.surveyId;
|
||||
})!
|
||||
);
|
||||
setSelectedQuestions(selectedIntegration.questionIds);
|
||||
setSelectedElements(selectedIntegration.elementIds);
|
||||
setIncludeVariables(!!selectedIntegration.includeVariables);
|
||||
setIncludeHiddenFields(!!selectedIntegration.includeHiddenFields);
|
||||
setIncludeMetadata(!!selectedIntegration.includeMetadata);
|
||||
@@ -112,7 +117,7 @@ export const AddChannelMappingModal = ({
|
||||
throw new Error(t("environments.integrations.please_select_a_survey_error"));
|
||||
}
|
||||
|
||||
if (selectedQuestions.length === 0) {
|
||||
if (selectedElements.length === 0) {
|
||||
throw new Error(t("environments.integrations.select_at_least_one_question_error"));
|
||||
}
|
||||
setIsLinkingChannel(true);
|
||||
@@ -121,9 +126,9 @@ export const AddChannelMappingModal = ({
|
||||
channelName: selectedChannel.name,
|
||||
surveyId: selectedSurvey.id,
|
||||
surveyName: selectedSurvey.name,
|
||||
questionIds: selectedQuestions,
|
||||
questions:
|
||||
selectedQuestions.length === selectedSurvey?.questions.length
|
||||
elementIds: selectedElements,
|
||||
elements:
|
||||
selectedElements.length === surveyElements.length
|
||||
? t("common.all_questions")
|
||||
: t("common.selected_questions"),
|
||||
createdAt: new Date(),
|
||||
@@ -154,11 +159,11 @@ export const AddChannelMappingModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (questionId: TSurveyQuestionId) => {
|
||||
setSelectedQuestions((prevValues) =>
|
||||
prevValues.includes(questionId)
|
||||
? prevValues.filter((value) => value !== questionId)
|
||||
: [...prevValues, questionId]
|
||||
const handleCheckboxChange = (elementId: string) => {
|
||||
setSelectedElements((prevValues) =>
|
||||
prevValues.includes(elementId)
|
||||
? prevValues.filter((value) => value !== elementId)
|
||||
: [...prevValues, elementId]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -269,21 +274,25 @@ export const AddChannelMappingModal = ({
|
||||
<Label htmlFor="Surveys">{t("common.questions")}</Label>
|
||||
<div className="mt-1 max-h-[15vh] overflow-y-auto rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{replaceHeadlineRecall(selectedSurvey, "default")?.questions?.map((question) => (
|
||||
<div key={question.id} className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
{surveyElements.map((element) => (
|
||||
<div key={element.id} className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={element.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
type="button"
|
||||
id={question.id}
|
||||
value={question.id}
|
||||
id={element.id}
|
||||
value={element.id}
|
||||
className="bg-white"
|
||||
checked={selectedQuestions.includes(question.id)}
|
||||
checked={selectedElements.includes(element.id)}
|
||||
onCheckedChange={() => {
|
||||
handleCheckboxChange(question.id);
|
||||
handleCheckboxChange(element.id);
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">
|
||||
{getTextContent(getLocalizedValue(question.headline, "default"))}
|
||||
{getTextContent(
|
||||
recallToHeadline(element.headline, selectedSurvey, false, "default")[
|
||||
"default"
|
||||
]
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -126,7 +126,7 @@ export const ManageIntegration = ({
|
||||
}}>
|
||||
<div className="col-span-2 text-center">{data.surveyName}</div>
|
||||
<div className="col-span-2 text-center">{data.channelName}</div>
|
||||
<div className="col-span-2 text-center">{data.questions}</div>
|
||||
<div className="col-span-2 text-center">{data.elements}</div>
|
||||
<div className="col-span-2 text-center">{timeSince(data.createdAt.toString(), locale)}</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
+9
-9
@@ -2,14 +2,14 @@
|
||||
|
||||
import React, { createContext, useCallback, useContext, useState } from "react";
|
||||
import {
|
||||
QuestionOption,
|
||||
QuestionOptions,
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
|
||||
import { QuestionFilterOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter";
|
||||
ElementOption,
|
||||
ElementOptions,
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ElementsComboBox";
|
||||
import { ElementFilterOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter";
|
||||
import { getTodayDate } from "@/app/lib/surveys/surveys";
|
||||
|
||||
export interface FilterValue {
|
||||
questionType: Partial<QuestionOption>;
|
||||
elementType: Partial<ElementOption>;
|
||||
filterType: {
|
||||
filterValue: string | undefined;
|
||||
filterComboBoxValue: string | string[] | undefined;
|
||||
@@ -24,8 +24,8 @@ export interface SelectedFilterValue {
|
||||
}
|
||||
|
||||
interface SelectedFilterOptions {
|
||||
questionOptions: QuestionOptions[];
|
||||
questionFilterOptions: QuestionFilterOptions[];
|
||||
elementOptions: ElementOptions[];
|
||||
elementFilterOptions: ElementFilterOptions[];
|
||||
}
|
||||
|
||||
export interface DateRange {
|
||||
@@ -53,8 +53,8 @@ const ResponseFilterProvider = ({ children }: { children: React.ReactNode }) =>
|
||||
});
|
||||
// state holds all the options of the responses fetched
|
||||
const [selectedOptions, setSelectedOptions] = useState<SelectedFilterOptions>({
|
||||
questionFilterOptions: [],
|
||||
questionOptions: [],
|
||||
elementFilterOptions: [],
|
||||
elementOptions: [],
|
||||
});
|
||||
|
||||
const [dateRange, setDateRange] = useState<DateRange>({
|
||||
|
||||
+7
-4
@@ -10,6 +10,7 @@ import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { TUser, TUserLocale } from "@formbricks/types/user";
|
||||
import { ResponseTable } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTable";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
|
||||
interface ResponseDataViewProps {
|
||||
survey: TSurvey;
|
||||
@@ -55,9 +56,11 @@ export const formatContactInfoData = (responseValue: TResponseDataValue): Record
|
||||
export const extractResponseData = (response: TResponseWithQuotas, survey: TSurvey): Record<string, any> => {
|
||||
const responseData: Record<string, any> = {};
|
||||
|
||||
for (const question of survey.questions) {
|
||||
const responseValue = response.data[question.id];
|
||||
switch (question.type) {
|
||||
const elements = getElementsFromBlocks(survey.blocks);
|
||||
|
||||
for (const element of elements) {
|
||||
const responseValue = response.data[element.id];
|
||||
switch (element.type) {
|
||||
case "matrix":
|
||||
if (typeof responseValue === "object") {
|
||||
Object.assign(responseData, responseValue);
|
||||
@@ -70,7 +73,7 @@ export const extractResponseData = (response: TResponseWithQuotas, survey: TSurv
|
||||
Object.assign(responseData, formatContactInfoData(responseValue));
|
||||
break;
|
||||
default:
|
||||
responseData[question.id] = responseValue;
|
||||
responseData[element.id] = responseValue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+40
-44
@@ -5,7 +5,8 @@ import { TFunction } from "i18next";
|
||||
import { CircleHelpIcon, EyeOffIcon, MailIcon, TagIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { TResponseTableData } from "@formbricks/types/responses";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { extractChoiceIdsFromResponse } from "@/lib/response/utils";
|
||||
@@ -13,7 +14,8 @@ import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { getFormattedDateTimeString } from "@/lib/utils/datetime";
|
||||
import { recallToHeadline } from "@/lib/utils/recall";
|
||||
import { RenderResponse } from "@/modules/analysis/components/SingleResponseCard/components/RenderResponse";
|
||||
import { VARIABLES_ICON_MAP, getQuestionIconMap } from "@/modules/survey/lib/questions";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { VARIABLES_ICON_MAP, getElementIconMap } from "@/modules/survey/lib/elements";
|
||||
import { getSelectionColumn } from "@/modules/ui/components/data-table";
|
||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
import { ResponseBadges } from "@/modules/ui/components/response-badges";
|
||||
@@ -28,35 +30,33 @@ import {
|
||||
getMetadataValue,
|
||||
} from "../lib/utils";
|
||||
|
||||
const getQuestionColumnsData = (
|
||||
question: TSurveyQuestion,
|
||||
const getElementColumnsData = (
|
||||
element: TSurveyElement,
|
||||
survey: TSurvey,
|
||||
isExpanded: boolean,
|
||||
t: TFunction
|
||||
): ColumnDef<TResponseTableData>[] => {
|
||||
const QUESTIONS_ICON_MAP = getQuestionIconMap(t);
|
||||
const ELEMENTS_ICON_MAP = getElementIconMap(t);
|
||||
const addressFields = ["addressLine1", "addressLine2", "city", "state", "zip", "country"];
|
||||
const contactInfoFields = ["firstName", "lastName", "email", "phone", "company"];
|
||||
|
||||
// Helper function to create consistent column headers
|
||||
const createQuestionHeader = (questionType: string, headline: string, suffix?: string) => {
|
||||
const createElementHeader = (elementType: string, headline: string, suffix?: string) => {
|
||||
const title = suffix ? `${headline} - ${suffix}` : headline;
|
||||
const QuestionHeader = () => (
|
||||
const ElementHeader = () => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2 overflow-hidden">
|
||||
<span className="h-4 w-4">{QUESTIONS_ICON_MAP[questionType]}</span>
|
||||
<span className="h-4 w-4">{ELEMENTS_ICON_MAP[elementType]}</span>
|
||||
<span className="truncate">{title}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
QuestionHeader.displayName = "QuestionHeader";
|
||||
return QuestionHeader;
|
||||
return ElementHeader;
|
||||
};
|
||||
|
||||
// Helper function to get localized question headline
|
||||
const getQuestionHeadline = (question: TSurveyQuestion, survey: TSurvey) => {
|
||||
const getElementHeadline = (element: TSurveyElement, survey: TSurvey) => {
|
||||
return getTextContent(
|
||||
getLocalizedValue(recallToHeadline(question.headline, survey, false, "default"), "default")
|
||||
getLocalizedValue(recallToHeadline(element.headline, survey, false, "default"), "default")
|
||||
);
|
||||
};
|
||||
|
||||
@@ -75,18 +75,18 @@ const getQuestionColumnsData = (
|
||||
);
|
||||
};
|
||||
|
||||
switch (question.type) {
|
||||
switch (element.type) {
|
||||
case "matrix":
|
||||
return question.rows.map((matrixRow) => {
|
||||
return element.rows.map((matrixRow) => {
|
||||
return {
|
||||
accessorKey: "QUESTION_" + question.id + "_" + matrixRow.label.default,
|
||||
accessorKey: "ELEMENT_" + element.id + "_" + matrixRow.label.default,
|
||||
header: () => {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2 overflow-hidden">
|
||||
<span className="h-4 w-4">{QUESTIONS_ICON_MAP["matrix"]}</span>
|
||||
<span className="h-4 w-4">{ELEMENTS_ICON_MAP["matrix"]}</span>
|
||||
<span className="truncate">
|
||||
{getTextContent(getLocalizedValue(question.headline, "default")) +
|
||||
{getTextContent(getLocalizedValue(element.headline, "default")) +
|
||||
" - " +
|
||||
getLocalizedValue(matrixRow.label, "default")}
|
||||
</span>
|
||||
@@ -106,12 +106,12 @@ const getQuestionColumnsData = (
|
||||
case "address":
|
||||
return addressFields.map((addressField) => {
|
||||
return {
|
||||
accessorKey: "QUESTION_" + question.id + "_" + addressField,
|
||||
accessorKey: "ELEMENT_" + element.id + "_" + addressField,
|
||||
header: () => {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2 overflow-hidden">
|
||||
<span className="h-4 w-4">{QUESTIONS_ICON_MAP["address"]}</span>
|
||||
<span className="h-4 w-4">{ELEMENTS_ICON_MAP["address"]}</span>
|
||||
<span className="truncate">{getAddressFieldLabel(addressField, t)}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -129,12 +129,12 @@ const getQuestionColumnsData = (
|
||||
case "contactInfo":
|
||||
return contactInfoFields.map((contactInfoField) => {
|
||||
return {
|
||||
accessorKey: "QUESTION_" + question.id + "_" + contactInfoField,
|
||||
accessorKey: "ELEMENT_" + element.id + "_" + contactInfoField,
|
||||
header: () => {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2 overflow-hidden">
|
||||
<span className="h-4 w-4">{QUESTIONS_ICON_MAP["contactInfo"]}</span>
|
||||
<span className="h-4 w-4">{ELEMENTS_ICON_MAP["contactInfo"]}</span>
|
||||
<span className="truncate">{getContactInfoFieldLabel(contactInfoField, t)}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -153,17 +153,17 @@ const getQuestionColumnsData = (
|
||||
case "multipleChoiceSingle":
|
||||
case "ranking":
|
||||
case "pictureSelection": {
|
||||
const questionHeadline = getQuestionHeadline(question, survey);
|
||||
const elementHeadline = getElementHeadline(element, survey);
|
||||
return [
|
||||
{
|
||||
accessorKey: "QUESTION_" + question.id,
|
||||
header: createQuestionHeader(question.type, questionHeadline),
|
||||
accessorKey: "ELEMENT_" + element.id,
|
||||
header: createElementHeader(element.type, elementHeadline),
|
||||
cell: ({ row }) => {
|
||||
const responseValue = row.original.responseData[question.id];
|
||||
const responseValue = row.original.responseData[element.id];
|
||||
const language = row.original.language;
|
||||
return (
|
||||
<RenderResponse
|
||||
question={question}
|
||||
element={element}
|
||||
survey={survey}
|
||||
responseData={responseValue}
|
||||
language={language}
|
||||
@@ -174,15 +174,15 @@ const getQuestionColumnsData = (
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "QUESTION_" + question.id + "optionIds",
|
||||
header: createQuestionHeader(question.type, questionHeadline, t("common.option_id")),
|
||||
accessorKey: "ELEMENT_" + element.id + "optionIds",
|
||||
header: createElementHeader(element.type, elementHeadline, t("common.option_id")),
|
||||
cell: ({ row }) => {
|
||||
const responseValue = row.original.responseData[question.id];
|
||||
const responseValue = row.original.responseData[element.id];
|
||||
// Type guard to ensure responseValue is the correct type
|
||||
if (typeof responseValue === "string" || Array.isArray(responseValue)) {
|
||||
const choiceIds = extractChoiceIdsFromResponse(
|
||||
responseValue,
|
||||
question,
|
||||
element,
|
||||
row.original.language || undefined
|
||||
);
|
||||
return renderChoiceIdBadges(choiceIds, isExpanded);
|
||||
@@ -196,28 +196,25 @@ const getQuestionColumnsData = (
|
||||
default:
|
||||
return [
|
||||
{
|
||||
accessorKey: "QUESTION_" + question.id,
|
||||
accessorKey: "ELEMENT_" + element.id,
|
||||
header: () => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2 overflow-hidden">
|
||||
<span className="h-4 w-4">{QUESTIONS_ICON_MAP[question.type]}</span>
|
||||
<span className="h-4 w-4">{ELEMENTS_ICON_MAP[element.type]}</span>
|
||||
<span className="truncate">
|
||||
{getTextContent(
|
||||
getLocalizedValue(
|
||||
recallToHeadline(question.headline, survey, false, "default"),
|
||||
"default"
|
||||
)
|
||||
getLocalizedValue(recallToHeadline(element.headline, survey, false, "default"), "default")
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const responseValue = row.original.responseData[question.id];
|
||||
const responseValue = row.original.responseData[element.id];
|
||||
const language = row.original.language;
|
||||
return (
|
||||
<RenderResponse
|
||||
question={question}
|
||||
element={element}
|
||||
survey={survey}
|
||||
responseData={responseValue}
|
||||
language={language}
|
||||
@@ -265,9 +262,8 @@ export const generateResponseTableColumns = (
|
||||
t: TFunction,
|
||||
showQuotasColumn: boolean
|
||||
): ColumnDef<TResponseTableData>[] => {
|
||||
const questionColumns = survey.questions.flatMap((question) =>
|
||||
getQuestionColumnsData(question, survey, isExpanded, t)
|
||||
);
|
||||
const elements = getElementsFromBlocks(survey.blocks);
|
||||
const elementColumns = elements.flatMap((element) => getElementColumnsData(element, survey, isExpanded, t));
|
||||
|
||||
const dateColumn: ColumnDef<TResponseTableData> = {
|
||||
accessorKey: "createdAt",
|
||||
@@ -414,7 +410,7 @@ export const generateResponseTableColumns = (
|
||||
),
|
||||
};
|
||||
|
||||
// Combine the selection column with the dynamic question columns
|
||||
// Combine the selection column with the dynamic element columns
|
||||
const baseColumns = [
|
||||
personColumn,
|
||||
singleUseIdColumn,
|
||||
@@ -422,7 +418,7 @@ export const generateResponseTableColumns = (
|
||||
...(showQuotasColumn ? [quotasColumn] : []),
|
||||
statusColumn,
|
||||
...(survey.isVerifyEmailEnabled ? [verifiedEmailColumn] : []),
|
||||
...questionColumns,
|
||||
...elementColumns,
|
||||
...variableColumns,
|
||||
...hiddenFieldColumns,
|
||||
...metadataColumns,
|
||||
|
||||
+7
-7
@@ -2,27 +2,27 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyQuestionSummaryAddress } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyElementSummaryAddress } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { ArrayResponse } from "@/modules/ui/components/array-response";
|
||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
|
||||
interface AddressSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryAddress;
|
||||
elementSummary: TSurveyElementSummaryAddress;
|
||||
environmentId: string;
|
||||
survey: TSurvey;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const AddressSummary = ({ questionSummary, environmentId, survey, locale }: AddressSummaryProps) => {
|
||||
export const AddressSummary = ({ elementSummary, environmentId, survey, locale }: AddressSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
|
||||
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} />
|
||||
<div>
|
||||
<div className="grid h-10 grid-cols-4 items-center border-y border-slate-200 bg-slate-100 text-sm font-bold text-slate-600">
|
||||
<div className="pl-4 md:pl-6">{t("common.user")}</div>
|
||||
@@ -30,12 +30,12 @@ export const AddressSummary = ({ questionSummary, environmentId, survey, locale
|
||||
<div className="px-4 md:px-6">{t("common.time")}</div>
|
||||
</div>
|
||||
<div className="max-h-[62vh] w-full overflow-y-auto">
|
||||
{questionSummary.samples.length === 0 ? (
|
||||
{elementSummary.samples.length === 0 ? (
|
||||
<div className="p-8">
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} variant="simple" />
|
||||
</div>
|
||||
) : (
|
||||
questionSummary.samples.map((response) => {
|
||||
elementSummary.samples.map((response) => {
|
||||
return (
|
||||
<div
|
||||
key={response.id}
|
||||
|
||||
+14
-14
@@ -2,39 +2,39 @@
|
||||
|
||||
import { InboxIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyQuestionSummaryCta } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyElementSummaryCta } from "@formbricks/types/surveys/types";
|
||||
import { ProgressBar } from "@/modules/ui/components/progress-bar";
|
||||
import { convertFloatToNDecimal } from "../lib/utils";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
|
||||
interface CTASummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryCta;
|
||||
elementSummary: TSurveyElementSummaryCta;
|
||||
survey: TSurvey;
|
||||
}
|
||||
|
||||
export const CTASummary = ({ questionSummary, survey }: CTASummaryProps) => {
|
||||
export const CTASummary = ({ elementSummary, survey }: CTASummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader
|
||||
<ElementSummaryHeader
|
||||
survey={survey}
|
||||
questionSummary={questionSummary}
|
||||
elementSummary={elementSummary}
|
||||
showResponses={false}
|
||||
additionalInfo={
|
||||
<>
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxIcon className="mr-2 h-4 w-4" />
|
||||
{`${questionSummary.impressionCount} ${t("common.impressions")}`}
|
||||
{`${elementSummary.impressionCount} ${t("common.impressions")}`}
|
||||
</div>
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxIcon className="mr-2 h-4 w-4" />
|
||||
{`${questionSummary.clickCount} ${t("common.clicks")}`}
|
||||
{`${elementSummary.clickCount} ${t("common.clicks")}`}
|
||||
</div>
|
||||
{!questionSummary.question.required && (
|
||||
{!elementSummary.element.required && (
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxIcon className="mr-2 h-4 w-4" />
|
||||
{`${questionSummary.skipCount} ${t("common.skips")}`}
|
||||
{`${elementSummary.skipCount} ${t("common.skips")}`}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -46,16 +46,16 @@ export const CTASummary = ({ questionSummary, survey }: CTASummaryProps) => {
|
||||
<p className="font-semibold text-slate-700">CTR</p>
|
||||
<div>
|
||||
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(questionSummary.ctr.percentage, 2)}%
|
||||
{convertFloatToNDecimal(elementSummary.ctr.percentage, 2)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="flex w-32 items-end justify-end text-slate-600">
|
||||
{questionSummary.ctr.count}{" "}
|
||||
{questionSummary.ctr.count === 1 ? t("common.click") : t("common.clicks")}
|
||||
{elementSummary.ctr.count}{" "}
|
||||
{elementSummary.ctr.count === 1 ? t("common.click") : t("common.clicks")}
|
||||
</p>
|
||||
</div>
|
||||
<ProgressBar barColor="bg-brand-dark" progress={questionSummary.ctr.percentage / 100} />
|
||||
<ProgressBar barColor="bg-brand-dark" progress={elementSummary.ctr.percentage / 100} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+13
-13
@@ -1,23 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyQuestionSummaryCal } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyElementSummaryCal } from "@formbricks/types/surveys/types";
|
||||
import { convertFloatToNDecimal } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
|
||||
import { ProgressBar } from "@/modules/ui/components/progress-bar";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
|
||||
interface CalSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryCal;
|
||||
elementSummary: TSurveyElementSummaryCal;
|
||||
environmentId: string;
|
||||
survey: TSurvey;
|
||||
}
|
||||
|
||||
export const CalSummary = ({ questionSummary, survey }: CalSummaryProps) => {
|
||||
export const CalSummary = ({ elementSummary, survey }: CalSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
|
||||
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} />
|
||||
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
|
||||
<div>
|
||||
<div className="text flex justify-between px-2 pb-2">
|
||||
@@ -25,16 +25,16 @@ export const CalSummary = ({ questionSummary, survey }: CalSummaryProps) => {
|
||||
<p className="font-semibold text-slate-700">{t("common.booked")}</p>
|
||||
<div>
|
||||
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(questionSummary.booked.percentage, 2)}%
|
||||
{convertFloatToNDecimal(elementSummary.booked.percentage, 2)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="flex w-32 items-end justify-end text-slate-600">
|
||||
{questionSummary.booked.count}{" "}
|
||||
{questionSummary.booked.count === 1 ? t("common.response") : t("common.responses")}
|
||||
{elementSummary.booked.count}{" "}
|
||||
{elementSummary.booked.count === 1 ? t("common.response") : t("common.responses")}
|
||||
</p>
|
||||
</div>
|
||||
<ProgressBar barColor="bg-brand-dark" progress={questionSummary.booked.percentage / 100} />
|
||||
<ProgressBar barColor="bg-brand-dark" progress={elementSummary.booked.percentage / 100} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text flex justify-between px-2 pb-2">
|
||||
@@ -42,16 +42,16 @@ export const CalSummary = ({ questionSummary, survey }: CalSummaryProps) => {
|
||||
<p className="font-semibold text-slate-700">{t("common.dismissed")}</p>
|
||||
<div>
|
||||
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(questionSummary.skipped.percentage, 2)}%
|
||||
{convertFloatToNDecimal(elementSummary.skipped.percentage, 2)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="flex w-32 items-end justify-end text-slate-600">
|
||||
{questionSummary.skipped.count}{" "}
|
||||
{questionSummary.skipped.count === 1 ? t("common.response") : t("common.responses")}
|
||||
{elementSummary.skipped.count}{" "}
|
||||
{elementSummary.skipped.count === 1 ? t("common.response") : t("common.responses")}
|
||||
</p>
|
||||
</div>
|
||||
<ProgressBar barColor="bg-brand-dark" progress={questionSummary.skipped.percentage / 100} />
|
||||
<ProgressBar barColor="bg-brand-dark" progress={elementSummary.skipped.percentage / 100} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+16
-20
@@ -1,46 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
TSurveyQuestionId,
|
||||
TSurveyQuestionSummaryConsent,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { type TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey, TSurveyElementSummaryConsent } from "@formbricks/types/surveys/types";
|
||||
import { ProgressBar } from "@/modules/ui/components/progress-bar";
|
||||
import { convertFloatToNDecimal } from "../lib/utils";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
|
||||
interface ConsentSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryConsent;
|
||||
elementSummary: TSurveyElementSummaryConsent;
|
||||
survey: TSurvey;
|
||||
setFilter: (
|
||||
questionId: TSurveyQuestionId,
|
||||
elementId: string,
|
||||
label: TI18nString,
|
||||
questionType: TSurveyQuestionTypeEnum,
|
||||
elementType: TSurveyElementTypeEnum,
|
||||
filterValue: string,
|
||||
filterComboBoxValue?: string | string[]
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const ConsentSummary = ({ questionSummary, survey, setFilter }: ConsentSummaryProps) => {
|
||||
export const ConsentSummary = ({ elementSummary, survey, setFilter }: ConsentSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
const summaryItems = [
|
||||
{
|
||||
title: t("common.accepted"),
|
||||
percentage: questionSummary.accepted.percentage,
|
||||
count: questionSummary.accepted.count,
|
||||
percentage: elementSummary.accepted.percentage,
|
||||
count: elementSummary.accepted.count,
|
||||
},
|
||||
{
|
||||
title: t("common.dismissed"),
|
||||
percentage: questionSummary.dismissed.percentage,
|
||||
count: questionSummary.dismissed.count,
|
||||
percentage: elementSummary.dismissed.percentage,
|
||||
count: elementSummary.dismissed.count,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
|
||||
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} />
|
||||
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
|
||||
{summaryItems.map((summaryItem) => {
|
||||
return (
|
||||
@@ -49,9 +45,9 @@ export const ConsentSummary = ({ questionSummary, survey, setFilter }: ConsentSu
|
||||
key={summaryItem.title}
|
||||
onClick={() =>
|
||||
setFilter(
|
||||
questionSummary.question.id,
|
||||
questionSummary.question.headline,
|
||||
questionSummary.question.type,
|
||||
elementSummary.element.id,
|
||||
elementSummary.element.headline,
|
||||
elementSummary.element.type,
|
||||
"is",
|
||||
summaryItem.title
|
||||
)
|
||||
|
||||
+7
-7
@@ -2,24 +2,24 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyQuestionSummaryContactInfo } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyElementSummaryContactInfo } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { ArrayResponse } from "@/modules/ui/components/array-response";
|
||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
|
||||
interface ContactInfoSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryContactInfo;
|
||||
elementSummary: TSurveyElementSummaryContactInfo;
|
||||
environmentId: string;
|
||||
survey: TSurvey;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const ContactInfoSummary = ({
|
||||
questionSummary,
|
||||
elementSummary,
|
||||
environmentId,
|
||||
survey,
|
||||
locale,
|
||||
@@ -27,7 +27,7 @@ export const ContactInfoSummary = ({
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
|
||||
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} />
|
||||
<div>
|
||||
<div className="grid h-10 grid-cols-4 items-center border-y border-slate-200 bg-slate-100 text-sm font-bold text-slate-600">
|
||||
<div className="pl-4 md:pl-6">{t("common.user")}</div>
|
||||
@@ -35,12 +35,12 @@ export const ContactInfoSummary = ({
|
||||
<div className="px-4 md:px-6">{t("common.time")}</div>
|
||||
</div>
|
||||
<div className="max-h-[62vh] w-full overflow-y-auto">
|
||||
{questionSummary.samples.length === 0 ? (
|
||||
{elementSummary.samples.length === 0 ? (
|
||||
<div className="p-8">
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} variant="simple" />
|
||||
</div>
|
||||
) : (
|
||||
questionSummary.samples.map((response) => {
|
||||
elementSummary.samples.map((response) => {
|
||||
return (
|
||||
<div
|
||||
key={response.id}
|
||||
|
||||
+10
-15
@@ -3,7 +3,7 @@
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyQuestionSummaryDate } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyElementSummaryDate } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
@@ -11,28 +11,23 @@ import { formatDateWithOrdinal } from "@/lib/utils/datetime";
|
||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
|
||||
interface DateQuestionSummary {
|
||||
questionSummary: TSurveyQuestionSummaryDate;
|
||||
interface DateElementSummary {
|
||||
elementSummary: TSurveyElementSummaryDate;
|
||||
environmentId: string;
|
||||
survey: TSurvey;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const DateQuestionSummary = ({
|
||||
questionSummary,
|
||||
environmentId,
|
||||
survey,
|
||||
locale,
|
||||
}: DateQuestionSummary) => {
|
||||
export const DateElementSummary = ({ elementSummary, environmentId, survey, locale }: DateElementSummary) => {
|
||||
const { t } = useTranslation();
|
||||
const [visibleResponses, setVisibleResponses] = useState(10);
|
||||
|
||||
const handleLoadMore = () => {
|
||||
// Increase the number of visible responses by 10, not exceeding the total number of responses
|
||||
setVisibleResponses((prevVisibleResponses) =>
|
||||
Math.min(prevVisibleResponses + 10, questionSummary.samples.length)
|
||||
Math.min(prevVisibleResponses + 10, elementSummary.samples.length)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -48,7 +43,7 @@ export const DateQuestionSummary = ({
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
|
||||
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} />
|
||||
<div className="">
|
||||
<div className="grid h-10 grid-cols-4 items-center border-y border-slate-200 bg-slate-100 text-sm font-bold text-slate-600">
|
||||
<div className="pl-4 md:pl-6">{t("common.user")}</div>
|
||||
@@ -56,12 +51,12 @@ export const DateQuestionSummary = ({
|
||||
<div className="px-4 md:px-6">{t("common.time")}</div>
|
||||
</div>
|
||||
<div className="max-h-[62vh] w-full overflow-y-auto">
|
||||
{questionSummary.samples.length === 0 ? (
|
||||
{elementSummary.samples.length === 0 ? (
|
||||
<div className="p-8">
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} variant="simple" />
|
||||
</div>
|
||||
) : (
|
||||
questionSummary.samples.slice(0, visibleResponses).map((response) => (
|
||||
elementSummary.samples.slice(0, visibleResponses).map((response) => (
|
||||
<div
|
||||
key={response.id}
|
||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 last:border-transparent md:text-base">
|
||||
@@ -96,7 +91,7 @@ export const DateQuestionSummary = ({
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{questionSummary.samples.length > 0 && visibleResponses < questionSummary.samples.length && (
|
||||
{elementSummary.samples.length > 0 && visibleResponses < elementSummary.samples.length && (
|
||||
<div className="flex justify-center py-4">
|
||||
<Button onClick={handleLoadMore} variant="secondary" size="sm">
|
||||
{t("common.load_more")}
|
||||
+12
-12
@@ -3,28 +3,28 @@
|
||||
import { InboxIcon } from "lucide-react";
|
||||
import type { JSX } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyQuestionSummary } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyElementSummary } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { recallToHeadline } from "@/lib/utils/recall";
|
||||
import { formatTextWithSlashes } from "@/modules/survey/editor/lib/utils";
|
||||
import { getQuestionTypes } from "@/modules/survey/lib/questions";
|
||||
import { getElementTypes } from "@/modules/survey/lib/elements";
|
||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
|
||||
interface HeadProps {
|
||||
questionSummary: TSurveyQuestionSummary;
|
||||
elementSummary: TSurveyElementSummary;
|
||||
showResponses?: boolean;
|
||||
additionalInfo?: JSX.Element;
|
||||
survey: TSurvey;
|
||||
}
|
||||
|
||||
export const QuestionSummaryHeader = ({
|
||||
questionSummary,
|
||||
export const ElementSummaryHeader = ({
|
||||
elementSummary,
|
||||
additionalInfo,
|
||||
showResponses = true,
|
||||
survey,
|
||||
}: HeadProps) => {
|
||||
const { t } = useTranslation();
|
||||
const questionType = getQuestionTypes(t).find((type) => type.id === questionSummary.question.type);
|
||||
const elementType = getElementTypes(t).find((type) => type.id === elementSummary.element.type);
|
||||
|
||||
return (
|
||||
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
|
||||
@@ -32,7 +32,7 @@ export const QuestionSummaryHeader = ({
|
||||
<h3 className="pb-1 text-lg font-semibold text-slate-900 md:text-xl">
|
||||
{formatTextWithSlashes(
|
||||
getTextContent(
|
||||
recallToHeadline(questionSummary.question.headline, survey, true, "default")["default"]
|
||||
recallToHeadline(elementSummary.element.headline, survey, true, "default")["default"]
|
||||
),
|
||||
"@",
|
||||
["text-lg"]
|
||||
@@ -41,23 +41,23 @@ export const QuestionSummaryHeader = ({
|
||||
</div>
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
{questionType && <questionType.icon className="mr-2 h-4 w-4" />}
|
||||
{questionType ? questionType.label : t("environments.surveys.summary.unknown_question_type")}{" "}
|
||||
{elementType && <elementType.icon className="mr-2 h-4 w-4" />}
|
||||
{elementType ? elementType.label : t("environments.surveys.summary.unknown_question_type")}{" "}
|
||||
{t("common.question")}
|
||||
</div>
|
||||
{showResponses && (
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxIcon className="mr-2 h-4 w-4" />
|
||||
{`${questionSummary.responseCount} ${t("common.responses")}`}
|
||||
{`${elementSummary.responseCount} ${t("common.responses")}`}
|
||||
</div>
|
||||
)}
|
||||
{additionalInfo}
|
||||
{!questionSummary.question.required && (
|
||||
{!elementSummary.element.required && (
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
{t("environments.surveys.edit.optional")}
|
||||
</div>
|
||||
)}
|
||||
<IdBadge id={questionSummary.question.id} />
|
||||
<IdBadge id={elementSummary.element.id} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
+9
-9
@@ -4,7 +4,7 @@ import { DownloadIcon, FileIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyQuestionSummaryFileUpload } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyElementSummaryFileUpload } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
@@ -12,17 +12,17 @@ import { getOriginalFileNameFromUrl } from "@/modules/storage/utils";
|
||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
|
||||
interface FileUploadSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryFileUpload;
|
||||
elementSummary: TSurveyElementSummaryFileUpload;
|
||||
environmentId: string;
|
||||
survey: TSurvey;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const FileUploadSummary = ({
|
||||
questionSummary,
|
||||
elementSummary,
|
||||
environmentId,
|
||||
survey,
|
||||
locale,
|
||||
@@ -32,13 +32,13 @@ export const FileUploadSummary = ({
|
||||
const handleLoadMore = () => {
|
||||
// Increase the number of visible responses by 10, not exceeding the total number of responses
|
||||
setVisibleResponses((prevVisibleResponses) =>
|
||||
Math.min(prevVisibleResponses + 10, questionSummary.files.length)
|
||||
Math.min(prevVisibleResponses + 10, elementSummary.files.length)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
|
||||
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} />
|
||||
<div className="">
|
||||
<div className="grid h-10 grid-cols-4 items-center border-y border-slate-200 bg-slate-100 text-sm font-bold text-slate-600">
|
||||
<div className="pl-4 md:pl-6">{t("common.user")}</div>
|
||||
@@ -46,12 +46,12 @@ export const FileUploadSummary = ({
|
||||
<div className="px-4 md:px-6">{t("common.time")}</div>
|
||||
</div>
|
||||
<div className="max-h-[62vh] w-full overflow-y-auto">
|
||||
{questionSummary.files.length === 0 ? (
|
||||
{elementSummary.files.length === 0 ? (
|
||||
<div className="p-8">
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} variant="simple" />
|
||||
</div>
|
||||
) : (
|
||||
questionSummary.files.slice(0, visibleResponses).map((response) => (
|
||||
elementSummary.files.slice(0, visibleResponses).map((response) => (
|
||||
<div
|
||||
key={response.id}
|
||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 last:border-transparent md:text-base">
|
||||
@@ -116,7 +116,7 @@ export const FileUploadSummary = ({
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{questionSummary.files.length > 0 && visibleResponses < questionSummary.files.length && (
|
||||
{elementSummary.files.length > 0 && visibleResponses < elementSummary.files.length && (
|
||||
<div className="flex justify-center py-4">
|
||||
<Button onClick={handleLoadMore} variant="secondary" size="sm">
|
||||
{t("common.load_more")}
|
||||
|
||||
+10
-10
@@ -4,7 +4,7 @@ import { InboxIcon, Link, MessageSquareTextIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurveyQuestionSummaryHiddenFields } from "@formbricks/types/surveys/types";
|
||||
import { TSurveyElementSummaryHiddenFields } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
@@ -14,24 +14,24 @@ import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
|
||||
interface HiddenFieldsSummaryProps {
|
||||
environment: TEnvironment;
|
||||
questionSummary: TSurveyQuestionSummaryHiddenFields;
|
||||
elementSummary: TSurveyElementSummaryHiddenFields;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const HiddenFieldsSummary = ({ environment, questionSummary, locale }: HiddenFieldsSummaryProps) => {
|
||||
export const HiddenFieldsSummary = ({ environment, elementSummary, locale }: HiddenFieldsSummaryProps) => {
|
||||
const [visibleResponses, setVisibleResponses] = useState(10);
|
||||
const { t } = useTranslation();
|
||||
const handleLoadMore = () => {
|
||||
// Increase the number of visible responses by 10, not exceeding the total number of responses
|
||||
setVisibleResponses((prevVisibleResponses) =>
|
||||
Math.min(prevVisibleResponses + 10, questionSummary.samples.length)
|
||||
Math.min(prevVisibleResponses + 10, elementSummary.samples.length)
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
|
||||
<div className={"align-center flex justify-between gap-4"}>
|
||||
<h3 className="pb-1 text-lg font-semibold text-slate-900 md:text-xl">{questionSummary.id}</h3>
|
||||
<h3 className="pb-1 text-lg font-semibold text-slate-900 md:text-xl">{elementSummary.id}</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
@@ -41,8 +41,8 @@ export const HiddenFieldsSummary = ({ environment, questionSummary, locale }: Hi
|
||||
</div>
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxIcon className="mr-2 h-4 w-4" />
|
||||
{questionSummary.responseCount}{" "}
|
||||
{questionSummary.responseCount === 1 ? t("common.response") : t("common.responses")}
|
||||
{elementSummary.responseCount}{" "}
|
||||
{elementSummary.responseCount === 1 ? t("common.response") : t("common.responses")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -52,12 +52,12 @@ export const HiddenFieldsSummary = ({ environment, questionSummary, locale }: Hi
|
||||
<div className="col-span-2 pl-4 md:pl-6">{t("common.response")}</div>
|
||||
<div className="px-4 md:px-6">{t("common.time")}</div>
|
||||
</div>
|
||||
{questionSummary.samples.length === 0 ? (
|
||||
{elementSummary.samples.length === 0 ? (
|
||||
<div className="p-8">
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} variant="simple" />
|
||||
</div>
|
||||
) : (
|
||||
questionSummary.samples.slice(0, visibleResponses).map((response, idx) => (
|
||||
elementSummary.samples.slice(0, visibleResponses).map((response, idx) => (
|
||||
<div
|
||||
key={`${response.value}-${idx}`}
|
||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 md:text-base">
|
||||
@@ -91,7 +91,7 @@ export const HiddenFieldsSummary = ({ environment, questionSummary, locale }: Hi
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
{questionSummary.samples.length > 0 && visibleResponses < questionSummary.samples.length && (
|
||||
{elementSummary.samples.length > 0 && visibleResponses < elementSummary.samples.length && (
|
||||
<div className="flex justify-center py-4">
|
||||
<Button onClick={handleLoadMore} variant="secondary" size="sm">
|
||||
{t("common.load_more")}
|
||||
|
||||
+16
-22
@@ -1,29 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
TSurveyQuestionId,
|
||||
TSurveyQuestionSummaryMatrix,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { type TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey, TSurveyElementSummaryMatrix } from "@formbricks/types/surveys/types";
|
||||
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
|
||||
interface MatrixQuestionSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryMatrix;
|
||||
interface MatrixElementSummaryProps {
|
||||
elementSummary: TSurveyElementSummaryMatrix;
|
||||
survey: TSurvey;
|
||||
setFilter: (
|
||||
questionId: TSurveyQuestionId,
|
||||
elementId: string,
|
||||
label: TI18nString,
|
||||
questionType: TSurveyQuestionTypeEnum,
|
||||
elementType: TSurveyElementTypeEnum,
|
||||
filterValue: string,
|
||||
filterComboBoxValue?: string | string[]
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const MatrixQuestionSummary = ({ questionSummary, survey, setFilter }: MatrixQuestionSummaryProps) => {
|
||||
export const MatrixElementSummary = ({ elementSummary, survey, setFilter }: MatrixElementSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
const getOpacityLevel = (percentage: number): string => {
|
||||
const parsedPercentage = percentage;
|
||||
@@ -40,13 +36,11 @@ export const MatrixQuestionSummary = ({ questionSummary, survey, setFilter }: Ma
|
||||
return "";
|
||||
};
|
||||
|
||||
const columns = questionSummary.data[0]
|
||||
? questionSummary.data[0].columnPercentages.map((c) => c.column)
|
||||
: [];
|
||||
const columns = elementSummary.data[0] ? elementSummary.data[0].columnPercentages.map((c) => c.column) : [];
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
|
||||
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} />
|
||||
<div className="overflow-x-auto p-6">
|
||||
{/* Summary Table */}
|
||||
<table className="mx-auto border-collapse cursor-default text-left">
|
||||
@@ -63,7 +57,7 @@ export const MatrixQuestionSummary = ({ questionSummary, survey, setFilter }: Ma
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{questionSummary.data.map(({ rowLabel, columnPercentages }, rowIndex) => (
|
||||
{elementSummary.data.map(({ rowLabel, columnPercentages }, rowIndex) => (
|
||||
<tr key={rowLabel}>
|
||||
<td className="max-w-60 overflow-hidden text-ellipsis whitespace-nowrap p-4">
|
||||
<TooltipRenderer tooltipContent={getTooltipContent(rowLabel)} shouldRender={true}>
|
||||
@@ -79,16 +73,16 @@ export const MatrixQuestionSummary = ({ questionSummary, survey, setFilter }: Ma
|
||||
tooltipContent={getTooltipContent(
|
||||
undefined,
|
||||
percentage,
|
||||
questionSummary.data[rowIndex].totalResponsesForRow
|
||||
elementSummary.data[rowIndex].totalResponsesForRow
|
||||
)}>
|
||||
<button
|
||||
style={{ backgroundColor: `rgba(0,196,184,${getOpacityLevel(percentage)})` }}
|
||||
className="hover:outline-brand-dark m-1 flex h-full w-40 cursor-pointer items-center justify-center rounded p-4 text-sm text-slate-950 hover:outline"
|
||||
onClick={() =>
|
||||
setFilter(
|
||||
questionSummary.question.id,
|
||||
questionSummary.question.headline,
|
||||
questionSummary.question.type,
|
||||
elementSummary.element.id,
|
||||
elementSummary.element.headline,
|
||||
elementSummary.element.type,
|
||||
rowLabel,
|
||||
column
|
||||
)
|
||||
+20
-24
@@ -4,14 +4,9 @@ import { InboxIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Fragment, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
TSurveyQuestionId,
|
||||
TSurveyQuestionSummaryMultipleChoice,
|
||||
TSurveyQuestionTypeEnum,
|
||||
TSurveyType,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey, TSurveyElementSummaryMultipleChoice, TSurveyType } from "@formbricks/types/surveys/types";
|
||||
import { getChoiceIdByValue } from "@/lib/response/utils";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
@@ -19,24 +14,24 @@ import { Button } from "@/modules/ui/components/button";
|
||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
import { ProgressBar } from "@/modules/ui/components/progress-bar";
|
||||
import { convertFloatToNDecimal } from "../lib/utils";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
|
||||
interface MultipleChoiceSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryMultipleChoice;
|
||||
elementSummary: TSurveyElementSummaryMultipleChoice;
|
||||
environmentId: string;
|
||||
surveyType: TSurveyType;
|
||||
survey: TSurvey;
|
||||
setFilter: (
|
||||
questionId: TSurveyQuestionId,
|
||||
elementId: string,
|
||||
label: TI18nString,
|
||||
questionType: TSurveyQuestionTypeEnum,
|
||||
elementType: TSurveyElementTypeEnum,
|
||||
filterValue: string,
|
||||
filterComboBoxValue?: string | string[]
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const MultipleChoiceSummary = ({
|
||||
questionSummary,
|
||||
elementSummary,
|
||||
environmentId,
|
||||
surveyType,
|
||||
survey,
|
||||
@@ -44,9 +39,9 @@ export const MultipleChoiceSummary = ({
|
||||
}: MultipleChoiceSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [visibleOtherResponses, setVisibleOtherResponses] = useState(10);
|
||||
const otherValue = questionSummary.question.choices.find((choice) => choice.id === "other")?.label.default;
|
||||
const otherValue = elementSummary.element.choices.find((choice) => choice.id === "other")?.label.default;
|
||||
// sort by count and transform to array
|
||||
const results = Object.values(questionSummary.choices).sort((a, b) => {
|
||||
const results = Object.values(elementSummary.choices).sort((a, b) => {
|
||||
const aHasOthers = (a.others?.length ?? 0) > 0;
|
||||
const bHasOthers = (b.others?.length ?? 0) > 0;
|
||||
|
||||
@@ -73,14 +68,14 @@ export const MultipleChoiceSummary = ({
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader
|
||||
questionSummary={questionSummary}
|
||||
<ElementSummaryHeader
|
||||
elementSummary={elementSummary}
|
||||
survey={survey}
|
||||
additionalInfo={
|
||||
questionSummary.type === "multipleChoiceMulti" ? (
|
||||
elementSummary.type === "multipleChoiceMulti" ? (
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxIcon className="mr-2 h-4 w-4" />
|
||||
{`${questionSummary.selectionCount} ${t("common.selections")}`}
|
||||
{`${elementSummary.selectionCount} ${t("common.selections")}`}
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
@@ -88,7 +83,7 @@ export const MultipleChoiceSummary = ({
|
||||
<div className="px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
|
||||
<div className="space-y-5">
|
||||
{results.map((result) => {
|
||||
const choiceId = getChoiceIdByValue(result.value, questionSummary.question);
|
||||
const choiceId = getChoiceIdByValue(result.value, elementSummary.element);
|
||||
return (
|
||||
<Fragment key={result.value}>
|
||||
<button
|
||||
@@ -96,10 +91,11 @@ export const MultipleChoiceSummary = ({
|
||||
className="group w-full cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilter(
|
||||
questionSummary.question.id,
|
||||
questionSummary.question.headline,
|
||||
questionSummary.question.type,
|
||||
questionSummary.type === "multipleChoiceSingle" || otherValue === result.value
|
||||
elementSummary.element.id,
|
||||
elementSummary.element.headline,
|
||||
elementSummary.element.type,
|
||||
elementSummary.type === TSurveyElementTypeEnum.MultipleChoiceSingle ||
|
||||
otherValue === result.value
|
||||
? t("environments.surveys.summary.includes_either")
|
||||
: t("environments.surveys.summary.includes_all"),
|
||||
[result.value]
|
||||
|
||||
+24
-28
@@ -3,28 +3,24 @@
|
||||
import { BarChart, BarChartHorizontal } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
TSurveyQuestionId,
|
||||
TSurveyQuestionSummaryNps,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { type TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey, TSurveyElementSummaryNps } from "@formbricks/types/surveys/types";
|
||||
import { HalfCircle, ProgressBar } from "@/modules/ui/components/progress-bar";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/modules/ui/components/tabs";
|
||||
import { TooltipProvider } from "@/modules/ui/components/tooltip";
|
||||
import { convertFloatToNDecimal } from "../lib/utils";
|
||||
import { ClickableBarSegment } from "./ClickableBarSegment";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
import { SatisfactionIndicator } from "./SatisfactionIndicator";
|
||||
|
||||
interface NPSSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryNps;
|
||||
elementSummary: TSurveyElementSummaryNps;
|
||||
survey: TSurvey;
|
||||
setFilter: (
|
||||
questionId: TSurveyQuestionId,
|
||||
elementId: string,
|
||||
label: TI18nString,
|
||||
questionType: TSurveyQuestionTypeEnum,
|
||||
elementType: TSurveyElementTypeEnum,
|
||||
filterValue: string,
|
||||
filterComboBoxValue?: string | string[]
|
||||
) => void;
|
||||
@@ -40,7 +36,7 @@ const calculateNPSOpacity = (rating: number): number => {
|
||||
return 0.8 + ((rating - 8) / 2) * 0.2;
|
||||
};
|
||||
|
||||
export const NPSSummary = ({ questionSummary, survey, setFilter }: NPSSummaryProps) => {
|
||||
export const NPSSummary = ({ elementSummary, survey, setFilter }: NPSSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<"aggregated" | "individual">("aggregated");
|
||||
|
||||
@@ -68,9 +64,9 @@ export const NPSSummary = ({ questionSummary, survey, setFilter }: NPSSummaryPro
|
||||
|
||||
if (filter) {
|
||||
setFilter(
|
||||
questionSummary.question.id,
|
||||
questionSummary.question.headline,
|
||||
questionSummary.question.type,
|
||||
elementSummary.element.id,
|
||||
elementSummary.element.headline,
|
||||
elementSummary.element.type,
|
||||
filter.comparison,
|
||||
filter.values
|
||||
);
|
||||
@@ -79,15 +75,15 @@ export const NPSSummary = ({ questionSummary, survey, setFilter }: NPSSummaryPro
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader
|
||||
questionSummary={questionSummary}
|
||||
<ElementSummaryHeader
|
||||
elementSummary={elementSummary}
|
||||
survey={survey}
|
||||
additionalInfo={
|
||||
<div className="flex items-center space-x-2 rounded-lg bg-slate-100 p-2">
|
||||
<SatisfactionIndicator percentage={questionSummary.promoters.percentage} />
|
||||
<SatisfactionIndicator percentage={elementSummary.promoters.percentage} />
|
||||
<div>
|
||||
{t("environments.surveys.summary.promoters")}:{" "}
|
||||
{convertFloatToNDecimal(questionSummary.promoters.percentage, 2)}%
|
||||
{convertFloatToNDecimal(elementSummary.promoters.percentage, 2)}%
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -122,18 +118,18 @@ export const NPSSummary = ({ questionSummary, survey, setFilter }: NPSSummaryPro
|
||||
</p>
|
||||
<div>
|
||||
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(questionSummary[group]?.percentage, 2)}%
|
||||
{convertFloatToNDecimal(elementSummary[group]?.percentage, 2)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="flex w-32 items-end justify-end text-slate-600">
|
||||
{questionSummary[group]?.count}{" "}
|
||||
{questionSummary[group]?.count === 1 ? t("common.response") : t("common.responses")}
|
||||
{elementSummary[group]?.count}{" "}
|
||||
{elementSummary[group]?.count === 1 ? t("common.response") : t("common.responses")}
|
||||
</p>
|
||||
</div>
|
||||
<ProgressBar
|
||||
barColor={group === "dismissed" ? "bg-slate-600" : "bg-brand-dark"}
|
||||
progress={questionSummary[group]?.percentage / 100}
|
||||
progress={elementSummary[group]?.percentage / 100}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
@@ -144,7 +140,7 @@ export const NPSSummary = ({ questionSummary, survey, setFilter }: NPSSummaryPro
|
||||
<TabsContent value="individual" className="mt-4">
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<div className="grid grid-cols-11 gap-2 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
|
||||
{questionSummary.choices.map((choice) => {
|
||||
{elementSummary.choices.map((choice) => {
|
||||
const opacity = calculateNPSOpacity(choice.rating);
|
||||
|
||||
return (
|
||||
@@ -153,9 +149,9 @@ export const NPSSummary = ({ questionSummary, survey, setFilter }: NPSSummaryPro
|
||||
className="group flex cursor-pointer flex-col items-center"
|
||||
onClick={() =>
|
||||
setFilter(
|
||||
questionSummary.question.id,
|
||||
questionSummary.question.headline,
|
||||
questionSummary.question.type,
|
||||
elementSummary.element.id,
|
||||
elementSummary.element.headline,
|
||||
elementSummary.element.type,
|
||||
t("environments.surveys.summary.is_equal_to"),
|
||||
choice.rating.toString()
|
||||
)
|
||||
@@ -187,7 +183,7 @@ export const NPSSummary = ({ questionSummary, survey, setFilter }: NPSSummaryPro
|
||||
</Tabs>
|
||||
|
||||
<div className="flex justify-center pb-4 pt-4">
|
||||
<HalfCircle value={questionSummary.score} />
|
||||
<HalfCircle value={elementSummary.score} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+9
-9
@@ -3,7 +3,7 @@
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyQuestionSummaryOpenText } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyElementSummaryOpenText } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
@@ -12,31 +12,31 @@ import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/modules/ui/components/table";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
|
||||
interface OpenTextSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryOpenText;
|
||||
elementSummary: TSurveyElementSummaryOpenText;
|
||||
environmentId: string;
|
||||
survey: TSurvey;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const OpenTextSummary = ({ questionSummary, environmentId, survey, locale }: OpenTextSummaryProps) => {
|
||||
export const OpenTextSummary = ({ elementSummary, environmentId, survey, locale }: OpenTextSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [visibleResponses, setVisibleResponses] = useState(10);
|
||||
|
||||
const handleLoadMore = () => {
|
||||
// Increase the number of visible responses by 10, not exceeding the total number of responses
|
||||
setVisibleResponses((prevVisibleResponses) =>
|
||||
Math.min(prevVisibleResponses + 10, questionSummary.samples.length)
|
||||
Math.min(prevVisibleResponses + 10, elementSummary.samples.length)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
|
||||
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} />
|
||||
<div className="border-t border-slate-200"></div>
|
||||
{questionSummary.samples.length === 0 ? (
|
||||
{elementSummary.samples.length === 0 ? (
|
||||
<div className="p-8">
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} variant="simple" />
|
||||
</div>
|
||||
@@ -51,7 +51,7 @@ export const OpenTextSummary = ({ questionSummary, environmentId, survey, locale
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{questionSummary.samples.slice(0, visibleResponses).map((response) => (
|
||||
{elementSummary.samples.slice(0, visibleResponses).map((response) => (
|
||||
<TableRow key={response.id}>
|
||||
<TableCell className="w-1/4">
|
||||
{response.contact ? (
|
||||
@@ -86,7 +86,7 @@ export const OpenTextSummary = ({ questionSummary, environmentId, survey, locale
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{visibleResponses < questionSummary.samples.length && (
|
||||
{visibleResponses < elementSummary.samples.length && (
|
||||
<div className="flex justify-center py-4">
|
||||
<Button onClick={handleLoadMore} variant="secondary" size="sm">
|
||||
{t("common.load_more")}
|
||||
|
||||
+17
-21
@@ -3,52 +3,48 @@
|
||||
import { InboxIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
TSurveyQuestionId,
|
||||
TSurveyQuestionSummaryPictureSelection,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { type TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey, TSurveyElementSummaryPictureSelection } from "@formbricks/types/surveys/types";
|
||||
import { getChoiceIdByValue } from "@/lib/response/utils";
|
||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
import { ProgressBar } from "@/modules/ui/components/progress-bar";
|
||||
import { convertFloatToNDecimal } from "../lib/utils";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
|
||||
interface PictureChoiceSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryPictureSelection;
|
||||
elementSummary: TSurveyElementSummaryPictureSelection;
|
||||
survey: TSurvey;
|
||||
setFilter: (
|
||||
questionId: TSurveyQuestionId,
|
||||
elementId: string,
|
||||
label: TI18nString,
|
||||
questionType: TSurveyQuestionTypeEnum,
|
||||
elementType: TSurveyElementTypeEnum,
|
||||
filterValue: string,
|
||||
filterComboBoxValue?: string | string[]
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const PictureChoiceSummary = ({ questionSummary, survey, setFilter }: PictureChoiceSummaryProps) => {
|
||||
const results = questionSummary.choices;
|
||||
export const PictureChoiceSummary = ({ elementSummary, survey, setFilter }: PictureChoiceSummaryProps) => {
|
||||
const results = elementSummary.choices;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader
|
||||
questionSummary={questionSummary}
|
||||
<ElementSummaryHeader
|
||||
elementSummary={elementSummary}
|
||||
survey={survey}
|
||||
additionalInfo={
|
||||
questionSummary.question.allowMulti ? (
|
||||
elementSummary.element.allowMulti ? (
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxIcon className="mr-2 h-4 w-4" />
|
||||
{`${questionSummary.selectionCount} ${t("common.selections")}`}
|
||||
{`${elementSummary.selectionCount} ${t("common.selections")}`}
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
|
||||
{results.map((result, index) => {
|
||||
const choiceId = getChoiceIdByValue(result.imageUrl, questionSummary.question);
|
||||
const choiceId = getChoiceIdByValue(result.imageUrl, elementSummary.element);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@@ -56,9 +52,9 @@ export const PictureChoiceSummary = ({ questionSummary, survey, setFilter }: Pic
|
||||
key={result.id}
|
||||
onClick={() =>
|
||||
setFilter(
|
||||
questionSummary.question.id,
|
||||
questionSummary.question.headline,
|
||||
questionSummary.question.type,
|
||||
elementSummary.element.id,
|
||||
elementSummary.element.headline,
|
||||
elementSummary.element.type,
|
||||
t("environments.surveys.summary.includes_all"),
|
||||
[`${t("environments.surveys.edit.picture_idx", { idx: index + 1 })}`]
|
||||
)
|
||||
|
||||
+7
-7
@@ -1,28 +1,28 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyQuestionSummaryRanking } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyElementSummaryRanking } from "@formbricks/types/surveys/types";
|
||||
import { getChoiceIdByValue } from "@/lib/response/utils";
|
||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
import { convertFloatToNDecimal } from "../lib/utils";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
|
||||
interface RankingSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryRanking;
|
||||
elementSummary: TSurveyElementSummaryRanking;
|
||||
survey: TSurvey;
|
||||
}
|
||||
|
||||
export const RankingSummary = ({ questionSummary, survey }: RankingSummaryProps) => {
|
||||
export const RankingSummary = ({ elementSummary, survey }: RankingSummaryProps) => {
|
||||
// sort by count and transform to array
|
||||
const { t } = useTranslation();
|
||||
const results = Object.values(questionSummary.choices).sort((a, b) => {
|
||||
const results = Object.values(elementSummary.choices).sort((a, b) => {
|
||||
return a.avgRanking - b.avgRanking; // Sort by count
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
|
||||
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} />
|
||||
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
|
||||
{results.map((result, resultsIdx) => {
|
||||
const choiceId = getChoiceIdByValue(result.value, questionSummary.question);
|
||||
const choiceId = getChoiceIdByValue(result.value, elementSummary.element);
|
||||
return (
|
||||
<div key={result.value} className="group cursor-pointer">
|
||||
<div className="text flex flex-col justify-between px-2 pb-2 sm:flex-row">
|
||||
|
||||
+40
-45
@@ -3,13 +3,9 @@
|
||||
import { BarChart, BarChartHorizontal, CircleSlash2, SmileIcon, StarIcon } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
TSurveyQuestionId,
|
||||
TSurveyQuestionSummaryRating,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { type TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey, TSurveyElementSummaryRating } from "@formbricks/types/surveys/types";
|
||||
import { convertFloatToNDecimal } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
import { ProgressBar } from "@/modules/ui/components/progress-bar";
|
||||
@@ -17,52 +13,51 @@ import { RatingResponse } from "@/modules/ui/components/rating-response";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/modules/ui/components/tabs";
|
||||
import { TooltipProvider } from "@/modules/ui/components/tooltip";
|
||||
import { ClickableBarSegment } from "./ClickableBarSegment";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
import { RatingScaleLegend } from "./RatingScaleLegend";
|
||||
import { SatisfactionIndicator } from "./SatisfactionIndicator";
|
||||
|
||||
interface RatingSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryRating;
|
||||
elementSummary: TSurveyElementSummaryRating;
|
||||
survey: TSurvey;
|
||||
setFilter: (
|
||||
questionId: TSurveyQuestionId,
|
||||
elementId: string,
|
||||
label: TI18nString,
|
||||
questionType: TSurveyQuestionTypeEnum,
|
||||
elementType: TSurveyElementTypeEnum,
|
||||
filterValue: string,
|
||||
filterComboBoxValue?: string | string[]
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const RatingSummary = ({ questionSummary, survey, setFilter }: RatingSummaryProps) => {
|
||||
export const RatingSummary = ({ elementSummary, survey, setFilter }: RatingSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<"aggregated" | "individual">("aggregated");
|
||||
|
||||
const getIconBasedOnScale = useMemo(() => {
|
||||
const scale = questionSummary.question.scale;
|
||||
const scale = elementSummary.element.scale;
|
||||
if (scale === "number") return <CircleSlash2 className="h-4 w-4" />;
|
||||
else if (scale === "star") return <StarIcon fill="rgb(250 204 21)" className="h-4 w-4 text-yellow-400" />;
|
||||
else if (scale === "smiley") return <SmileIcon className="h-4 w-4" />;
|
||||
}, [questionSummary]);
|
||||
}, [elementSummary]);
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader
|
||||
questionSummary={questionSummary}
|
||||
<ElementSummaryHeader
|
||||
elementSummary={elementSummary}
|
||||
survey={survey}
|
||||
additionalInfo={
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center space-x-2 rounded-lg bg-slate-100 p-2">
|
||||
{getIconBasedOnScale}
|
||||
<div>
|
||||
{t("environments.surveys.summary.overall")}: {questionSummary.average.toFixed(2)}
|
||||
{t("environments.surveys.summary.overall")}: {elementSummary.average.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 rounded-lg bg-slate-100 p-2">
|
||||
<SatisfactionIndicator percentage={questionSummary.csat.satisfiedPercentage} />
|
||||
<SatisfactionIndicator percentage={elementSummary.csat.satisfiedPercentage} />
|
||||
<div>
|
||||
CSAT: {questionSummary.csat.satisfiedPercentage}%{" "}
|
||||
{t("environments.surveys.summary.satisfied")}
|
||||
CSAT: {elementSummary.csat.satisfiedPercentage}% {t("environments.surveys.summary.satisfied")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,25 +78,25 @@ export const RatingSummary = ({ questionSummary, survey, setFilter }: RatingSumm
|
||||
|
||||
<TabsContent value="aggregated" className="mt-4">
|
||||
<div className="px-4 pb-6 pt-4 md:px-6">
|
||||
{questionSummary.responseCount === 0 ? (
|
||||
{elementSummary.responseCount === 0 ? (
|
||||
<>
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} variant="simple" />
|
||||
<RatingScaleLegend
|
||||
scale={questionSummary.question.scale}
|
||||
range={questionSummary.question.range}
|
||||
scale={elementSummary.element.scale}
|
||||
range={elementSummary.element.range}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<div className="flex h-12 w-full overflow-hidden rounded-t-lg border border-slate-200">
|
||||
{questionSummary.choices.map((result, index) => {
|
||||
{elementSummary.choices.map((result, index) => {
|
||||
if (result.percentage === 0) return null;
|
||||
|
||||
const range = questionSummary.question.range;
|
||||
const range = elementSummary.element.range;
|
||||
const opacity = 0.3 + (result.rating / range) * 0.8;
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === questionSummary.choices.length - 1;
|
||||
const isLast = index === elementSummary.choices.length - 1;
|
||||
|
||||
return (
|
||||
<ClickableBarSegment
|
||||
@@ -113,9 +108,9 @@ export const RatingSummary = ({ questionSummary, survey, setFilter }: RatingSumm
|
||||
}}
|
||||
onClick={() =>
|
||||
setFilter(
|
||||
questionSummary.question.id,
|
||||
questionSummary.question.headline,
|
||||
questionSummary.question.type,
|
||||
elementSummary.element.id,
|
||||
elementSummary.element.headline,
|
||||
elementSummary.element.type,
|
||||
t("environments.surveys.summary.is_equal_to"),
|
||||
result.rating.toString()
|
||||
)
|
||||
@@ -130,7 +125,7 @@ export const RatingSummary = ({ questionSummary, survey, setFilter }: RatingSumm
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
<div className="flex w-full overflow-hidden rounded-b-lg border border-t-0 border-slate-200 bg-slate-50">
|
||||
{questionSummary.choices.map((result, index) => {
|
||||
{elementSummary.choices.map((result, index) => {
|
||||
if (result.percentage === 0) return null;
|
||||
|
||||
return (
|
||||
@@ -140,15 +135,15 @@ export const RatingSummary = ({ questionSummary, survey, setFilter }: RatingSumm
|
||||
style={{
|
||||
width: `${result.percentage}%`,
|
||||
borderRight:
|
||||
index < questionSummary.choices.length - 1
|
||||
index < elementSummary.choices.length - 1
|
||||
? "1px solid rgb(226, 232, 240)"
|
||||
: "none",
|
||||
}}>
|
||||
<div className="mb-1 flex items-center justify-center">
|
||||
<RatingResponse
|
||||
scale={questionSummary.question.scale}
|
||||
scale={elementSummary.element.scale}
|
||||
answer={result.rating}
|
||||
range={questionSummary.question.range}
|
||||
range={elementSummary.element.range}
|
||||
addColors={false}
|
||||
variant="aggregated"
|
||||
/>
|
||||
@@ -161,8 +156,8 @@ export const RatingSummary = ({ questionSummary, survey, setFilter }: RatingSumm
|
||||
})}
|
||||
</div>
|
||||
<RatingScaleLegend
|
||||
scale={questionSummary.question.scale}
|
||||
range={questionSummary.question.range}
|
||||
scale={elementSummary.element.scale}
|
||||
range={elementSummary.element.range}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -172,15 +167,15 @@ export const RatingSummary = ({ questionSummary, survey, setFilter }: RatingSumm
|
||||
<TabsContent value="individual" className="mt-4">
|
||||
<div className="px-4 pb-6 pt-4 md:px-6">
|
||||
<div className="space-y-5 text-sm md:text-base">
|
||||
{questionSummary.choices.map((result) => (
|
||||
{elementSummary.choices.map((result) => (
|
||||
<div key={result.rating}>
|
||||
<button
|
||||
className="w-full cursor-pointer hover:opacity-80"
|
||||
onClick={() =>
|
||||
setFilter(
|
||||
questionSummary.question.id,
|
||||
questionSummary.question.headline,
|
||||
questionSummary.question.type,
|
||||
elementSummary.element.id,
|
||||
elementSummary.element.headline,
|
||||
elementSummary.element.type,
|
||||
t("environments.surveys.summary.is_equal_to"),
|
||||
result.rating.toString()
|
||||
)
|
||||
@@ -189,10 +184,10 @@ export const RatingSummary = ({ questionSummary, survey, setFilter }: RatingSumm
|
||||
<div className="mr-8 flex items-center space-x-1">
|
||||
<div className="font-semibold text-slate-700">
|
||||
<RatingResponse
|
||||
scale={questionSummary.question.scale}
|
||||
scale={elementSummary.element.scale}
|
||||
answer={result.rating}
|
||||
range={questionSummary.question.range}
|
||||
addColors={questionSummary.question.isColorCodingEnabled}
|
||||
range={elementSummary.element.range}
|
||||
addColors={elementSummary.element.isColorCodingEnabled}
|
||||
variant="individual"
|
||||
/>
|
||||
</div>
|
||||
@@ -214,14 +209,14 @@ export const RatingSummary = ({ questionSummary, survey, setFilter }: RatingSumm
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
{questionSummary.dismissed && questionSummary.dismissed.count > 0 && (
|
||||
{elementSummary.dismissed && elementSummary.dismissed.count > 0 && (
|
||||
<div className="rounded-b-lg border-t bg-white px-6 py-4">
|
||||
<div key="dismissed">
|
||||
<div className="text flex justify-between px-2">
|
||||
<p className="font-semibold text-slate-700">{t("common.dismissed")}</p>
|
||||
<p className="flex w-32 items-end justify-end text-slate-600">
|
||||
{questionSummary.dismissed.count}{" "}
|
||||
{questionSummary.dismissed.count === 1 ? t("common.response") : t("common.responses")}
|
||||
{elementSummary.dismissed.count}{" "}
|
||||
{elementSummary.dismissed.count === 1 ? t("common.response") : t("common.responses")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+7
-6
@@ -2,10 +2,11 @@
|
||||
|
||||
import { TimerIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyQuestionType, TSurveySummary } from "@formbricks/types/surveys/types";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey, TSurveySummary } from "@formbricks/types/surveys/types";
|
||||
import { recallToHeadline } from "@/lib/utils/recall";
|
||||
import { formatTextWithSlashes } from "@/modules/survey/editor/lib/utils";
|
||||
import { getQuestionIcon } from "@/modules/survey/lib/questions";
|
||||
import { getElementIcon } from "@/modules/survey/lib/elements";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||
|
||||
interface SummaryDropOffsProps {
|
||||
@@ -15,8 +16,8 @@ interface SummaryDropOffsProps {
|
||||
|
||||
export const SummaryDropOffs = ({ dropOff, survey }: SummaryDropOffsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const getIcon = (questionType: TSurveyQuestionType) => {
|
||||
const Icon = getQuestionIcon(questionType, t);
|
||||
const getIcon = (elementType: TSurveyElementTypeEnum) => {
|
||||
const Icon = getElementIcon(elementType, t);
|
||||
return <Icon className="mt-[3px] h-5 w-5 shrink-0 text-slate-600" />;
|
||||
};
|
||||
|
||||
@@ -44,10 +45,10 @@ export const SummaryDropOffs = ({ dropOff, survey }: SummaryDropOffsProps) => {
|
||||
</div>
|
||||
{dropOff.map((quesDropOff) => (
|
||||
<div
|
||||
key={quesDropOff.questionId}
|
||||
key={quesDropOff.elementId}
|
||||
className="grid grid-cols-6 items-start border-b border-slate-100 text-xs text-slate-800 md:text-sm">
|
||||
<div className="col-span-3 flex gap-3 px-4 py-2 md:px-6">
|
||||
{getIcon(quesDropOff.questionType)}
|
||||
{getIcon(quesDropOff.elementType)}
|
||||
<p>
|
||||
{formatTextWithSlashes(
|
||||
recallToHeadline(
|
||||
|
||||
+65
-72
@@ -3,13 +3,10 @@
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
TSurveyQuestionId,
|
||||
TSurveyQuestionTypeEnum,
|
||||
TSurveySummary,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurveySummary } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { EmptyAppSurveys } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys";
|
||||
@@ -21,10 +18,10 @@ import { CTASummary } from "@/app/(app)/environments/[environmentId]/surveys/[su
|
||||
import { CalSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CalSummary";
|
||||
import { ConsentSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary";
|
||||
import { ContactInfoSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ContactInfoSummary";
|
||||
import { DateQuestionSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/DateQuestionSummary";
|
||||
import { DateElementSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/DateElementSummary";
|
||||
import { FileUploadSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/FileUploadSummary";
|
||||
import { HiddenFieldsSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary";
|
||||
import { MatrixQuestionSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixQuestionSummary";
|
||||
import { MatrixElementSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixElementSummary";
|
||||
import { MultipleChoiceSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary";
|
||||
import { NPSSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary";
|
||||
import { OpenTextSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary";
|
||||
@@ -32,7 +29,7 @@ import { PictureChoiceSummary } from "@/app/(app)/environments/[environmentId]/s
|
||||
import { RankingSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RankingSummary";
|
||||
import { RatingSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary";
|
||||
import { constructToastMessage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
|
||||
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
|
||||
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ElementsComboBox";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
import { SkeletonLoader } from "@/modules/ui/components/skeleton-loader";
|
||||
@@ -50,29 +47,29 @@ export const SummaryList = ({ summary, environment, responseCount, survey, local
|
||||
const { setSelectedFilter, selectedFilter } = useResponseFilter();
|
||||
const { t } = useTranslation();
|
||||
const setFilter = (
|
||||
questionId: TSurveyQuestionId,
|
||||
elementId: string,
|
||||
label: TI18nString,
|
||||
questionType: TSurveyQuestionTypeEnum,
|
||||
elementType: TSurveyElementTypeEnum,
|
||||
filterValue: string,
|
||||
filterComboBoxValue?: string | string[]
|
||||
) => {
|
||||
const filterObject: SelectedFilterValue = { ...selectedFilter };
|
||||
const value = {
|
||||
id: questionId,
|
||||
id: elementId,
|
||||
label: getTextContent(getLocalizedValue(label, "default")),
|
||||
questionType: questionType,
|
||||
type: OptionsType.QUESTIONS,
|
||||
elementType,
|
||||
type: OptionsType.ELEMENTS,
|
||||
};
|
||||
|
||||
// Find the index of the existing filter with the same questionId
|
||||
// Find the index of the existing filter with the same elementId
|
||||
const existingFilterIndex = filterObject.filter.findIndex(
|
||||
(filter) => filter.questionType.id === questionId
|
||||
(filter) => filter.elementType.id === elementId
|
||||
);
|
||||
|
||||
if (existingFilterIndex !== -1) {
|
||||
// Replace the existing filter
|
||||
filterObject.filter[existingFilterIndex] = {
|
||||
questionType: value,
|
||||
elementType: value,
|
||||
filterType: {
|
||||
filterComboBoxValue: filterComboBoxValue,
|
||||
filterValue: filterValue,
|
||||
@@ -82,14 +79,14 @@ export const SummaryList = ({ summary, environment, responseCount, survey, local
|
||||
} else {
|
||||
// Add new filter
|
||||
filterObject.filter.push({
|
||||
questionType: value,
|
||||
elementType: value,
|
||||
filterType: {
|
||||
filterComboBoxValue: filterComboBoxValue,
|
||||
filterValue: filterValue,
|
||||
},
|
||||
});
|
||||
toast.success(
|
||||
constructToastMessage(questionType, filterValue, survey, questionId, t, filterComboBoxValue) ??
|
||||
constructToastMessage(elementType, filterValue, survey, elementId, t, filterComboBoxValue) ??
|
||||
t("environments.surveys.summary.filter_added_successfully"),
|
||||
{ duration: 5000 }
|
||||
);
|
||||
@@ -110,12 +107,12 @@ export const SummaryList = ({ summary, environment, responseCount, survey, local
|
||||
) : responseCount === 0 ? (
|
||||
<EmptyState text={t("environments.surveys.summary.no_responses_found")} />
|
||||
) : (
|
||||
summary.map((questionSummary) => {
|
||||
if (questionSummary.type === TSurveyQuestionTypeEnum.OpenText) {
|
||||
summary.map((elementSummary) => {
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.OpenText) {
|
||||
return (
|
||||
<OpenTextSummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary}
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
environmentId={environment.id}
|
||||
survey={survey}
|
||||
locale={locale}
|
||||
@@ -123,13 +120,13 @@ export const SummaryList = ({ summary, environment, responseCount, survey, local
|
||||
);
|
||||
}
|
||||
if (
|
||||
questionSummary.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ||
|
||||
questionSummary.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti
|
||||
elementSummary.type === TSurveyElementTypeEnum.MultipleChoiceSingle ||
|
||||
elementSummary.type === TSurveyElementTypeEnum.MultipleChoiceMulti
|
||||
) {
|
||||
return (
|
||||
<MultipleChoiceSummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary}
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
environmentId={environment.id}
|
||||
surveyType={survey.type}
|
||||
survey={survey}
|
||||
@@ -137,132 +134,128 @@ export const SummaryList = ({ summary, environment, responseCount, survey, local
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (questionSummary.type === TSurveyQuestionTypeEnum.NPS) {
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.NPS) {
|
||||
return (
|
||||
<NPSSummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary}
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
survey={survey}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (questionSummary.type === TSurveyQuestionTypeEnum.CTA) {
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.CTA) {
|
||||
return (
|
||||
<CTASummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
/>
|
||||
<CTASummary key={elementSummary.element.id} elementSummary={elementSummary} survey={survey} />
|
||||
);
|
||||
}
|
||||
if (questionSummary.type === TSurveyQuestionTypeEnum.Rating) {
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.Rating) {
|
||||
return (
|
||||
<RatingSummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary}
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
survey={survey}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (questionSummary.type === TSurveyQuestionTypeEnum.Consent) {
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.Consent) {
|
||||
return (
|
||||
<ConsentSummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary}
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
survey={survey}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (questionSummary.type === TSurveyQuestionTypeEnum.PictureSelection) {
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.PictureSelection) {
|
||||
return (
|
||||
<PictureChoiceSummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary}
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
survey={survey}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (questionSummary.type === TSurveyQuestionTypeEnum.Date) {
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.Date) {
|
||||
return (
|
||||
<DateQuestionSummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary}
|
||||
<DateElementSummary
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
environmentId={environment.id}
|
||||
survey={survey}
|
||||
locale={locale}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (questionSummary.type === TSurveyQuestionTypeEnum.FileUpload) {
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.FileUpload) {
|
||||
return (
|
||||
<FileUploadSummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary}
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
environmentId={environment.id}
|
||||
survey={survey}
|
||||
locale={locale}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (questionSummary.type === TSurveyQuestionTypeEnum.Cal) {
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.Cal) {
|
||||
return (
|
||||
<CalSummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary}
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
environmentId={environment.id}
|
||||
survey={survey}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (questionSummary.type === TSurveyQuestionTypeEnum.Matrix) {
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.Matrix) {
|
||||
return (
|
||||
<MatrixQuestionSummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary}
|
||||
<MatrixElementSummary
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
survey={survey}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (questionSummary.type === TSurveyQuestionTypeEnum.Address) {
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.Address) {
|
||||
return (
|
||||
<AddressSummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary}
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
environmentId={environment.id}
|
||||
survey={survey}
|
||||
locale={locale}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (questionSummary.type === TSurveyQuestionTypeEnum.Ranking) {
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.Ranking) {
|
||||
return (
|
||||
<RankingSummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary}
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
survey={survey}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (questionSummary.type === "hiddenField") {
|
||||
if (elementSummary.type === "hiddenField") {
|
||||
return (
|
||||
<HiddenFieldsSummary
|
||||
key={questionSummary.id}
|
||||
questionSummary={questionSummary}
|
||||
key={elementSummary.id}
|
||||
elementSummary={elementSummary}
|
||||
environment={environment}
|
||||
locale={locale}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (questionSummary.type === TSurveyQuestionTypeEnum.ContactInfo) {
|
||||
if (elementSummary.type === TSurveyElementTypeEnum.ContactInfo) {
|
||||
return (
|
||||
<ContactInfoSummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary}
|
||||
key={elementSummary.element.id}
|
||||
elementSummary={elementSummary}
|
||||
environmentId={environment.id}
|
||||
survey={survey}
|
||||
locale={locale}
|
||||
|
||||
+2
-1
@@ -5,7 +5,8 @@ import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils";
|
||||
import { TI18nString, TSurvey, TSurveyMetadata } from "@formbricks/types/surveys/types";
|
||||
import { TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurvey, TSurveyMetadata } from "@formbricks/types/surveys/types";
|
||||
import { useSurvey } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/context/survey-context";
|
||||
import { createI18nString, extractLanguageCodes, getEnabledLanguages } from "@/lib/i18n/utils";
|
||||
import { updateSurveyAction } from "@/modules/survey/editor/actions";
|
||||
|
||||
+1054
-327
File diff suppressed because it is too large
Load Diff
+220
-165
@@ -14,23 +14,24 @@ import {
|
||||
TResponseVariables,
|
||||
ZResponseFilterCriteria,
|
||||
} from "@formbricks/types/responses";
|
||||
import {
|
||||
TSurveyElement,
|
||||
TSurveyElementChoice,
|
||||
TSurveyElementTypeEnum,
|
||||
} from "@formbricks/types/surveys/elements";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyContactInfoQuestion,
|
||||
TSurveyElementSummaryAddress,
|
||||
TSurveyElementSummaryContactInfo,
|
||||
TSurveyElementSummaryDate,
|
||||
TSurveyElementSummaryFileUpload,
|
||||
TSurveyElementSummaryHiddenFields,
|
||||
TSurveyElementSummaryMultipleChoice,
|
||||
TSurveyElementSummaryOpenText,
|
||||
TSurveyElementSummaryPictureSelection,
|
||||
TSurveyElementSummaryRanking,
|
||||
TSurveyElementSummaryRating,
|
||||
TSurveyLanguage,
|
||||
TSurveyMultipleChoiceQuestion,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionId,
|
||||
TSurveyQuestionSummaryAddress,
|
||||
TSurveyQuestionSummaryDate,
|
||||
TSurveyQuestionSummaryFileUpload,
|
||||
TSurveyQuestionSummaryHiddenFields,
|
||||
TSurveyQuestionSummaryMultipleChoice,
|
||||
TSurveyQuestionSummaryOpenText,
|
||||
TSurveyQuestionSummaryPictureSelection,
|
||||
TSurveyQuestionSummaryRanking,
|
||||
TSurveyQuestionSummaryRating,
|
||||
TSurveyQuestionTypeEnum,
|
||||
TSurveySummary,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
@@ -40,6 +41,7 @@ import { getDisplayCountBySurveyId } from "@/lib/display/service";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { buildWhereClause } from "@/lib/response/utils";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { findElementLocation, getElementsFromBlocks } from "@/lib/survey/utils";
|
||||
import { evaluateLogic, performActions } from "@/lib/surveyLogic/utils";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { convertFloatTo2Decimal } from "./utils";
|
||||
@@ -95,39 +97,44 @@ export const getSurveySummaryMeta = (
|
||||
};
|
||||
};
|
||||
|
||||
const evaluateLogicAndGetNextQuestionId = (
|
||||
const evaluateLogicAndGetNextElementId = (
|
||||
localSurvey: TSurvey,
|
||||
elements: TSurveyElement[],
|
||||
data: TResponseData,
|
||||
localVariables: TResponseVariables,
|
||||
currentQuestionIndex: number,
|
||||
currQuesTemp: TSurveyQuestion,
|
||||
currentElementIndex: number,
|
||||
currElementTemp: TSurveyElement,
|
||||
selectedLanguage: string | null
|
||||
): {
|
||||
nextQuestionId: TSurveyQuestionId | undefined;
|
||||
nextElementId: string | undefined;
|
||||
updatedSurvey: TSurvey;
|
||||
updatedVariables: TResponseVariables;
|
||||
} => {
|
||||
const questions = localSurvey.questions;
|
||||
|
||||
let updatedSurvey = { ...localSurvey };
|
||||
let updatedVariables = { ...localVariables };
|
||||
|
||||
let firstJumpTarget: string | undefined;
|
||||
|
||||
if (currQuesTemp.logic && currQuesTemp.logic.length > 0) {
|
||||
for (const logic of currQuesTemp.logic) {
|
||||
const { block: currentBlock } = findElementLocation(localSurvey, currElementTemp.id);
|
||||
|
||||
if (currentBlock?.logic && currentBlock.logic.length > 0) {
|
||||
for (const logic of currentBlock.logic) {
|
||||
if (evaluateLogic(localSurvey, data, localVariables, logic.conditions, selectedLanguage ?? "default")) {
|
||||
const { jumpTarget, requiredQuestionIds, calculations } = performActions(
|
||||
const { jumpTarget, requiredElementIds, calculations } = performActions(
|
||||
updatedSurvey,
|
||||
logic.actions,
|
||||
data,
|
||||
updatedVariables
|
||||
);
|
||||
|
||||
if (requiredQuestionIds.length > 0) {
|
||||
updatedSurvey.questions = updatedSurvey.questions.map((q) =>
|
||||
requiredQuestionIds.includes(q.id) ? { ...q, required: true } : q
|
||||
);
|
||||
if (requiredElementIds.length > 0) {
|
||||
// Update blocks to mark elements as required
|
||||
updatedSurvey.blocks = updatedSurvey.blocks.map((block) => ({
|
||||
...block,
|
||||
elements: block.elements.map((e) =>
|
||||
requiredElementIds.includes(e.id) ? { ...e, required: true } : e
|
||||
),
|
||||
}));
|
||||
}
|
||||
updatedVariables = { ...updatedVariables, ...calculations };
|
||||
|
||||
@@ -139,32 +146,33 @@ const evaluateLogicAndGetNextQuestionId = (
|
||||
}
|
||||
|
||||
// If no jump target was set, check for a fallback logic
|
||||
if (!firstJumpTarget && currQuesTemp.logicFallback) {
|
||||
firstJumpTarget = currQuesTemp.logicFallback;
|
||||
if (!firstJumpTarget && currentBlock?.logicFallback) {
|
||||
firstJumpTarget = currentBlock.logicFallback;
|
||||
}
|
||||
|
||||
// Return the first jump target if found, otherwise go to the next question
|
||||
const nextQuestionId = firstJumpTarget || questions[currentQuestionIndex + 1]?.id || undefined;
|
||||
// Return the first jump target if found, otherwise go to the next element
|
||||
const nextElementId = firstJumpTarget || elements[currentElementIndex + 1]?.id || undefined;
|
||||
|
||||
return { nextQuestionId, updatedSurvey, updatedVariables };
|
||||
return { nextElementId, updatedSurvey, updatedVariables };
|
||||
};
|
||||
|
||||
export const getSurveySummaryDropOff = (
|
||||
survey: TSurvey,
|
||||
elements: TSurveyElement[],
|
||||
responses: TSurveySummaryResponse[],
|
||||
displayCount: number
|
||||
): TSurveySummary["dropOff"] => {
|
||||
const initialTtc = survey.questions.reduce((acc: Record<string, number>, question) => {
|
||||
acc[question.id] = 0;
|
||||
const initialTtc = elements.reduce((acc: Record<string, number>, element) => {
|
||||
acc[element.id] = 0;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
let totalTtc = { ...initialTtc };
|
||||
let responseCounts = { ...initialTtc };
|
||||
|
||||
let dropOffArr = new Array(survey.questions.length).fill(0) as number[];
|
||||
let impressionsArr = new Array(survey.questions.length).fill(0) as number[];
|
||||
let dropOffPercentageArr = new Array(survey.questions.length).fill(0) as number[];
|
||||
let dropOffArr = new Array(elements.length).fill(0) as number[];
|
||||
let impressionsArr = new Array(elements.length).fill(0) as number[];
|
||||
let dropOffPercentageArr = new Array(elements.length).fill(0) as number[];
|
||||
|
||||
const surveyVariablesData = survey.variables?.reduce(
|
||||
(acc, variable) => {
|
||||
@@ -176,10 +184,10 @@ export const getSurveySummaryDropOff = (
|
||||
|
||||
responses.forEach((response) => {
|
||||
// Calculate total time-to-completion
|
||||
Object.keys(totalTtc).forEach((questionId) => {
|
||||
if (response.ttc && response.ttc[questionId]) {
|
||||
totalTtc[questionId] += response.ttc[questionId];
|
||||
responseCounts[questionId]++;
|
||||
Object.keys(totalTtc).forEach((elementId) => {
|
||||
if (response.ttc && response.ttc[elementId]) {
|
||||
totalTtc[elementId] += response.ttc[elementId];
|
||||
responseCounts[elementId]++;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -191,11 +199,11 @@ export const getSurveySummaryDropOff = (
|
||||
|
||||
let currQuesIdx = 0;
|
||||
|
||||
while (currQuesIdx < localSurvey.questions.length) {
|
||||
const currQues = localSurvey.questions[currQuesIdx];
|
||||
while (currQuesIdx < elements.length) {
|
||||
const currQues = elements[currQuesIdx];
|
||||
if (!currQues) break;
|
||||
|
||||
// question is not answered and required
|
||||
// element is not answered and required
|
||||
if (response.data[currQues.id] === undefined && currQues.required) {
|
||||
dropOffArr[currQuesIdx]++;
|
||||
impressionsArr[currQuesIdx]++;
|
||||
@@ -204,8 +212,9 @@ export const getSurveySummaryDropOff = (
|
||||
|
||||
impressionsArr[currQuesIdx]++;
|
||||
|
||||
const { nextQuestionId, updatedSurvey, updatedVariables } = evaluateLogicAndGetNextQuestionId(
|
||||
const { nextElementId, updatedSurvey, updatedVariables } = evaluateLogicAndGetNextElementId(
|
||||
localSurvey,
|
||||
elements,
|
||||
localResponseData,
|
||||
localVariables,
|
||||
currQuesIdx,
|
||||
@@ -216,9 +225,9 @@ export const getSurveySummaryDropOff = (
|
||||
localSurvey = updatedSurvey;
|
||||
localVariables = updatedVariables;
|
||||
|
||||
if (nextQuestionId) {
|
||||
const nextQuesIdx = survey.questions.findIndex((q) => q.id === nextQuestionId);
|
||||
if (!response.data[nextQuestionId] && !response.finished) {
|
||||
if (nextElementId) {
|
||||
const nextQuesIdx = elements.findIndex((q) => q.id === nextElementId);
|
||||
if (!response.data[nextElementId] && !response.finished) {
|
||||
dropOffArr[nextQuesIdx]++;
|
||||
impressionsArr[nextQuesIdx]++;
|
||||
break;
|
||||
@@ -230,10 +239,9 @@ export const getSurveySummaryDropOff = (
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate the average time for each question
|
||||
Object.keys(totalTtc).forEach((questionId) => {
|
||||
totalTtc[questionId] =
|
||||
responseCounts[questionId] > 0 ? totalTtc[questionId] / responseCounts[questionId] : 0;
|
||||
// Calculate the average time for each element
|
||||
Object.keys(totalTtc).forEach((elementId) => {
|
||||
totalTtc[elementId] = responseCounts[elementId] > 0 ? totalTtc[elementId] / responseCounts[elementId] : 0;
|
||||
});
|
||||
|
||||
if (!survey.welcomeCard.enabled) {
|
||||
@@ -250,18 +258,18 @@ export const getSurveySummaryDropOff = (
|
||||
dropOffPercentageArr[0] = (dropOffArr[0] / impressionsArr[0]) * 100;
|
||||
}
|
||||
|
||||
for (let i = 1; i < survey.questions.length; i++) {
|
||||
for (let i = 1; i < elements.length; i++) {
|
||||
if (impressionsArr[i] !== 0) {
|
||||
dropOffPercentageArr[i] = (dropOffArr[i] / impressionsArr[i]) * 100;
|
||||
}
|
||||
}
|
||||
|
||||
const dropOff = survey.questions.map((question, index) => {
|
||||
const dropOff = elements.map((element, index) => {
|
||||
return {
|
||||
questionId: question.id,
|
||||
questionType: question.type,
|
||||
headline: getTextContent(getLocalizedValue(question.headline, "default")),
|
||||
ttc: convertFloatTo2Decimal(totalTtc[question.id]) || 0,
|
||||
elementId: element.id,
|
||||
elementType: element.type,
|
||||
headline: getTextContent(getLocalizedValue(element.headline, "default")),
|
||||
ttc: convertFloatTo2Decimal(totalTtc[element.id]) || 0,
|
||||
impressions: impressionsArr[index] || 0,
|
||||
dropOffCount: dropOffArr[index] || 0,
|
||||
dropOffPercentage: convertFloatTo2Decimal(dropOffPercentageArr[index]) || 0,
|
||||
@@ -277,51 +285,66 @@ const getLanguageCode = (surveyLanguages: TSurveyLanguage[], languageCode: strin
|
||||
return language?.default ? "default" : language?.language.code || "default";
|
||||
};
|
||||
|
||||
const checkForI18n = (responseData: TResponseData, id: string, survey: TSurvey, languageCode: string) => {
|
||||
const question = survey.questions.find((question) => question.id === id);
|
||||
const checkForI18n = (
|
||||
responseData: TResponseData,
|
||||
id: string,
|
||||
elements: TSurveyElement[],
|
||||
languageCode: string
|
||||
) => {
|
||||
const element = elements.find((element) => element.id === id);
|
||||
|
||||
if (question?.type === "multipleChoiceMulti" || question?.type === "ranking") {
|
||||
if (element?.type === "multipleChoiceMulti" || element?.type === "ranking") {
|
||||
// Initialize an array to hold the choice values
|
||||
let choiceValues = [] as string[];
|
||||
|
||||
// Type guard: both element types have choices property
|
||||
const hasChoices = "choices" in element;
|
||||
if (!hasChoices) return [];
|
||||
|
||||
(typeof responseData[id] === "string"
|
||||
? ([responseData[id]] as string[])
|
||||
: (responseData[id] as string[])
|
||||
)?.forEach((data) => {
|
||||
choiceValues.push(
|
||||
getLocalizedValue(
|
||||
question.choices.find((choice) => choice.label[languageCode] === data)?.label,
|
||||
element.choices.find((choice) => choice.label[languageCode] === data)?.label,
|
||||
"default"
|
||||
) || data
|
||||
);
|
||||
});
|
||||
|
||||
// Return the array of localized choice values of multiSelect multi questions
|
||||
// Return the array of localized choice values of multiSelect multi elements
|
||||
return choiceValues;
|
||||
}
|
||||
|
||||
// Return the localized value of the choice fo multiSelect single question
|
||||
const choice = (question as TSurveyMultipleChoiceQuestion)?.choices.find(
|
||||
(choice) => choice.label[languageCode] === responseData[id]
|
||||
);
|
||||
// Return the localized value of the choice fo multiSelect single element
|
||||
if (element && "choices" in element) {
|
||||
const choice = element.choices?.find(
|
||||
(choice: TSurveyElementChoice) => choice.label?.[languageCode] === responseData[id]
|
||||
);
|
||||
return choice && "label" in choice
|
||||
? getLocalizedValue(choice.label, "default") || responseData[id]
|
||||
: responseData[id];
|
||||
}
|
||||
|
||||
return getLocalizedValue(choice?.label, "default") || responseData[id];
|
||||
return responseData[id];
|
||||
};
|
||||
|
||||
export const getQuestionSummary = async (
|
||||
export const getElementSummary = async (
|
||||
survey: TSurvey,
|
||||
elements: TSurveyElement[],
|
||||
responses: TSurveySummaryResponse[],
|
||||
dropOff: TSurveySummary["dropOff"]
|
||||
): Promise<TSurveySummary["summary"]> => {
|
||||
const VALUES_LIMIT = 50;
|
||||
let summary: TSurveySummary["summary"] = [];
|
||||
|
||||
for (const question of survey.questions) {
|
||||
switch (question.type) {
|
||||
case TSurveyQuestionTypeEnum.OpenText: {
|
||||
let values: TSurveyQuestionSummaryOpenText["samples"] = [];
|
||||
for (const element of elements) {
|
||||
switch (element.type) {
|
||||
case TSurveyElementTypeEnum.OpenText: {
|
||||
let values: TSurveyElementSummaryOpenText["samples"] = [];
|
||||
responses.forEach((response) => {
|
||||
const answer = response.data[question.id];
|
||||
const answer = response.data[element.id];
|
||||
if (answer && typeof answer === "string") {
|
||||
values.push({
|
||||
id: response.id,
|
||||
@@ -334,8 +357,8 @@ export const getQuestionSummary = async (
|
||||
});
|
||||
|
||||
summary.push({
|
||||
type: question.type,
|
||||
question,
|
||||
type: element.type,
|
||||
element: element,
|
||||
responseCount: values.length,
|
||||
samples: values.slice(0, VALUES_LIMIT),
|
||||
});
|
||||
@@ -343,18 +366,18 @@ export const getQuestionSummary = async (
|
||||
values = [];
|
||||
break;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.MultipleChoiceSingle:
|
||||
case TSurveyQuestionTypeEnum.MultipleChoiceMulti: {
|
||||
let values: TSurveyQuestionSummaryMultipleChoice["choices"] = [];
|
||||
case TSurveyElementTypeEnum.MultipleChoiceSingle:
|
||||
case TSurveyElementTypeEnum.MultipleChoiceMulti: {
|
||||
let values: TSurveyElementSummaryMultipleChoice["choices"] = [];
|
||||
|
||||
const otherOption = question.choices.find((choice) => choice.id === "other");
|
||||
const noneOption = question.choices.find((choice) => choice.id === "none");
|
||||
const otherOption = element.choices.find((choice) => choice.id === "other");
|
||||
const noneOption = element.choices.find((choice) => choice.id === "none");
|
||||
|
||||
const questionChoices = question.choices
|
||||
const elementChoices = element.choices
|
||||
.filter((choice) => choice.id !== "other" && choice.id !== "none")
|
||||
.map((choice) => getLocalizedValue(choice.label, "default"));
|
||||
|
||||
const choiceCountMap = questionChoices.reduce((acc: Record<string, number>, choice) => {
|
||||
const choiceCountMap = elementChoices.reduce((acc: Record<string, number>, choice) => {
|
||||
acc[choice] = 0;
|
||||
return acc;
|
||||
}, {});
|
||||
@@ -363,7 +386,7 @@ export const getQuestionSummary = async (
|
||||
const noneLabel = noneOption ? getLocalizedValue(noneOption.label, "default") : null;
|
||||
let noneCount = 0;
|
||||
|
||||
const otherValues: TSurveyQuestionSummaryMultipleChoice["choices"][number]["others"] = [];
|
||||
const otherValues: TSurveyElementSummaryMultipleChoice["choices"][number]["others"] = [];
|
||||
let totalSelectionCount = 0;
|
||||
let totalResponseCount = 0;
|
||||
responses.forEach((response) => {
|
||||
@@ -371,16 +394,16 @@ export const getQuestionSummary = async (
|
||||
|
||||
const answer =
|
||||
responseLanguageCode === "default"
|
||||
? response.data[question.id]
|
||||
: checkForI18n(response.data, question.id, survey, responseLanguageCode);
|
||||
? response.data[element.id]
|
||||
: checkForI18n(response.data, element.id, elements, responseLanguageCode);
|
||||
|
||||
let hasValidAnswer = false;
|
||||
|
||||
if (Array.isArray(answer) && question.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti) {
|
||||
if (Array.isArray(answer) && element.type === TSurveyElementTypeEnum.MultipleChoiceMulti) {
|
||||
answer.forEach((value) => {
|
||||
if (value) {
|
||||
totalSelectionCount++;
|
||||
if (questionChoices.includes(value)) {
|
||||
if (elementChoices.includes(value)) {
|
||||
choiceCountMap[value]++;
|
||||
} else if (noneLabel && value === noneLabel) {
|
||||
noneCount++;
|
||||
@@ -396,11 +419,11 @@ export const getQuestionSummary = async (
|
||||
});
|
||||
} else if (
|
||||
typeof answer === "string" &&
|
||||
question.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle
|
||||
element.type === TSurveyElementTypeEnum.MultipleChoiceSingle
|
||||
) {
|
||||
if (answer) {
|
||||
totalSelectionCount++;
|
||||
if (questionChoices.includes(answer)) {
|
||||
if (elementChoices.includes(answer)) {
|
||||
choiceCountMap[answer]++;
|
||||
} else if (noneLabel && answer === noneLabel) {
|
||||
noneCount++;
|
||||
@@ -452,8 +475,8 @@ export const getQuestionSummary = async (
|
||||
}
|
||||
|
||||
summary.push({
|
||||
type: question.type,
|
||||
question,
|
||||
type: element.type,
|
||||
element,
|
||||
responseCount: totalResponseCount,
|
||||
selectionCount: totalSelectionCount,
|
||||
choices: values,
|
||||
@@ -462,18 +485,18 @@ export const getQuestionSummary = async (
|
||||
values = [];
|
||||
break;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.PictureSelection: {
|
||||
let values: TSurveyQuestionSummaryPictureSelection["choices"] = [];
|
||||
case TSurveyElementTypeEnum.PictureSelection: {
|
||||
let values: TSurveyElementSummaryPictureSelection["choices"] = [];
|
||||
const choiceCountMap: Record<string, number> = {};
|
||||
|
||||
question.choices.forEach((choice) => {
|
||||
element.choices.forEach((choice) => {
|
||||
choiceCountMap[choice.id] = 0;
|
||||
});
|
||||
let totalResponseCount = 0;
|
||||
let totalSelectionCount = 0;
|
||||
|
||||
responses.forEach((response) => {
|
||||
const answer = response.data[question.id];
|
||||
const answer = response.data[element.id];
|
||||
if (Array.isArray(answer)) {
|
||||
totalResponseCount++;
|
||||
answer.forEach((value) => {
|
||||
@@ -483,7 +506,7 @@ export const getQuestionSummary = async (
|
||||
}
|
||||
});
|
||||
|
||||
question.choices.forEach((choice) => {
|
||||
element.choices.forEach((choice) => {
|
||||
values.push({
|
||||
id: choice.id,
|
||||
imageUrl: choice.imageUrl,
|
||||
@@ -496,8 +519,8 @@ export const getQuestionSummary = async (
|
||||
});
|
||||
|
||||
summary.push({
|
||||
type: question.type,
|
||||
question,
|
||||
type: element.type,
|
||||
element,
|
||||
responseCount: totalResponseCount,
|
||||
selectionCount: totalSelectionCount,
|
||||
choices: values,
|
||||
@@ -506,10 +529,10 @@ export const getQuestionSummary = async (
|
||||
values = [];
|
||||
break;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.Rating: {
|
||||
let values: TSurveyQuestionSummaryRating["choices"] = [];
|
||||
case TSurveyElementTypeEnum.Rating: {
|
||||
let values: TSurveyElementSummaryRating["choices"] = [];
|
||||
const choiceCountMap: Record<number, number> = {};
|
||||
const range = question.range;
|
||||
const range = element.range;
|
||||
|
||||
for (let i = 1; i <= range; i++) {
|
||||
choiceCountMap[i] = 0;
|
||||
@@ -520,12 +543,12 @@ export const getQuestionSummary = async (
|
||||
let dismissed = 0;
|
||||
|
||||
responses.forEach((response) => {
|
||||
const answer = response.data[question.id];
|
||||
const answer = response.data[element.id];
|
||||
if (typeof answer === "number") {
|
||||
totalResponseCount++;
|
||||
choiceCountMap[answer]++;
|
||||
totalRating += answer;
|
||||
} else if (response.ttc && response.ttc[question.id] > 0) {
|
||||
} else if (response.ttc && response.ttc[element.id] > 0) {
|
||||
dismissed++;
|
||||
}
|
||||
});
|
||||
@@ -558,8 +581,8 @@ export const getQuestionSummary = async (
|
||||
totalResponseCount > 0 ? Math.round((satisfiedCount / totalResponseCount) * 100) : 0;
|
||||
|
||||
summary.push({
|
||||
type: question.type,
|
||||
question,
|
||||
type: element.type,
|
||||
element,
|
||||
average: convertFloatTo2Decimal(totalRating / totalResponseCount) || 0,
|
||||
responseCount: totalResponseCount,
|
||||
choices: values,
|
||||
@@ -575,7 +598,7 @@ export const getQuestionSummary = async (
|
||||
values = [];
|
||||
break;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.NPS: {
|
||||
case TSurveyElementTypeEnum.NPS: {
|
||||
const data = {
|
||||
promoters: 0,
|
||||
passives: 0,
|
||||
@@ -592,7 +615,7 @@ export const getQuestionSummary = async (
|
||||
}
|
||||
|
||||
responses.forEach((response) => {
|
||||
const value = response.data[question.id];
|
||||
const value = response.data[element.id];
|
||||
if (typeof value === "number") {
|
||||
data.total++;
|
||||
scoreCountMap[value]++;
|
||||
@@ -603,7 +626,7 @@ export const getQuestionSummary = async (
|
||||
} else {
|
||||
data.detractors++;
|
||||
}
|
||||
} else if (response.ttc && response.ttc[question.id] > 0) {
|
||||
} else if (response.ttc && response.ttc[element.id] > 0) {
|
||||
data.total++;
|
||||
data.dismissed++;
|
||||
}
|
||||
@@ -622,8 +645,8 @@ export const getQuestionSummary = async (
|
||||
}));
|
||||
|
||||
summary.push({
|
||||
type: question.type,
|
||||
question,
|
||||
type: element.type,
|
||||
element,
|
||||
responseCount: data.total,
|
||||
total: data.total,
|
||||
score: data.score,
|
||||
@@ -647,14 +670,19 @@ export const getQuestionSummary = async (
|
||||
});
|
||||
break;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.CTA: {
|
||||
case TSurveyElementTypeEnum.CTA: {
|
||||
// Only calculate summary for CTA elements with external buttons (CTR tracking is only meaningful for external links)
|
||||
if (!element.buttonExternal) {
|
||||
break;
|
||||
}
|
||||
|
||||
const data = {
|
||||
clicked: 0,
|
||||
dismissed: 0,
|
||||
};
|
||||
|
||||
responses.forEach((response) => {
|
||||
const value = response.data[question.id];
|
||||
const value = response.data[element.id];
|
||||
if (value === "clicked") {
|
||||
data.clicked++;
|
||||
} else if (value === "dismissed") {
|
||||
@@ -663,12 +691,12 @@ export const getQuestionSummary = async (
|
||||
});
|
||||
|
||||
const totalResponses = data.clicked + data.dismissed;
|
||||
const idx = survey.questions.findIndex((q) => q.id === question.id);
|
||||
const idx = elements.findIndex((q) => q.id === element.id);
|
||||
const impressions = dropOff[idx].impressions;
|
||||
|
||||
summary.push({
|
||||
type: question.type,
|
||||
question,
|
||||
type: element.type,
|
||||
element,
|
||||
impressionCount: impressions,
|
||||
clickCount: data.clicked,
|
||||
skipCount: data.dismissed,
|
||||
@@ -680,17 +708,17 @@ export const getQuestionSummary = async (
|
||||
});
|
||||
break;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.Consent: {
|
||||
case TSurveyElementTypeEnum.Consent: {
|
||||
const data = {
|
||||
accepted: 0,
|
||||
dismissed: 0,
|
||||
};
|
||||
|
||||
responses.forEach((response) => {
|
||||
const value = response.data[question.id];
|
||||
const value = response.data[element.id];
|
||||
if (value === "accepted") {
|
||||
data.accepted++;
|
||||
} else if (response.ttc && response.ttc[question.id] > 0) {
|
||||
} else if (response.ttc && response.ttc[element.id] > 0) {
|
||||
data.dismissed++;
|
||||
}
|
||||
});
|
||||
@@ -698,8 +726,8 @@ export const getQuestionSummary = async (
|
||||
const totalResponses = data.accepted + data.dismissed;
|
||||
|
||||
summary.push({
|
||||
type: question.type,
|
||||
question,
|
||||
type: element.type,
|
||||
element,
|
||||
responseCount: totalResponses,
|
||||
accepted: {
|
||||
count: data.accepted,
|
||||
@@ -715,10 +743,10 @@ export const getQuestionSummary = async (
|
||||
|
||||
break;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.Date: {
|
||||
let values: TSurveyQuestionSummaryDate["samples"] = [];
|
||||
case TSurveyElementTypeEnum.Date: {
|
||||
let values: TSurveyElementSummaryDate["samples"] = [];
|
||||
responses.forEach((response) => {
|
||||
const answer = response.data[question.id];
|
||||
const answer = response.data[element.id];
|
||||
if (answer && typeof answer === "string") {
|
||||
values.push({
|
||||
id: response.id,
|
||||
@@ -731,8 +759,8 @@ export const getQuestionSummary = async (
|
||||
});
|
||||
|
||||
summary.push({
|
||||
type: question.type,
|
||||
question,
|
||||
type: element.type,
|
||||
element,
|
||||
responseCount: values.length,
|
||||
samples: values.slice(0, VALUES_LIMIT),
|
||||
});
|
||||
@@ -740,10 +768,10 @@ export const getQuestionSummary = async (
|
||||
values = [];
|
||||
break;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.FileUpload: {
|
||||
let values: TSurveyQuestionSummaryFileUpload["files"] = [];
|
||||
case TSurveyElementTypeEnum.FileUpload: {
|
||||
let values: TSurveyElementSummaryFileUpload["files"] = [];
|
||||
responses.forEach((response) => {
|
||||
const answer = response.data[question.id];
|
||||
const answer = response.data[element.id];
|
||||
if (Array.isArray(answer)) {
|
||||
values.push({
|
||||
id: response.id,
|
||||
@@ -756,8 +784,8 @@ export const getQuestionSummary = async (
|
||||
});
|
||||
|
||||
summary.push({
|
||||
type: question.type,
|
||||
question,
|
||||
type: element.type,
|
||||
element,
|
||||
responseCount: values.length,
|
||||
files: values.slice(0, VALUES_LIMIT),
|
||||
});
|
||||
@@ -765,25 +793,25 @@ export const getQuestionSummary = async (
|
||||
values = [];
|
||||
break;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.Cal: {
|
||||
case TSurveyElementTypeEnum.Cal: {
|
||||
const data = {
|
||||
booked: 0,
|
||||
skipped: 0,
|
||||
};
|
||||
|
||||
responses.forEach((response) => {
|
||||
const value = response.data[question.id];
|
||||
const value = response.data[element.id];
|
||||
if (value === "booked") {
|
||||
data.booked++;
|
||||
} else if (response.ttc && response.ttc[question.id] > 0) {
|
||||
} else if (response.ttc && response.ttc[element.id] > 0) {
|
||||
data.skipped++;
|
||||
}
|
||||
});
|
||||
const totalResponses = data.booked + data.skipped;
|
||||
|
||||
summary.push({
|
||||
type: question.type,
|
||||
question,
|
||||
type: element.type,
|
||||
element,
|
||||
responseCount: totalResponses,
|
||||
booked: {
|
||||
count: data.booked,
|
||||
@@ -798,9 +826,9 @@ export const getQuestionSummary = async (
|
||||
|
||||
break;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.Matrix: {
|
||||
const rows = question.rows.map((row) => getLocalizedValue(row.label, "default"));
|
||||
const columns = question.columns.map((column) => getLocalizedValue(column.label, "default"));
|
||||
case TSurveyElementTypeEnum.Matrix: {
|
||||
const rows = element.rows.map((row) => getLocalizedValue(row.label, "default"));
|
||||
const columns = element.columns.map((column) => getLocalizedValue(column.label, "default"));
|
||||
let totalResponseCount = 0;
|
||||
|
||||
// Initialize count object
|
||||
@@ -813,13 +841,13 @@ export const getQuestionSummary = async (
|
||||
}, {});
|
||||
|
||||
responses.forEach((response) => {
|
||||
const selectedResponses = response.data[question.id] as Record<string, string>;
|
||||
const selectedResponses = response.data[element.id] as Record<string, string>;
|
||||
const responseLanguageCode = getLanguageCode(survey.languages, response.language);
|
||||
if (selectedResponses) {
|
||||
totalResponseCount++;
|
||||
question.rows.forEach((row) => {
|
||||
element.rows.forEach((row) => {
|
||||
const localizedRow = getLocalizedValue(row.label, responseLanguageCode);
|
||||
const colValue = question.columns.find((column) => {
|
||||
const colValue = element.columns.find((column) => {
|
||||
return (
|
||||
getLocalizedValue(column.label, responseLanguageCode) === selectedResponses[localizedRow]
|
||||
);
|
||||
@@ -852,18 +880,17 @@ export const getQuestionSummary = async (
|
||||
});
|
||||
|
||||
summary.push({
|
||||
type: question.type,
|
||||
question,
|
||||
type: element.type,
|
||||
element,
|
||||
responseCount: totalResponseCount,
|
||||
data: matrixSummary,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.Address:
|
||||
case TSurveyQuestionTypeEnum.ContactInfo: {
|
||||
let values: TSurveyQuestionSummaryAddress["samples"] = [];
|
||||
case TSurveyElementTypeEnum.Address: {
|
||||
let values: TSurveyElementSummaryAddress["samples"] = [];
|
||||
responses.forEach((response) => {
|
||||
const answer = response.data[question.id];
|
||||
const answer = response.data[element.id];
|
||||
if (Array.isArray(answer) && answer.length > 0) {
|
||||
values.push({
|
||||
id: response.id,
|
||||
@@ -876,8 +903,8 @@ export const getQuestionSummary = async (
|
||||
});
|
||||
|
||||
summary.push({
|
||||
type: question.type as TSurveyQuestionTypeEnum.ContactInfo,
|
||||
question: question as TSurveyContactInfoQuestion,
|
||||
type: TSurveyElementTypeEnum.Address,
|
||||
element,
|
||||
responseCount: values.length,
|
||||
samples: values.slice(0, VALUES_LIMIT),
|
||||
});
|
||||
@@ -885,13 +912,39 @@ export const getQuestionSummary = async (
|
||||
values = [];
|
||||
break;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.Ranking: {
|
||||
let values: TSurveyQuestionSummaryRanking["choices"] = [];
|
||||
const questionChoices = question.choices.map((choice) => getLocalizedValue(choice.label, "default"));
|
||||
case TSurveyElementTypeEnum.ContactInfo: {
|
||||
let values: TSurveyElementSummaryContactInfo["samples"] = [];
|
||||
responses.forEach((response) => {
|
||||
const answer = response.data[element.id];
|
||||
if (Array.isArray(answer) && answer.length > 0) {
|
||||
values.push({
|
||||
id: response.id,
|
||||
updatedAt: response.updatedAt,
|
||||
value: answer,
|
||||
contact: response.contact,
|
||||
contactAttributes: response.contactAttributes,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
summary.push({
|
||||
type: TSurveyElementTypeEnum.ContactInfo,
|
||||
element,
|
||||
responseCount: values.length,
|
||||
samples: values.slice(0, VALUES_LIMIT),
|
||||
});
|
||||
|
||||
values = [];
|
||||
break;
|
||||
}
|
||||
case TSurveyElementTypeEnum.Ranking: {
|
||||
let values: TSurveyElementSummaryRanking["choices"] = [];
|
||||
const elementChoices = element.choices.map((choice) => getLocalizedValue(choice.label, "default"));
|
||||
let totalResponseCount = 0;
|
||||
const choiceRankSums: Record<string, number> = {};
|
||||
const choiceCountMap: Record<string, number> = {};
|
||||
questionChoices.forEach((choice) => {
|
||||
|
||||
elementChoices.forEach((choice: string) => {
|
||||
choiceRankSums[choice] = 0;
|
||||
choiceCountMap[choice] = 0;
|
||||
});
|
||||
@@ -901,14 +954,14 @@ export const getQuestionSummary = async (
|
||||
|
||||
const answer =
|
||||
responseLanguageCode === "default"
|
||||
? response.data[question.id]
|
||||
: checkForI18n(response.data, question.id, survey, responseLanguageCode);
|
||||
? response.data[element.id]
|
||||
: checkForI18n(response.data, element.id, elements, responseLanguageCode);
|
||||
|
||||
if (Array.isArray(answer)) {
|
||||
totalResponseCount++;
|
||||
answer.forEach((value, index) => {
|
||||
const ranking = index + 1; // Calculate ranking based on index
|
||||
if (questionChoices.includes(value)) {
|
||||
if (elementChoices.includes(value)) {
|
||||
choiceRankSums[value] += ranking;
|
||||
choiceCountMap[value]++;
|
||||
}
|
||||
@@ -916,7 +969,7 @@ export const getQuestionSummary = async (
|
||||
}
|
||||
});
|
||||
|
||||
questionChoices.forEach((choice) => {
|
||||
elementChoices.forEach((choice: string) => {
|
||||
const count = choiceCountMap[choice];
|
||||
const avgRanking = count > 0 ? choiceRankSums[choice] / count : 0;
|
||||
values.push({
|
||||
@@ -927,8 +980,8 @@ export const getQuestionSummary = async (
|
||||
});
|
||||
|
||||
summary.push({
|
||||
type: question.type,
|
||||
question,
|
||||
type: element.type,
|
||||
element,
|
||||
responseCount: totalResponseCount,
|
||||
choices: values,
|
||||
});
|
||||
@@ -939,7 +992,7 @@ export const getQuestionSummary = async (
|
||||
}
|
||||
|
||||
survey.hiddenFields?.fieldIds?.forEach((hiddenFieldId) => {
|
||||
let values: TSurveyQuestionSummaryHiddenFields["samples"] = [];
|
||||
let values: TSurveyElementSummaryHiddenFields["samples"] = [];
|
||||
responses.forEach((response) => {
|
||||
const answer = response.data[hiddenFieldId];
|
||||
if (answer && typeof answer === "string") {
|
||||
@@ -975,6 +1028,8 @@ export const getSurveySummary = reactCache(
|
||||
throw new ResourceNotFoundError("Survey", surveyId);
|
||||
}
|
||||
|
||||
const elements = getElementsFromBlocks(survey.blocks);
|
||||
|
||||
const batchSize = 5000;
|
||||
const hasFilter = Object.keys(filterCriteria ?? {}).length > 0;
|
||||
|
||||
@@ -1005,16 +1060,16 @@ export const getSurveySummary = reactCache(
|
||||
getQuotasSummary(surveyId),
|
||||
]);
|
||||
|
||||
const dropOff = getSurveySummaryDropOff(survey, responses, displayCount);
|
||||
const [meta, questionWiseSummary] = await Promise.all([
|
||||
const dropOff = getSurveySummaryDropOff(survey, elements, responses, displayCount);
|
||||
const [meta, elementSummary] = await Promise.all([
|
||||
getSurveySummaryMeta(responses, displayCount, quotas),
|
||||
getQuestionSummary(survey, responses, dropOff),
|
||||
getElementSummary(survey, elements, responses, dropOff),
|
||||
]);
|
||||
|
||||
return {
|
||||
meta,
|
||||
dropOff,
|
||||
summary: questionWiseSummary,
|
||||
summary: elementSummary,
|
||||
quotas,
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
+39
-27
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { constructToastMessage, convertFloatTo2Decimal, convertFloatToNDecimal } from "./utils";
|
||||
|
||||
describe("Utils Tests", () => {
|
||||
@@ -34,29 +35,40 @@ describe("Utils Tests", () => {
|
||||
type: "app",
|
||||
environmentId: "env1",
|
||||
status: "draft",
|
||||
questions: [
|
||||
blocks: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Q1" },
|
||||
required: false,
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "q2",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Q2" },
|
||||
required: false,
|
||||
choices: [{ id: "c1", label: { default: "Choice 1" } }],
|
||||
},
|
||||
{
|
||||
id: "q3",
|
||||
type: TSurveyQuestionTypeEnum.Matrix,
|
||||
headline: { default: "Q3" },
|
||||
required: false,
|
||||
rows: [{ id: "r1", label: { default: "Row 1" } }],
|
||||
columns: [{ id: "col1", label: { default: "Col 1" } }],
|
||||
id: "block1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Q1" },
|
||||
required: false,
|
||||
charLimit: { enabled: false },
|
||||
},
|
||||
{
|
||||
id: "q2",
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Q2" },
|
||||
required: false,
|
||||
choices: [{ id: "c1", label: { default: "Choice 1" } }],
|
||||
buttonLabel: { default: "Next" },
|
||||
shuffleOption: "none",
|
||||
},
|
||||
{
|
||||
id: "q3",
|
||||
type: TSurveyElementTypeEnum.Matrix,
|
||||
headline: { default: "Q3" },
|
||||
required: false,
|
||||
rows: [{ id: "r1", label: { default: "Row 1" } }],
|
||||
columns: [{ id: "col1", label: { default: "Col 1" } }],
|
||||
buttonLabel: { default: "Next" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
questions: [],
|
||||
triggers: [],
|
||||
recontactDays: null,
|
||||
autoClose: null,
|
||||
@@ -74,7 +86,7 @@ describe("Utils Tests", () => {
|
||||
|
||||
test("should construct message for matrix question type", () => {
|
||||
const message = constructToastMessage(
|
||||
TSurveyQuestionTypeEnum.Matrix,
|
||||
TSurveyElementTypeEnum.Matrix,
|
||||
"is",
|
||||
mockSurvey,
|
||||
"q3",
|
||||
@@ -95,7 +107,7 @@ describe("Utils Tests", () => {
|
||||
});
|
||||
|
||||
test("should construct message for matrix question type with array filterComboBoxValue", () => {
|
||||
const message = constructToastMessage(TSurveyQuestionTypeEnum.Matrix, "is", mockSurvey, "q3", mockT, [
|
||||
const message = constructToastMessage(TSurveyElementTypeEnum.Matrix, "is", mockSurvey, "q3", mockT, [
|
||||
"MatrixValue1",
|
||||
"MatrixValue2",
|
||||
]);
|
||||
@@ -114,7 +126,7 @@ describe("Utils Tests", () => {
|
||||
|
||||
test("should construct message when filterComboBoxValue is undefined (skipped)", () => {
|
||||
const message = constructToastMessage(
|
||||
TSurveyQuestionTypeEnum.OpenText,
|
||||
TSurveyElementTypeEnum.OpenText,
|
||||
"is skipped",
|
||||
mockSurvey,
|
||||
"q1",
|
||||
@@ -134,7 +146,7 @@ describe("Utils Tests", () => {
|
||||
|
||||
test("should construct message for non-matrix question with string filterComboBoxValue", () => {
|
||||
const message = constructToastMessage(
|
||||
TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||
"is",
|
||||
mockSurvey,
|
||||
"q2",
|
||||
@@ -156,7 +168,7 @@ describe("Utils Tests", () => {
|
||||
|
||||
test("should construct message for non-matrix question with array filterComboBoxValue", () => {
|
||||
const message = constructToastMessage(
|
||||
TSurveyQuestionTypeEnum.MultipleChoiceMulti,
|
||||
TSurveyElementTypeEnum.MultipleChoiceMulti,
|
||||
"includes all of",
|
||||
mockSurvey,
|
||||
"q2", // Assuming q2 can be multi for this test case logic
|
||||
@@ -178,7 +190,7 @@ describe("Utils Tests", () => {
|
||||
|
||||
test("should handle questionId not found in survey", () => {
|
||||
const message = constructToastMessage(
|
||||
TSurveyQuestionTypeEnum.OpenText,
|
||||
TSurveyElementTypeEnum.OpenText,
|
||||
"is",
|
||||
mockSurvey,
|
||||
"qNonExistent",
|
||||
|
||||
+11
-8
@@ -1,5 +1,7 @@
|
||||
import { TFunction } from "i18next";
|
||||
import { TSurvey, TSurveyQuestionId, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
|
||||
export const convertFloatToNDecimal = (num: number, N: number = 2) => {
|
||||
return Math.round(num * Math.pow(10, N)) / Math.pow(10, N);
|
||||
@@ -10,27 +12,28 @@ export const convertFloatTo2Decimal = (num: number) => {
|
||||
};
|
||||
|
||||
export const constructToastMessage = (
|
||||
questionType: TSurveyQuestionTypeEnum,
|
||||
elementType: TSurveyElementTypeEnum,
|
||||
filterValue: string,
|
||||
survey: TSurvey,
|
||||
questionId: TSurveyQuestionId,
|
||||
elementId: string,
|
||||
t: TFunction,
|
||||
filterComboBoxValue?: string | string[]
|
||||
) => {
|
||||
const questionIdx = survey.questions.findIndex((question) => question.id === questionId);
|
||||
if (questionType === "matrix") {
|
||||
const elements = getElementsFromBlocks(survey.blocks);
|
||||
const elementIdx = elements.findIndex((element) => element.id === elementId);
|
||||
if (elementType === "matrix") {
|
||||
return t("environments.surveys.summary.added_filter_for_responses_where_answer_to_question", {
|
||||
questionIdx: questionIdx + 1,
|
||||
questionIdx: elementIdx + 1,
|
||||
filterComboBoxValue: filterComboBoxValue?.toString() ?? "",
|
||||
filterValue,
|
||||
});
|
||||
} else if (filterComboBoxValue === undefined) {
|
||||
return t("environments.surveys.summary.added_filter_for_responses_where_answer_to_question_is_skipped", {
|
||||
questionIdx: questionIdx + 1,
|
||||
questionIdx: elementIdx + 1,
|
||||
});
|
||||
} else {
|
||||
return t("environments.surveys.summary.added_filter_for_responses_where_answer_to_question", {
|
||||
questionIdx: questionIdx + 1,
|
||||
questionIdx: elementIdx + 1,
|
||||
filterComboBoxValue: Array.isArray(filterComboBoxValue)
|
||||
? filterComboBoxValue.join(",")
|
||||
: filterComboBoxValue,
|
||||
|
||||
+2
-2
@@ -164,12 +164,12 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => {
|
||||
|
||||
const datePickerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const extracMetadataKeys = useCallback((obj, parentKey = "") => {
|
||||
const extractMetadataKeys = useCallback((obj, parentKey = "") => {
|
||||
let keys: string[] = [];
|
||||
|
||||
for (let key in obj) {
|
||||
if (typeof obj[key] === "object" && obj[key] !== null) {
|
||||
keys = keys.concat(extracMetadataKeys(obj[key], parentKey + key + " - "));
|
||||
keys = keys.concat(extractMetadataKeys(obj[key], parentKey + key + " - "));
|
||||
} else {
|
||||
keys.push(parentKey + key);
|
||||
}
|
||||
|
||||
+32
-29
@@ -4,8 +4,9 @@ import clsx from "clsx";
|
||||
import { ChevronDown, ChevronUp, X } from "lucide-react";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TI18nString, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
|
||||
import { TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ElementsComboBox";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { useClickOutside } from "@/lib/utils/hooks/useClickOutside";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -25,20 +26,29 @@ import {
|
||||
} from "@/modules/ui/components/dropdown-menu";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
|
||||
type QuestionFilterComboBoxProps = {
|
||||
const DEFAULT_LANGUAGE_CODE = "default";
|
||||
|
||||
// Helper to get localized option value
|
||||
const getOptionValue = (option: string | TI18nString): string => {
|
||||
return typeof option === "object" && option !== null
|
||||
? getLocalizedValue(option, DEFAULT_LANGUAGE_CODE)
|
||||
: option;
|
||||
};
|
||||
|
||||
type ElementFilterComboBoxProps = {
|
||||
filterOptions: (string | TI18nString)[] | undefined;
|
||||
filterComboBoxOptions: (string | TI18nString)[] | undefined;
|
||||
filterValue: string | undefined;
|
||||
filterComboBoxValue: string | string[] | undefined;
|
||||
onChangeFilterValue: (o: string) => void;
|
||||
onChangeFilterComboBoxValue: (o: string | string[]) => void;
|
||||
type?: TSurveyQuestionTypeEnum | Omit<OptionsType, OptionsType.QUESTIONS>;
|
||||
type?: TSurveyElementTypeEnum | Omit<OptionsType, OptionsType.ELEMENTS>;
|
||||
handleRemoveMultiSelect: (value: string[]) => void;
|
||||
disabled?: boolean;
|
||||
fieldId?: string;
|
||||
};
|
||||
|
||||
export const QuestionFilterComboBox = ({
|
||||
export const ElementFilterComboBox = ({
|
||||
filterComboBoxOptions,
|
||||
filterComboBoxValue,
|
||||
filterOptions,
|
||||
@@ -49,7 +59,7 @@ export const QuestionFilterComboBox = ({
|
||||
handleRemoveMultiSelect,
|
||||
disabled = false,
|
||||
fieldId,
|
||||
}: QuestionFilterComboBoxProps) => {
|
||||
}: ElementFilterComboBoxProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const commandRef = useRef(null);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
@@ -57,32 +67,28 @@ export const QuestionFilterComboBox = ({
|
||||
|
||||
useClickOutside(commandRef, () => setOpen(false));
|
||||
|
||||
const defaultLanguageCode = "default";
|
||||
|
||||
// Check if multiple selection is allowed
|
||||
const isMultiple = useMemo(
|
||||
() =>
|
||||
type === TSurveyQuestionTypeEnum.MultipleChoiceMulti ||
|
||||
type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ||
|
||||
type === TSurveyQuestionTypeEnum.PictureSelection ||
|
||||
(type === TSurveyQuestionTypeEnum.NPS && filterValue === "Includes either"),
|
||||
[type, filterValue]
|
||||
);
|
||||
const isMultiSelectType =
|
||||
type === TSurveyElementTypeEnum.MultipleChoiceMulti ||
|
||||
type === TSurveyElementTypeEnum.MultipleChoiceSingle ||
|
||||
type === TSurveyElementTypeEnum.PictureSelection;
|
||||
const isNPSIncludesEither = type === TSurveyElementTypeEnum.NPS && filterValue === "Includes either";
|
||||
const isMultiple = isMultiSelectType || isNPSIncludesEither;
|
||||
|
||||
// Filter out already selected options for multi-select
|
||||
const options = useMemo(() => {
|
||||
if (!isMultiple) return filterComboBoxOptions;
|
||||
|
||||
return filterComboBoxOptions?.filter((o) => {
|
||||
const optionValue = typeof o === "object" && o !== null ? getLocalizedValue(o, defaultLanguageCode) : o;
|
||||
const optionValue = getOptionValue(o);
|
||||
return !filterComboBoxValue?.includes(optionValue);
|
||||
});
|
||||
}, [isMultiple, filterComboBoxOptions, filterComboBoxValue, defaultLanguageCode]);
|
||||
}, [isMultiple, filterComboBoxOptions, filterComboBoxValue]);
|
||||
|
||||
// Disable combo box for NPS/Rating when Submitted/Skipped
|
||||
const isDisabledComboBox =
|
||||
(type === TSurveyQuestionTypeEnum.NPS || type === TSurveyQuestionTypeEnum.Rating) &&
|
||||
(filterValue === "Submitted" || filterValue === "Skipped");
|
||||
const isNPSOrRating = type === TSurveyElementTypeEnum.NPS || type === TSurveyElementTypeEnum.Rating;
|
||||
const isSubmittedOrSkipped = filterValue === "Submitted" || filterValue === "Skipped";
|
||||
const isDisabledComboBox = isNPSOrRating && isSubmittedOrSkipped;
|
||||
|
||||
// Check if this is a text input field (URL meta field)
|
||||
const isTextInputField = type === OptionsType.META && fieldId === "url";
|
||||
@@ -91,15 +97,14 @@ export const QuestionFilterComboBox = ({
|
||||
const filteredOptions = useMemo(
|
||||
() =>
|
||||
options?.filter((o) => {
|
||||
const optionValue =
|
||||
typeof o === "object" && o !== null ? getLocalizedValue(o, defaultLanguageCode) : o;
|
||||
const optionValue = getOptionValue(o);
|
||||
return optionValue.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
}),
|
||||
[options, searchQuery, defaultLanguageCode]
|
||||
[options, searchQuery]
|
||||
);
|
||||
|
||||
const handleCommandItemSelect = (o: string | TI18nString) => {
|
||||
const value = typeof o === "object" && o !== null ? getLocalizedValue(o, defaultLanguageCode) : o;
|
||||
const value = getOptionValue(o);
|
||||
|
||||
if (isMultiple) {
|
||||
const newValue = Array.isArray(filterComboBoxValue) ? [...filterComboBoxValue, value] : [value];
|
||||
@@ -202,8 +207,7 @@ export const QuestionFilterComboBox = ({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="bg-white">
|
||||
{filterOptions?.map((o, index) => {
|
||||
const optionValue =
|
||||
typeof o === "object" && o !== null ? getLocalizedValue(o, defaultLanguageCode) : o;
|
||||
const optionValue = getOptionValue(o);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={`${optionValue}-${index}`}
|
||||
@@ -274,8 +278,7 @@ export const QuestionFilterComboBox = ({
|
||||
<CommandEmpty>{t("common.no_result_found")}</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{filteredOptions?.map((o) => {
|
||||
const optionValue =
|
||||
typeof o === "object" && o !== null ? getLocalizedValue(o, defaultLanguageCode) : o;
|
||||
const optionValue = getOptionValue(o);
|
||||
return (
|
||||
<CommandItem
|
||||
key={optionValue}
|
||||
+29
-29
@@ -29,7 +29,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { Fragment, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { useClickOutside } from "@/lib/utils/hooks/useClickOutside";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
import { NetPromoterScoreIcon } from "@/modules/ui/components/icons";
|
||||
|
||||
export enum OptionsType {
|
||||
QUESTIONS = "Questions",
|
||||
ELEMENTS = "Elements",
|
||||
TAGS = "Tags",
|
||||
ATTRIBUTES = "Attributes",
|
||||
OTHERS = "Other Filters",
|
||||
@@ -53,37 +53,37 @@ export enum OptionsType {
|
||||
QUOTAS = "Quotas",
|
||||
}
|
||||
|
||||
export type QuestionOption = {
|
||||
export type ElementOption = {
|
||||
label: string;
|
||||
questionType?: TSurveyQuestionTypeEnum;
|
||||
elementType?: TSurveyElementTypeEnum;
|
||||
type: OptionsType;
|
||||
id: string;
|
||||
};
|
||||
export type QuestionOptions = {
|
||||
export type ElementOptions = {
|
||||
header: OptionsType;
|
||||
option: QuestionOption[];
|
||||
option: ElementOption[];
|
||||
};
|
||||
|
||||
interface QuestionComboBoxProps {
|
||||
options: QuestionOptions[];
|
||||
selected: Partial<QuestionOption>;
|
||||
onChangeValue: (option: QuestionOption) => void;
|
||||
interface ElementComboBoxProps {
|
||||
options: ElementOptions[];
|
||||
selected: Partial<ElementOption>;
|
||||
onChangeValue: (option: ElementOption) => void;
|
||||
}
|
||||
|
||||
const questionIcons = {
|
||||
// questions
|
||||
[TSurveyQuestionTypeEnum.OpenText]: MessageSquareTextIcon,
|
||||
[TSurveyQuestionTypeEnum.Rating]: StarIcon,
|
||||
[TSurveyQuestionTypeEnum.CTA]: MousePointerClickIcon,
|
||||
[TSurveyQuestionTypeEnum.MultipleChoiceMulti]: ListIcon,
|
||||
[TSurveyQuestionTypeEnum.MultipleChoiceSingle]: Rows3Icon,
|
||||
[TSurveyQuestionTypeEnum.NPS]: NetPromoterScoreIcon,
|
||||
[TSurveyQuestionTypeEnum.Consent]: CheckIcon,
|
||||
[TSurveyQuestionTypeEnum.PictureSelection]: ImageIcon,
|
||||
[TSurveyQuestionTypeEnum.Matrix]: GridIcon,
|
||||
[TSurveyQuestionTypeEnum.Ranking]: ListOrderedIcon,
|
||||
[TSurveyQuestionTypeEnum.Address]: HomeIcon,
|
||||
[TSurveyQuestionTypeEnum.ContactInfo]: ContactIcon,
|
||||
const elementIcons = {
|
||||
// elements
|
||||
[TSurveyElementTypeEnum.OpenText]: MessageSquareTextIcon,
|
||||
[TSurveyElementTypeEnum.Rating]: StarIcon,
|
||||
[TSurveyElementTypeEnum.CTA]: MousePointerClickIcon,
|
||||
[TSurveyElementTypeEnum.MultipleChoiceMulti]: ListIcon,
|
||||
[TSurveyElementTypeEnum.MultipleChoiceSingle]: Rows3Icon,
|
||||
[TSurveyElementTypeEnum.NPS]: NetPromoterScoreIcon,
|
||||
[TSurveyElementTypeEnum.Consent]: CheckIcon,
|
||||
[TSurveyElementTypeEnum.PictureSelection]: ImageIcon,
|
||||
[TSurveyElementTypeEnum.Matrix]: GridIcon,
|
||||
[TSurveyElementTypeEnum.Ranking]: ListOrderedIcon,
|
||||
[TSurveyElementTypeEnum.Address]: HomeIcon,
|
||||
[TSurveyElementTypeEnum.ContactInfo]: ContactIcon,
|
||||
|
||||
// attributes
|
||||
[OptionsType.ATTRIBUTES]: User,
|
||||
@@ -111,14 +111,14 @@ const questionIcons = {
|
||||
};
|
||||
|
||||
const getIcon = (type: string) => {
|
||||
const IconComponent = questionIcons[type];
|
||||
const IconComponent = elementIcons[type];
|
||||
return IconComponent ? <IconComponent className="h-5 w-5" strokeWidth={1.5} /> : null;
|
||||
};
|
||||
|
||||
const getIconBackground = (type: OptionsType | string): string => {
|
||||
const backgroundMap: Record<string, string> = {
|
||||
[OptionsType.ATTRIBUTES]: "bg-indigo-500",
|
||||
[OptionsType.QUESTIONS]: "bg-brand-dark",
|
||||
[OptionsType.ELEMENTS]: "bg-brand-dark",
|
||||
[OptionsType.TAGS]: "bg-indigo-500",
|
||||
[OptionsType.QUOTAS]: "bg-slate-500",
|
||||
};
|
||||
@@ -130,10 +130,10 @@ const getLabelClassName = (type: OptionsType | string, label?: string): string =
|
||||
return label === "os" || label === "url" ? "uppercase" : "capitalize";
|
||||
};
|
||||
|
||||
export const SelectedCommandItem = ({ label, questionType, type }: Partial<QuestionOption>) => {
|
||||
export const SelectedCommandItem = ({ label, elementType, type }: Partial<ElementOption>) => {
|
||||
const getDisplayIcon = () => {
|
||||
if (!type) return null;
|
||||
if (type === OptionsType.QUESTIONS && questionType) return getIcon(questionType);
|
||||
if (type === OptionsType.ELEMENTS && elementType) return getIcon(elementType);
|
||||
if (type === OptionsType.ATTRIBUTES) return getIcon(OptionsType.ATTRIBUTES);
|
||||
if (type === OptionsType.HIDDEN_FIELDS) return getIcon(OptionsType.HIDDEN_FIELDS);
|
||||
if ([OptionsType.META, OptionsType.OTHERS].includes(type) && label) return getIcon(label);
|
||||
@@ -158,7 +158,7 @@ export const SelectedCommandItem = ({ label, questionType, type }: Partial<Quest
|
||||
);
|
||||
};
|
||||
|
||||
export const QuestionsComboBox = ({ options, selected, onChangeValue }: QuestionComboBoxProps) => {
|
||||
export const ElementsComboBox = ({ options, selected, onChangeValue }: ElementComboBoxProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const commandRef = useRef(null);
|
||||
+41
-39
@@ -4,15 +4,17 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { ChevronDown, ChevronUp, Plus, TrashIcon } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TI18nString, TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
SelectedFilterValue,
|
||||
TResponseStatus,
|
||||
useResponseFilter,
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
||||
import { getSurveyFilterDataAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions";
|
||||
import { QuestionFilterComboBox } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox";
|
||||
import { generateQuestionAndFilterOptions } from "@/app/lib/surveys/surveys";
|
||||
import { ElementFilterComboBox } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ElementFilterComboBox";
|
||||
import { generateElementAndFilterOptions } from "@/app/lib/surveys/surveys";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/modules/ui/components/popover";
|
||||
@@ -23,11 +25,11 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/modules/ui/components/select";
|
||||
import { OptionsType, QuestionOption, QuestionsComboBox } from "./QuestionsComboBox";
|
||||
import { ElementOption, ElementsComboBox, OptionsType } from "./ElementsComboBox";
|
||||
|
||||
export type QuestionFilterOptions = {
|
||||
export type ElementFilterOptions = {
|
||||
type:
|
||||
| TSurveyQuestionTypeEnum
|
||||
| TSurveyElementTypeEnum
|
||||
| "Attributes"
|
||||
| "Tags"
|
||||
| "Languages"
|
||||
@@ -78,7 +80,7 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [filterValue, setFilterValue] = useState<SelectedFilterValue>(selectedFilter);
|
||||
|
||||
const getDefaultFilterValue = (option?: QuestionFilterOptions): string | undefined => {
|
||||
const getDefaultFilterValue = (option?: ElementFilterOptions): string | undefined => {
|
||||
if (!option || option.filterOptions.length === 0) return undefined;
|
||||
const firstOption = option.filterOptions[0];
|
||||
return typeof firstOption === "object" ? getLocalizedValue(firstOption, "default") : firstOption;
|
||||
@@ -93,7 +95,7 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
||||
if (!surveyFilterData?.data) return;
|
||||
|
||||
const { attributes, meta, environmentTags, hiddenFields, quotas } = surveyFilterData.data;
|
||||
const { questionFilterOptions, questionOptions } = generateQuestionAndFilterOptions(
|
||||
const { elementFilterOptions, elementOptions } = generateElementAndFilterOptions(
|
||||
survey,
|
||||
environmentTags,
|
||||
attributes,
|
||||
@@ -101,23 +103,23 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
||||
hiddenFields,
|
||||
quotas
|
||||
);
|
||||
setSelectedOptions({ questionFilterOptions, questionOptions });
|
||||
setSelectedOptions({ elementFilterOptions: elementFilterOptions, elementOptions: elementOptions });
|
||||
}
|
||||
};
|
||||
|
||||
handleInitialData();
|
||||
}, [isOpen, setSelectedOptions, survey]);
|
||||
|
||||
const handleOnChangeQuestionComboBoxValue = (value: QuestionOption, index: number) => {
|
||||
const matchingFilterOption = selectedOptions.questionFilterOptions.find(
|
||||
(q) => q.type === value.type || q.type === value.questionType
|
||||
const handleOnChangeElementComboBoxValue = (value: ElementOption, index: number) => {
|
||||
const matchingFilterOption = selectedOptions.elementFilterOptions.find(
|
||||
(q) => q.type === value.type || q.type === value.elementType
|
||||
);
|
||||
const defaultFilterValue = getDefaultFilterValue(matchingFilterOption);
|
||||
|
||||
if (filterValue.filter[index].questionType) {
|
||||
if (filterValue.filter[index].elementType) {
|
||||
// Create a new array and copy existing values from SelectedFilter
|
||||
filterValue.filter[index] = {
|
||||
questionType: value,
|
||||
elementType: value,
|
||||
filterType: {
|
||||
filterComboBoxValue: undefined,
|
||||
filterValue: defaultFilterValue,
|
||||
@@ -126,7 +128,7 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
||||
setFilterValue({ filter: [...filterValue.filter], responseStatus: filterValue.responseStatus });
|
||||
} else {
|
||||
// Update the existing value at the specified index
|
||||
filterValue.filter[index].questionType = value;
|
||||
filterValue.filter[index].elementType = value;
|
||||
filterValue.filter[index].filterType = {
|
||||
filterComboBoxValue: undefined,
|
||||
filterValue: defaultFilterValue,
|
||||
@@ -139,8 +141,8 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
||||
const clearItem = () => {
|
||||
setFilterValue({
|
||||
filter: filterValue.filter.filter((s) => {
|
||||
// keep the filter if questionType is selected and filterComboBoxValue is selected
|
||||
return s.questionType.hasOwnProperty("label") && s.filterType.filterComboBoxValue?.length;
|
||||
// keep the filter if elementType is selected and filterComboBoxValue is selected
|
||||
return s.elementType.hasOwnProperty("label") && s.filterType.filterComboBoxValue?.length;
|
||||
}),
|
||||
responseStatus: filterValue.responseStatus,
|
||||
});
|
||||
@@ -160,7 +162,7 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
||||
filter: [
|
||||
...filterValue.filter,
|
||||
{
|
||||
questionType: {},
|
||||
elementType: {},
|
||||
filterType: { filterComboBoxValue: undefined, filterValue: undefined },
|
||||
},
|
||||
],
|
||||
@@ -212,10 +214,10 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
||||
};
|
||||
|
||||
// remove the filter which has already been selected
|
||||
const questionComboBoxOptions = selectedOptions.questionOptions.map((q) => {
|
||||
const elementComboBoxOptions = selectedOptions.elementOptions.map((q) => {
|
||||
return {
|
||||
...q,
|
||||
option: q.option.filter((o) => !filterValue.filter.some((f) => f?.questionType?.id === o?.id)),
|
||||
option: q.option.filter((o) => !filterValue.filter.some((f) => f?.elementType?.id === o?.id)),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -278,41 +280,41 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
||||
<div className="flex w-full flex-wrap gap-3 md:flex-nowrap">
|
||||
<div
|
||||
className="grid w-full grid-cols-1 items-center gap-3 md:grid-cols-2"
|
||||
key={`${s.questionType.id}-${i}-${s.questionType.label}`}>
|
||||
<QuestionsComboBox
|
||||
key={`${s.questionType.label}-${i}-${s.questionType.id}`}
|
||||
options={questionComboBoxOptions}
|
||||
selected={s.questionType}
|
||||
onChangeValue={(value) => handleOnChangeQuestionComboBoxValue(value, i)}
|
||||
key={`${s.elementType.id}-${i}-${s.elementType.label}`}>
|
||||
<ElementsComboBox
|
||||
key={`${s.elementType.label}-${i}-${s.elementType.id}`}
|
||||
options={elementComboBoxOptions}
|
||||
selected={s.elementType}
|
||||
onChangeValue={(value) => handleOnChangeElementComboBoxValue(value, i)}
|
||||
/>
|
||||
<QuestionFilterComboBox
|
||||
key={`${s.questionType.id}-${i}`}
|
||||
<ElementFilterComboBox
|
||||
key={`${s.elementType.id}-${i}`}
|
||||
filterOptions={
|
||||
selectedOptions.questionFilterOptions.find(
|
||||
selectedOptions.elementFilterOptions.find(
|
||||
(q) =>
|
||||
(q.type === s.questionType.questionType || q.type === s.questionType.type) &&
|
||||
q.id === s.questionType.id
|
||||
(q.type === s.elementType.elementType || q.type === s.elementType.type) &&
|
||||
q.id === s.elementType.id
|
||||
)?.filterOptions
|
||||
}
|
||||
filterComboBoxOptions={
|
||||
selectedOptions.questionFilterOptions.find(
|
||||
selectedOptions.elementFilterOptions.find(
|
||||
(q) =>
|
||||
(q.type === s.questionType.questionType || q.type === s.questionType.type) &&
|
||||
q.id === s.questionType.id
|
||||
(q.type === s.elementType.elementType || q.type === s.elementType.type) &&
|
||||
q.id === s.elementType.id
|
||||
)?.filterComboBoxOptions
|
||||
}
|
||||
filterValue={filterValue.filter[i].filterType.filterValue}
|
||||
filterComboBoxValue={filterValue.filter[i].filterType.filterComboBoxValue}
|
||||
type={
|
||||
s?.questionType?.type === OptionsType.QUESTIONS
|
||||
? s?.questionType?.questionType
|
||||
: s?.questionType?.type
|
||||
s?.elementType?.type === OptionsType.ELEMENTS
|
||||
? s?.elementType?.elementType
|
||||
: s?.elementType?.type
|
||||
}
|
||||
fieldId={s?.questionType?.id}
|
||||
fieldId={s?.elementType?.id}
|
||||
handleRemoveMultiSelect={(value) => handleRemoveMultiSelect(value, i)}
|
||||
onChangeFilterComboBoxValue={(value) => handleOnChangeFilterComboBoxValue(value, i)}
|
||||
onChangeFilterValue={(value) => handleOnChangeFilterValue(value, i)}
|
||||
disabled={!s?.questionType?.label}
|
||||
disabled={!s?.elementType?.label}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-end gap-1 md:w-auto">
|
||||
|
||||
@@ -23,12 +23,8 @@ import {
|
||||
TIntegrationSlackCredential,
|
||||
} from "@formbricks/types/integration/slack";
|
||||
import { TResponse, TResponseMeta } from "@formbricks/types/responses";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyOpenTextQuestion,
|
||||
TSurveyPictureSelectionQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TPipelineInput } from "@/app/api/(internal)/pipeline/types/pipelines";
|
||||
import { writeData as airtableWriteData } from "@/lib/airtable/service";
|
||||
import { writeData as googleSheetWriteData } from "@/lib/googleSheet/service";
|
||||
@@ -101,33 +97,47 @@ const mockPipelineInput = {
|
||||
const mockSurvey = {
|
||||
id: surveyId,
|
||||
name: "Test Survey",
|
||||
questions: [
|
||||
blocks: [
|
||||
{
|
||||
id: questionId1,
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Question 1 {{recall:q2}}" },
|
||||
required: true,
|
||||
} as unknown as TSurveyOpenTextQuestion,
|
||||
{
|
||||
id: questionId2,
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceMulti,
|
||||
headline: { default: "Question 2" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "choice1", label: { default: "Choice 1" } },
|
||||
{ id: "choice2", label: { default: "Choice 2" } },
|
||||
id: "block1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
{
|
||||
id: questionId1,
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question 1 {{recall:q2}}" },
|
||||
required: true,
|
||||
inputType: "text",
|
||||
charLimit: 1000,
|
||||
subheader: { default: "" },
|
||||
placeholder: { default: "" },
|
||||
},
|
||||
{
|
||||
id: questionId2,
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceMulti,
|
||||
headline: { default: "Question 2" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "choice1", label: { default: "Choice 1" } },
|
||||
{ id: "choice2", label: { default: "Choice 2" } },
|
||||
],
|
||||
shuffleOption: "none",
|
||||
subheader: { default: "" },
|
||||
},
|
||||
{
|
||||
id: questionId3,
|
||||
type: TSurveyElementTypeEnum.PictureSelection,
|
||||
headline: { default: "Question 3" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "picChoice1", imageUrl: "http://image.com/1" },
|
||||
{ id: "picChoice2", imageUrl: "http://image.com/2" },
|
||||
],
|
||||
allowMultiple: false,
|
||||
subheader: { default: "" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: questionId3,
|
||||
type: TSurveyQuestionTypeEnum.PictureSelection,
|
||||
headline: { default: "Question 3" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "picChoice1", imageUrl: "http://image.com/1" },
|
||||
{ id: "picChoice2", imageUrl: "http://image.com/2" },
|
||||
],
|
||||
} as unknown as TSurveyPictureSelectionQuestion,
|
||||
],
|
||||
hiddenFields: {
|
||||
enabled: true,
|
||||
@@ -162,7 +172,7 @@ const mockAirtableIntegration: TIntegrationAirtable = {
|
||||
data: [
|
||||
{
|
||||
surveyId: surveyId,
|
||||
questionIds: [questionId1, questionId2],
|
||||
elementIds: [questionId1, questionId2],
|
||||
baseId: "base1",
|
||||
tableId: "table1",
|
||||
createdAt: new Date(),
|
||||
@@ -186,8 +196,8 @@ const mockGoogleSheetsIntegration: TIntegrationGoogleSheets = {
|
||||
surveyId: surveyId,
|
||||
spreadsheetId: "sheet1",
|
||||
spreadsheetName: "Sheet Name",
|
||||
questionIds: [questionId1],
|
||||
questions: "What is Q1?",
|
||||
elementIds: [questionId1],
|
||||
elements: "What is Q1?",
|
||||
createdAt: new Date("2024-01-01T00:00:00.000Z"),
|
||||
includeHiddenFields: false,
|
||||
includeMetadata: false,
|
||||
@@ -209,8 +219,8 @@ const mockSlackIntegration: TIntegrationSlack = {
|
||||
surveyId: surveyId,
|
||||
channelId: "channel1",
|
||||
channelName: "Channel 1",
|
||||
questionIds: [questionId1, questionId2, questionId3],
|
||||
questions: "Q1, Q2, Q3",
|
||||
elementIds: [questionId1, questionId2, questionId3],
|
||||
elements: "Q1, Q2, Q3",
|
||||
createdAt: new Date(),
|
||||
includeHiddenFields: true,
|
||||
includeMetadata: true,
|
||||
@@ -239,19 +249,19 @@ const mockNotionIntegration: TIntegrationNotion = {
|
||||
databaseName: "DB 1",
|
||||
mapping: [
|
||||
{
|
||||
question: { id: questionId1, name: "Question 1", type: TSurveyQuestionTypeEnum.OpenText },
|
||||
element: { id: questionId1, name: "Question 1", type: TSurveyQuestionTypeEnum.OpenText },
|
||||
column: { id: "col1", name: "Column 1", type: "rich_text" },
|
||||
},
|
||||
{
|
||||
question: { id: questionId3, name: "Question 3", type: TSurveyQuestionTypeEnum.PictureSelection },
|
||||
element: { id: questionId3, name: "Question 3", type: TSurveyQuestionTypeEnum.PictureSelection },
|
||||
column: { id: "col3", name: "Column 3", type: "url" },
|
||||
},
|
||||
{
|
||||
question: { id: "metadata", name: "Metadata", type: "metadata" },
|
||||
element: { id: "metadata", name: "Metadata", type: "metadata" },
|
||||
column: { id: "col_meta", name: "Metadata Col", type: "rich_text" },
|
||||
},
|
||||
{
|
||||
question: { id: "createdAt", name: "Created At", type: "createdAt" },
|
||||
element: { id: "createdAt", name: "Created At", type: "createdAt" },
|
||||
column: { id: "col_created", name: "Created Col", type: "date" },
|
||||
},
|
||||
],
|
||||
@@ -341,16 +351,14 @@ describe("handleIntegrations", () => {
|
||||
mockAirtableIntegration.config.key,
|
||||
mockAirtableIntegration.config.data[0],
|
||||
[
|
||||
[
|
||||
"Answer 1",
|
||||
"Choice 1, Choice 2",
|
||||
"Hidden Value",
|
||||
expectedMetadataString,
|
||||
"Variable Value",
|
||||
"2024-01-01 12:00",
|
||||
], // responses + hidden + meta + var + created
|
||||
["Question 1 {{recall:q2}}", "Question 2", hiddenFieldId, "Metadata", "Variable 1", "Created At"], // questions (raw headline for Airtable) + hidden + meta + var + created
|
||||
]
|
||||
"Answer 1",
|
||||
"Choice 1, Choice 2",
|
||||
"Hidden Value",
|
||||
expectedMetadataString,
|
||||
"Variable Value",
|
||||
"2024-01-01 12:00",
|
||||
], // responses + hidden + meta + var + created
|
||||
["Question 1 {{recall:q2}}", "Question 2", hiddenFieldId, "Metadata", "Variable 1", "Created At"] // elements (raw headline for Airtable) + hidden + meta + var + created
|
||||
);
|
||||
});
|
||||
|
||||
@@ -385,10 +393,8 @@ describe("handleIntegrations", () => {
|
||||
expect(googleSheetWriteData).toHaveBeenCalledWith(
|
||||
expectedIntegrationData,
|
||||
mockGoogleSheetsIntegration.config.data[0].spreadsheetId,
|
||||
[
|
||||
["Answer 1"], // responses
|
||||
["Question 1 {{recall:q2}}"], // questions (raw headline for Google Sheets)
|
||||
]
|
||||
["Answer 1"], // responses
|
||||
["Question 1 {{recall:q2}}"] // elements (raw headline for Google Sheets)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@ import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||
import { TIntegrationNotion, TIntegrationNotionConfigData } from "@formbricks/types/integration/notion";
|
||||
import { TIntegrationSlack } from "@formbricks/types/integration/slack";
|
||||
import { TResponseMeta } from "@formbricks/types/responses";
|
||||
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TResponseDataValue, TResponseMeta } from "@formbricks/types/responses";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { TPipelineInput } from "@/app/api/(internal)/pipeline/types/pipelines";
|
||||
import { writeData as airtableWriteData } from "@/lib/airtable/service";
|
||||
@@ -16,6 +17,7 @@ import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { writeData as writeNotionData } from "@/lib/notion/service";
|
||||
import { processResponseData } from "@/lib/responses";
|
||||
import { writeDataToSlack } from "@/lib/slack/service";
|
||||
import { getElementsFromBlocks } from "@/lib/survey/utils";
|
||||
import { getFormattedDateTimeString } from "@/lib/utils/datetime";
|
||||
import { parseRecallInfo } from "@/lib/utils/recall";
|
||||
import { truncateText } from "@/lib/utils/strings";
|
||||
@@ -42,33 +44,40 @@ const processDataForIntegration = async (
|
||||
includeMetadata: boolean,
|
||||
includeHiddenFields: boolean,
|
||||
includeCreatedAt: boolean,
|
||||
questionIds: string[]
|
||||
): Promise<string[][]> => {
|
||||
elementIds: string[]
|
||||
): Promise<{
|
||||
responses: string[];
|
||||
elements: string[];
|
||||
}> => {
|
||||
const ids =
|
||||
includeHiddenFields && survey.hiddenFields.fieldIds
|
||||
? [...questionIds, ...survey.hiddenFields.fieldIds]
|
||||
: questionIds;
|
||||
const values = await extractResponses(integrationType, data, ids, survey);
|
||||
? [...elementIds, ...survey.hiddenFields.fieldIds]
|
||||
: elementIds;
|
||||
const { responses, elements } = await extractResponses(integrationType, data, ids, survey);
|
||||
|
||||
if (includeMetadata) {
|
||||
values[0].push(convertMetaObjectToString(data.response.meta));
|
||||
values[1].push("Metadata");
|
||||
responses.push(convertMetaObjectToString(data.response.meta));
|
||||
elements.push("Metadata");
|
||||
}
|
||||
if (includeVariables) {
|
||||
survey.variables.forEach((variable) => {
|
||||
survey.variables?.forEach((variable) => {
|
||||
const value = data.response.variables[variable.id];
|
||||
if (value !== undefined) {
|
||||
values[0].push(String(data.response.variables[variable.id]));
|
||||
values[1].push(variable.name);
|
||||
responses.push(String(data.response.variables[variable.id]));
|
||||
elements.push(variable.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (includeCreatedAt) {
|
||||
const date = new Date(data.response.createdAt);
|
||||
values[0].push(`${getFormattedDateTimeString(date)}`);
|
||||
values[1].push("Created At");
|
||||
responses.push(`${getFormattedDateTimeString(date)}`);
|
||||
elements.push("Created At");
|
||||
}
|
||||
|
||||
return values;
|
||||
return {
|
||||
responses,
|
||||
elements,
|
||||
};
|
||||
};
|
||||
|
||||
export const handleIntegrations = async (
|
||||
@@ -131,9 +140,9 @@ const handleAirtableIntegration = async (
|
||||
!!element.includeMetadata,
|
||||
!!element.includeHiddenFields,
|
||||
!!element.includeCreatedAt,
|
||||
element.questionIds
|
||||
element.elementIds
|
||||
);
|
||||
await airtableWriteData(integration.config.key, element, values);
|
||||
await airtableWriteData(integration.config.key, element, values.responses, values.elements);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,14 +176,14 @@ const handleGoogleSheetsIntegration = async (
|
||||
!!element.includeMetadata,
|
||||
!!element.includeHiddenFields,
|
||||
!!element.includeCreatedAt,
|
||||
element.questionIds
|
||||
element.elementIds
|
||||
);
|
||||
const integrationData = structuredClone(integration);
|
||||
integrationData.config.data.forEach((data) => {
|
||||
data.createdAt = new Date(data.createdAt);
|
||||
});
|
||||
|
||||
await writeData(integrationData, element.spreadsheetId, values);
|
||||
await writeData(integrationData, element.spreadsheetId, values.responses, values.elements);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,9 +217,15 @@ const handleSlackIntegration = async (
|
||||
!!element.includeMetadata,
|
||||
!!element.includeHiddenFields,
|
||||
!!element.includeCreatedAt,
|
||||
element.questionIds
|
||||
element.elementIds
|
||||
);
|
||||
await writeDataToSlack(
|
||||
integration.config.key,
|
||||
element.channelId,
|
||||
values.responses,
|
||||
values.elements,
|
||||
survey?.name
|
||||
);
|
||||
await writeDataToSlack(integration.config.key, element.channelId, values, survey?.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,63 +242,81 @@ const handleSlackIntegration = async (
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to process a single element's response for integrations
|
||||
const processElementResponse = (
|
||||
element: ReturnType<typeof getElementsFromBlocks>[number],
|
||||
responseValue: TResponseDataValue
|
||||
): string => {
|
||||
if (responseValue === undefined) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (element.type === TSurveyElementTypeEnum.PictureSelection) {
|
||||
const selectedChoiceIds = responseValue as string[];
|
||||
return element.choices
|
||||
.filter((choice) => selectedChoiceIds.includes(choice.id))
|
||||
.map((choice) => choice.imageUrl)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
return processResponseData(responseValue);
|
||||
};
|
||||
|
||||
// Helper to create empty response object for non-slack integrations
|
||||
const createEmptyResponseObject = (responseData: Record<string, unknown>): Record<string, string> => {
|
||||
return Object.keys(responseData).reduce(
|
||||
(acc, key) => {
|
||||
acc[key] = "";
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
};
|
||||
|
||||
const extractResponses = async (
|
||||
integrationType: TIntegrationType,
|
||||
pipelineData: TPipelineInput,
|
||||
questionIds: string[],
|
||||
elementIds: string[],
|
||||
survey: TSurvey
|
||||
): Promise<string[][]> => {
|
||||
): Promise<{
|
||||
responses: string[];
|
||||
elements: string[];
|
||||
}> => {
|
||||
const responses: string[] = [];
|
||||
const questions: string[] = [];
|
||||
const elements: string[] = [];
|
||||
const surveyElements = getElementsFromBlocks(survey.blocks);
|
||||
const emptyResponseObject = createEmptyResponseObject(pipelineData.response.data);
|
||||
|
||||
for (const questionId of questionIds) {
|
||||
//check for hidden field Ids
|
||||
if (survey.hiddenFields.fieldIds?.includes(questionId)) {
|
||||
responses.push(processResponseData(pipelineData.response.data[questionId]));
|
||||
questions.push(questionId);
|
||||
continue;
|
||||
}
|
||||
const question = survey?.questions.find((q) => q.id === questionId);
|
||||
if (!question) {
|
||||
for (const elementId of elementIds) {
|
||||
// Check for hidden field Ids
|
||||
if (survey.hiddenFields.fieldIds?.includes(elementId)) {
|
||||
responses.push(processResponseData(pipelineData.response.data[elementId]));
|
||||
elements.push(elementId);
|
||||
continue;
|
||||
}
|
||||
|
||||
const responseValue = pipelineData.response.data[questionId];
|
||||
|
||||
if (responseValue !== undefined) {
|
||||
let answer: typeof responseValue;
|
||||
if (question.type === TSurveyQuestionTypeEnum.PictureSelection) {
|
||||
const selectedChoiceIds = responseValue as string[];
|
||||
answer = question?.choices
|
||||
.filter((choice) => selectedChoiceIds.includes(choice.id))
|
||||
.map((choice) => choice.imageUrl)
|
||||
.join("\n");
|
||||
} else {
|
||||
answer = responseValue;
|
||||
}
|
||||
|
||||
responses.push(processResponseData(answer));
|
||||
} else {
|
||||
responses.push("");
|
||||
const element = surveyElements.find((q) => q.id === elementId);
|
||||
if (!element) {
|
||||
continue;
|
||||
}
|
||||
// Create emptyResponseObject with same keys but empty string values
|
||||
const emptyResponseObject = Object.keys(pipelineData.response.data).reduce(
|
||||
(acc, key) => {
|
||||
acc[key] = "";
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
questions.push(
|
||||
|
||||
const responseValue = pipelineData.response.data[elementId];
|
||||
responses.push(processElementResponse(element, responseValue));
|
||||
|
||||
const responseDataForRecall =
|
||||
integrationType === "slack" ? pipelineData.response.data : emptyResponseObject;
|
||||
const variablesForRecall = integrationType === "slack" ? pipelineData.response.variables : {};
|
||||
|
||||
elements.push(
|
||||
parseRecallInfo(
|
||||
getTextContent(getLocalizedValue(question?.headline, "default")),
|
||||
integrationType === "slack" ? pipelineData.response.data : emptyResponseObject,
|
||||
integrationType === "slack" ? pipelineData.response.variables : {}
|
||||
getTextContent(getLocalizedValue(element.headline, "default")),
|
||||
responseDataForRecall,
|
||||
variablesForRecall
|
||||
) || ""
|
||||
);
|
||||
}
|
||||
|
||||
return [responses, questions];
|
||||
return { responses, elements };
|
||||
};
|
||||
|
||||
const handleNotionIntegration = async (
|
||||
@@ -321,32 +354,34 @@ const buildNotionPayloadProperties = (
|
||||
const properties: any = {};
|
||||
const responses = data.response.data;
|
||||
|
||||
const mappingQIds = mapping
|
||||
.filter((m) => m.question.type === TSurveyQuestionTypeEnum.PictureSelection)
|
||||
.map((m) => m.question.id);
|
||||
const surveyElements = getElementsFromBlocks(surveyData.blocks);
|
||||
|
||||
const mappingElementIds = mapping
|
||||
.filter((m) => m.element.type === TSurveyElementTypeEnum.PictureSelection)
|
||||
.map((m) => m.element.id);
|
||||
|
||||
Object.keys(responses).forEach((resp) => {
|
||||
if (mappingQIds.find((qId) => qId === resp)) {
|
||||
if (mappingElementIds.find((elementId) => elementId === resp)) {
|
||||
const selectedChoiceIds = responses[resp] as string[];
|
||||
const pictureQuestion = surveyData.questions.find((q) => q.id === resp);
|
||||
const pictureElement = surveyElements.find((el) => el.id === resp);
|
||||
|
||||
responses[resp] = (pictureQuestion as any)?.choices
|
||||
responses[resp] = (pictureElement as any)?.choices
|
||||
.filter((choice) => selectedChoiceIds.includes(choice.id))
|
||||
.map((choice) => choice.imageUrl);
|
||||
}
|
||||
});
|
||||
|
||||
mapping.forEach((map) => {
|
||||
if (map.question.id === "metadata") {
|
||||
if (map.element.id === "metadata") {
|
||||
properties[map.column.name] = {
|
||||
[map.column.type]: getValue(map.column.type, convertMetaObjectToString(data.response.meta)) || null,
|
||||
};
|
||||
} else if (map.question.id === "createdAt") {
|
||||
} else if (map.element.id === "createdAt") {
|
||||
properties[map.column.name] = {
|
||||
[map.column.type]: getValue(map.column.type, data.response.createdAt) || null,
|
||||
};
|
||||
} else {
|
||||
const value = responses[map.question.id];
|
||||
const value = responses[map.element.id];
|
||||
properties[map.column.name] = {
|
||||
[map.column.type]: getValue(map.column.type, value) || null,
|
||||
};
|
||||
|
||||
@@ -92,6 +92,7 @@ export const getEnvironmentStateData = async (environmentId: string): Promise<En
|
||||
welcomeCard: true,
|
||||
name: true,
|
||||
questions: true,
|
||||
blocks: true,
|
||||
variables: true,
|
||||
type: true,
|
||||
showLanguageSwitch: true,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { getResponse } from "@/lib/response/service";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/question";
|
||||
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element";
|
||||
import { createQuotaFullObject } from "@/modules/ee/quotas/lib/helpers";
|
||||
import { validateFileUploads } from "@/modules/storage/utils";
|
||||
import { updateResponseWithQuotaEvaluation } from "./lib/response";
|
||||
|
||||
@@ -6,6 +6,11 @@ import { handleErrorResponse } from "@/app/api/v1/auth";
|
||||
import { deleteSurvey } from "@/app/api/v1/management/surveys/[surveyId]/lib/surveys";
|
||||
import { checkFeaturePermissions } from "@/app/api/v1/management/surveys/lib/utils";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import {
|
||||
transformBlocksToQuestions,
|
||||
transformQuestionsToBlocks,
|
||||
validateSurveyInput,
|
||||
} from "@/app/lib/api/survey-transformation";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { TApiAuditLog, TApiKeyAuthentication, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
@@ -45,6 +50,22 @@ export const GET = withV1ApiWrapper({
|
||||
response: result.error,
|
||||
};
|
||||
}
|
||||
|
||||
const shouldTransformToQuestions =
|
||||
result.survey.blocks &&
|
||||
result.survey.blocks.length > 0 &&
|
||||
result.survey.blocks.every((block) => block.elements.length === 1);
|
||||
|
||||
if (shouldTransformToQuestions) {
|
||||
return {
|
||||
response: responses.successResponse({
|
||||
...result.survey,
|
||||
questions: transformBlocksToQuestions(result.survey.blocks, result.survey.endings),
|
||||
blocks: [],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
response: responses.successResponse(result.survey),
|
||||
};
|
||||
@@ -131,6 +152,23 @@ export const PUT = withV1ApiWrapper({
|
||||
};
|
||||
}
|
||||
|
||||
const validateResult = validateSurveyInput({ ...surveyUpdate, updateOnly: true });
|
||||
if (!validateResult.ok) {
|
||||
return {
|
||||
response: responses.badRequestResponse(validateResult.error.message),
|
||||
};
|
||||
}
|
||||
|
||||
const { hasQuestions } = validateResult.data;
|
||||
|
||||
if (hasQuestions) {
|
||||
surveyUpdate.blocks = transformQuestionsToBlocks(
|
||||
surveyUpdate.questions,
|
||||
surveyUpdate.endings || result.survey.endings
|
||||
);
|
||||
surveyUpdate.questions = [];
|
||||
}
|
||||
|
||||
const inputValidation = ZSurveyUpdateInput.safeParse({
|
||||
...result.survey,
|
||||
...surveyUpdate,
|
||||
@@ -155,6 +193,19 @@ export const PUT = withV1ApiWrapper({
|
||||
try {
|
||||
const updatedSurvey = await updateSurvey({ ...inputValidation.data, id: params.surveyId });
|
||||
auditLog.newObject = updatedSurvey;
|
||||
|
||||
if (hasQuestions) {
|
||||
const surveyWithQuestions = {
|
||||
...updatedSurvey,
|
||||
questions: transformBlocksToQuestions(updatedSurvey.blocks, updatedSurvey.endings),
|
||||
blocks: [],
|
||||
};
|
||||
|
||||
return {
|
||||
response: responses.successResponse(surveyWithQuestions),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
response: responses.successResponse(updatedSurvey),
|
||||
};
|
||||
|
||||
@@ -4,6 +4,11 @@ import { DatabaseError } from "@formbricks/types/errors";
|
||||
import { ZSurveyCreateInputWithEnvironmentId } from "@formbricks/types/surveys/types";
|
||||
import { checkFeaturePermissions } from "@/app/api/v1/management/surveys/lib/utils";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import {
|
||||
transformBlocksToQuestions,
|
||||
transformQuestionsToBlocks,
|
||||
validateSurveyInput,
|
||||
} from "@/app/lib/api/survey-transformation";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { TApiAuditLog, TApiKeyAuthentication, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
@@ -27,10 +32,30 @@ export const GET = withV1ApiWrapper({
|
||||
const environmentIds = authentication.environmentPermissions.map(
|
||||
(permission) => permission.environmentId
|
||||
);
|
||||
|
||||
const surveys = await getSurveys(environmentIds, limit, offset);
|
||||
|
||||
const surveysWithQuestions = surveys.map((survey) => {
|
||||
// If the survey has blocks and each block has ONLY ONE element, we can transform the blocks to questions
|
||||
// This is only for backwards compatibility with the older surveys
|
||||
const shouldTransformToQuestions =
|
||||
survey.blocks &&
|
||||
survey.blocks.length > 0 &&
|
||||
survey.blocks.every((block) => block.elements.length === 1);
|
||||
|
||||
if (shouldTransformToQuestions) {
|
||||
return {
|
||||
...survey,
|
||||
questions: transformBlocksToQuestions(survey.blocks, survey.endings),
|
||||
blocks: [],
|
||||
};
|
||||
}
|
||||
|
||||
return survey;
|
||||
});
|
||||
|
||||
return {
|
||||
response: responses.successResponse(surveys),
|
||||
response: responses.successResponse(surveysWithQuestions),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof DatabaseError) {
|
||||
@@ -63,6 +88,7 @@ export const POST = withV1ApiWrapper({
|
||||
response: responses.badRequestResponse("Malformed JSON input, please check your request body"),
|
||||
};
|
||||
}
|
||||
|
||||
const inputValidation = ZSurveyCreateInputWithEnvironmentId.safeParse(surveyInput);
|
||||
|
||||
if (!inputValidation.success) {
|
||||
@@ -92,6 +118,20 @@ export const POST = withV1ApiWrapper({
|
||||
|
||||
const surveyData = { ...inputValidation.data, environmentId };
|
||||
|
||||
const validateResult = validateSurveyInput(surveyData);
|
||||
if (!validateResult.ok) {
|
||||
return {
|
||||
response: responses.badRequestResponse(validateResult.error.message),
|
||||
};
|
||||
}
|
||||
|
||||
const { hasQuestions } = validateResult.data;
|
||||
|
||||
if (hasQuestions) {
|
||||
surveyData.blocks = transformQuestionsToBlocks(surveyData.questions, surveyData.endings || []);
|
||||
surveyData.questions = [];
|
||||
}
|
||||
|
||||
const featureCheckResult = await checkFeaturePermissions(surveyData, organization);
|
||||
if (featureCheckResult) {
|
||||
return {
|
||||
@@ -103,6 +143,18 @@ export const POST = withV1ApiWrapper({
|
||||
auditLog.targetId = survey.id;
|
||||
auditLog.newObject = survey;
|
||||
|
||||
if (hasQuestions) {
|
||||
const surveyWithQuestions = {
|
||||
...survey,
|
||||
questions: transformBlocksToQuestions(survey.blocks, survey.endings),
|
||||
blocks: [],
|
||||
};
|
||||
|
||||
return {
|
||||
response: responses.successResponse(surveyWithQuestions),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
response: responses.successResponse(survey),
|
||||
};
|
||||
|
||||
@@ -9,7 +9,8 @@ import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/question";
|
||||
import { getElementsFromBlocks } from "@/lib/survey/utils";
|
||||
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element";
|
||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { createQuotaFullObject } from "@/modules/ee/quotas/lib/helpers";
|
||||
import { createResponseWithQuotaEvaluation } from "./lib/response";
|
||||
@@ -90,7 +91,7 @@ export const POST = async (request: Request, context: Context): Promise<Response
|
||||
// Validate response data for "other" options exceeding character limit
|
||||
const otherResponseInvalidQuestionId = validateOtherOptionLengthForMultipleChoice({
|
||||
responseData: responseInputData.data,
|
||||
surveyQuestions: survey.questions,
|
||||
surveyQuestions: getElementsFromBlocks(survey.blocks),
|
||||
responseLanguage: responseInputData.language,
|
||||
});
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,501 @@
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { Result, err, ok } from "@formbricks/types/error-handlers";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { type TSurveyBlock, TSurveyBlockLogicAction } from "@formbricks/types/surveys/blocks";
|
||||
import { type TConditionGroup, type TSingleCondition } from "@formbricks/types/surveys/logic";
|
||||
import {
|
||||
type TSurveyEnding,
|
||||
TSurveyLogicAction,
|
||||
type TSurveyQuestion,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { isConditionGroup, isSingleCondition } from "@formbricks/types/surveys/validation";
|
||||
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
||||
|
||||
type Condition = TSingleCondition | TConditionGroup;
|
||||
|
||||
const conditionReferencesCTA = (
|
||||
condition: Condition | null | undefined,
|
||||
ctaElementId: string,
|
||||
operator?: string
|
||||
): boolean => {
|
||||
if (!condition) return false;
|
||||
|
||||
if (isSingleCondition(condition)) {
|
||||
if (condition.leftOperand.value === ctaElementId) {
|
||||
if (operator) {
|
||||
return condition.operator === operator;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isConditionGroup(condition)) {
|
||||
return condition.conditions.some((c) => conditionReferencesCTA(c, ctaElementId, operator));
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const removeCtaConditions = (
|
||||
conditionGroup: TConditionGroup,
|
||||
ctaElementId: string,
|
||||
operatorsToRemove: string[]
|
||||
): TConditionGroup | null => {
|
||||
const filteredConditions = conditionGroup.conditions.filter((condition) => {
|
||||
if (isSingleCondition(condition)) {
|
||||
if (condition.leftOperand.value === ctaElementId) {
|
||||
return !operatorsToRemove.includes(condition.operator);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isConditionGroup(condition)) {
|
||||
const cleaned = removeCtaConditions(condition, ctaElementId, operatorsToRemove);
|
||||
if (!cleaned || cleaned.conditions.length === 0) {
|
||||
return false;
|
||||
}
|
||||
Object.assign(condition, cleaned);
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (filteredConditions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...conditionGroup,
|
||||
conditions: filteredConditions,
|
||||
};
|
||||
};
|
||||
|
||||
const migrateCTAQuestion = (question: Record<string, unknown>): void => {
|
||||
if (question.type !== "cta") return;
|
||||
|
||||
const hasExternalButton = question.buttonExternal === true && Boolean(question.buttonUrl);
|
||||
|
||||
if (hasExternalButton) {
|
||||
if (question.buttonLabel) {
|
||||
question.ctaButtonLabel = question.buttonLabel;
|
||||
}
|
||||
question.buttonExternal = true;
|
||||
} else {
|
||||
delete question.buttonExternal;
|
||||
delete question.buttonUrl;
|
||||
}
|
||||
|
||||
delete question.buttonLabel;
|
||||
delete question.dismissButtonLabel;
|
||||
};
|
||||
|
||||
const cleanCTALogicFromQuestion = (
|
||||
question: Record<string, unknown>,
|
||||
ctaQuestions: Map<string, boolean>
|
||||
): void => {
|
||||
if (!question.logic || !Array.isArray(question.logic) || question.logic.length === 0) return;
|
||||
|
||||
const cleanedLogic: unknown[] = [];
|
||||
|
||||
question.logic.forEach((logicRule: { conditions: TConditionGroup; [key: string]: unknown }) => {
|
||||
let shouldKeepRule = true;
|
||||
let modifiedConditions = logicRule.conditions;
|
||||
|
||||
ctaQuestions.forEach((hasExternalButton, ctaId) => {
|
||||
if (!hasExternalButton) {
|
||||
if (conditionReferencesCTA(modifiedConditions, ctaId)) {
|
||||
const cleanedConditions = removeCtaConditions(modifiedConditions, ctaId, [
|
||||
"isClicked",
|
||||
"isSkipped",
|
||||
]);
|
||||
if (!cleanedConditions?.conditions || cleanedConditions.conditions.length === 0) {
|
||||
shouldKeepRule = false;
|
||||
} else {
|
||||
modifiedConditions = cleanedConditions;
|
||||
}
|
||||
}
|
||||
} else if (conditionReferencesCTA(modifiedConditions, ctaId, "isSkipped")) {
|
||||
const cleanedConditions = removeCtaConditions(modifiedConditions, ctaId, ["isSkipped"]);
|
||||
if (!cleanedConditions?.conditions || cleanedConditions.conditions.length === 0) {
|
||||
shouldKeepRule = false;
|
||||
} else {
|
||||
modifiedConditions = cleanedConditions;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldKeepRule) {
|
||||
cleanedLogic.push({
|
||||
...logicRule,
|
||||
conditions: modifiedConditions,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (cleanedLogic.length === 0) {
|
||||
delete question.logic;
|
||||
} else {
|
||||
question.logic = cleanedLogic;
|
||||
}
|
||||
};
|
||||
|
||||
const processCTAQuestions = (questions: Record<string, unknown>[]): void => {
|
||||
const ctaQuestions = new Map<string, boolean>();
|
||||
|
||||
questions.forEach((question) => {
|
||||
if (question.type === "cta") {
|
||||
const hasExternalButton = question.buttonExternal === true && Boolean(question.buttonUrl);
|
||||
ctaQuestions.set(question.id as string, hasExternalButton);
|
||||
}
|
||||
});
|
||||
|
||||
if (ctaQuestions.size === 0) return;
|
||||
|
||||
questions.forEach((question) => {
|
||||
migrateCTAQuestion(question);
|
||||
});
|
||||
|
||||
questions.forEach((question) => {
|
||||
cleanCTALogicFromQuestion(question, ctaQuestions);
|
||||
});
|
||||
};
|
||||
|
||||
const getBlockName = (questionIdx: number): string => {
|
||||
return `Block ${String(questionIdx + 1)}`;
|
||||
};
|
||||
|
||||
const updateLogicActions = (
|
||||
actions: TSurveyLogicAction[],
|
||||
questionIdToBlockId: Map<string, string>,
|
||||
endingIds: Set<string>
|
||||
): TSurveyBlockLogicAction[] => {
|
||||
return actions.map((action) => {
|
||||
if (action.objective === "jumpToQuestion") {
|
||||
const target = action.target;
|
||||
const blockId = questionIdToBlockId.get(target);
|
||||
|
||||
if (blockId) {
|
||||
return {
|
||||
...action,
|
||||
objective: "jumpToBlock",
|
||||
target: blockId,
|
||||
};
|
||||
}
|
||||
|
||||
if (endingIds.has(target)) {
|
||||
return {
|
||||
...action,
|
||||
objective: "jumpToBlock",
|
||||
target,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...action,
|
||||
objective: "jumpToBlock",
|
||||
target,
|
||||
};
|
||||
}
|
||||
|
||||
return action as TSurveyBlockLogicAction;
|
||||
});
|
||||
};
|
||||
|
||||
const updateLogicFallback = (
|
||||
fallback: string,
|
||||
questionIdToBlockId: Map<string, string>,
|
||||
endingIds: Set<string>
|
||||
): string | undefined => {
|
||||
const blockId = questionIdToBlockId.get(fallback);
|
||||
|
||||
if (blockId) {
|
||||
return blockId;
|
||||
}
|
||||
|
||||
if (endingIds.has(fallback)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const convertQuestionToElementType = (condition: Condition | null | undefined): Condition | null => {
|
||||
if (!condition) return null;
|
||||
|
||||
if (isSingleCondition(condition)) {
|
||||
const newCondition = { ...condition } as Record<string, unknown>;
|
||||
const leftOperand = { ...condition.leftOperand } as Record<string, unknown>;
|
||||
|
||||
if ((leftOperand.type as string) === "question") {
|
||||
leftOperand.type = "element";
|
||||
}
|
||||
newCondition.leftOperand = leftOperand;
|
||||
|
||||
if (condition.rightOperand) {
|
||||
const rightOperand = { ...condition.rightOperand } as Record<string, unknown>;
|
||||
if ((rightOperand.type as string) === "question") {
|
||||
rightOperand.type = "element";
|
||||
}
|
||||
newCondition.rightOperand = rightOperand;
|
||||
}
|
||||
|
||||
return newCondition as TSingleCondition;
|
||||
}
|
||||
|
||||
if (isConditionGroup(condition)) {
|
||||
const newConditionGroup: TConditionGroup = {
|
||||
...condition,
|
||||
conditions: condition.conditions.map((nestedCondition) => {
|
||||
const converted = convertQuestionToElementType(nestedCondition);
|
||||
return converted ?? nestedCondition;
|
||||
}),
|
||||
};
|
||||
|
||||
return newConditionGroup;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const convertElementToQuestionType = (condition: Condition | null | undefined): Condition | null => {
|
||||
if (!condition) return null;
|
||||
|
||||
if (isSingleCondition(condition)) {
|
||||
const newCondition = { ...condition } as Record<string, unknown>;
|
||||
const leftOperand = { ...condition.leftOperand } as Record<string, unknown>;
|
||||
|
||||
newCondition.leftOperand = {
|
||||
...leftOperand,
|
||||
type: leftOperand.type === "element" ? "question" : leftOperand.type,
|
||||
};
|
||||
|
||||
if (condition.rightOperand) {
|
||||
const rightOperand = { ...condition.rightOperand } as Record<string, unknown>;
|
||||
newCondition.rightOperand = {
|
||||
...rightOperand,
|
||||
type: rightOperand.type === "element" ? "question" : rightOperand.type,
|
||||
};
|
||||
}
|
||||
|
||||
return newCondition as TSingleCondition;
|
||||
}
|
||||
|
||||
if (isConditionGroup(condition)) {
|
||||
const newConditionGroup: TConditionGroup = {
|
||||
...condition,
|
||||
conditions: condition.conditions.map((nestedCondition) => {
|
||||
const converted = convertElementToQuestionType(nestedCondition);
|
||||
return converted ?? nestedCondition;
|
||||
}),
|
||||
};
|
||||
|
||||
return newConditionGroup;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const reverseLogicActions = (
|
||||
actions: TSurveyBlockLogicAction[],
|
||||
blockIdToQuestionId: Map<string, string>,
|
||||
endingIds: Set<string>
|
||||
): TSurveyLogicAction[] => {
|
||||
return actions.map((action) => {
|
||||
if (action.objective === "jumpToBlock") {
|
||||
const target = action.target;
|
||||
const questionId = blockIdToQuestionId.get(target);
|
||||
|
||||
if (questionId) {
|
||||
return {
|
||||
...action,
|
||||
objective: "jumpToQuestion",
|
||||
target: questionId,
|
||||
};
|
||||
}
|
||||
|
||||
if (endingIds.has(target)) {
|
||||
return {
|
||||
...action,
|
||||
objective: "jumpToQuestion",
|
||||
target,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...action,
|
||||
objective: "jumpToQuestion",
|
||||
target,
|
||||
};
|
||||
}
|
||||
|
||||
return action;
|
||||
});
|
||||
};
|
||||
|
||||
const reverseLogicFallback = (
|
||||
fallback: string,
|
||||
blockIdToQuestionId: Map<string, string>,
|
||||
endingIds: Set<string>
|
||||
): string | undefined => {
|
||||
const questionId = blockIdToQuestionId.get(fallback);
|
||||
|
||||
if (questionId) {
|
||||
return questionId;
|
||||
}
|
||||
|
||||
if (endingIds.has(fallback)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const transformQuestionsToBlocks = (
|
||||
questions: TSurveyQuestion[],
|
||||
endings: TSurveyEnding[] = []
|
||||
): TSurveyBlock[] => {
|
||||
if (questions.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const questionsCopy = structuredClone(questions);
|
||||
|
||||
processCTAQuestions(questionsCopy);
|
||||
|
||||
const endingIds = new Set<string>(endings.map((ending) => ending.id));
|
||||
|
||||
const questionIdToBlockId = new Map<string, string>();
|
||||
const blocks: Record<string, unknown>[] = [];
|
||||
|
||||
for (let i = 0; i < questionsCopy.length; i++) {
|
||||
const question = questionsCopy[i];
|
||||
|
||||
const blockId = createId();
|
||||
questionIdToBlockId.set(question.id as string, blockId);
|
||||
|
||||
const { logic, logicFallback, buttonLabel, backButtonLabel, ...baseElement } = question;
|
||||
|
||||
blocks.push({
|
||||
id: blockId,
|
||||
name: getBlockName(i),
|
||||
elements: [baseElement],
|
||||
buttonLabel,
|
||||
backButtonLabel,
|
||||
logic,
|
||||
logicFallback,
|
||||
});
|
||||
}
|
||||
|
||||
for (const block of blocks) {
|
||||
if (Array.isArray(block.logic) && block.logic.length > 0) {
|
||||
block.logic = block.logic.map(
|
||||
(item: { conditions: TConditionGroup; actions: TSurveyLogicAction[] }) => {
|
||||
const updatedConditions = convertQuestionToElementType(item.conditions);
|
||||
|
||||
if (!updatedConditions || !isConditionGroup(updatedConditions)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
conditions: updatedConditions,
|
||||
actions: updateLogicActions(item.actions, questionIdToBlockId, endingIds),
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof block.logicFallback === "string") {
|
||||
block.logicFallback = updateLogicFallback(block.logicFallback, questionIdToBlockId, endingIds);
|
||||
}
|
||||
}
|
||||
|
||||
return blocks as TSurveyBlock[];
|
||||
};
|
||||
|
||||
export const transformBlocksToQuestions = (
|
||||
blocks: TSurveyBlock[],
|
||||
endings: TSurveyEnding[] = []
|
||||
): TSurveyQuestion[] => {
|
||||
if (blocks.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const endingIds = new Set<string>(endings.map((ending) => ending.id));
|
||||
const questions: Record<string, unknown>[] = [];
|
||||
|
||||
const blockIdToQuestionId = blocks.reduce((acc, block) => {
|
||||
if (block.elements.length === 0) return acc;
|
||||
acc.set(block.id, block.elements[0].id);
|
||||
return acc;
|
||||
}, new Map<string, string>());
|
||||
|
||||
for (const block of blocks) {
|
||||
if (block.elements.length === 0) continue;
|
||||
|
||||
const element = { ...block.elements[0] } as Record<string, unknown>;
|
||||
|
||||
if (element.type === "cta" && element.ctaButtonLabel) {
|
||||
element.buttonLabel = element.ctaButtonLabel;
|
||||
}
|
||||
|
||||
if (Array.isArray(block.logic) && block.logic.length > 0) {
|
||||
element.logic = block.logic.map(
|
||||
(item: { id: string; conditions: TConditionGroup; actions: TSurveyBlockLogicAction[] }) => {
|
||||
const updatedConditions = convertElementToQuestionType(item.conditions);
|
||||
|
||||
if (!updatedConditions || !isConditionGroup(updatedConditions)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
conditions: updatedConditions,
|
||||
actions: reverseLogicActions(item.actions, blockIdToQuestionId, endingIds),
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (block.logicFallback) {
|
||||
element.logicFallback = reverseLogicFallback(block.logicFallback, blockIdToQuestionId, endingIds);
|
||||
}
|
||||
|
||||
if (block.buttonLabel) {
|
||||
element.buttonLabel = block.buttonLabel;
|
||||
}
|
||||
|
||||
if (block.backButtonLabel) {
|
||||
element.backButtonLabel = block.backButtonLabel;
|
||||
}
|
||||
|
||||
questions.push(element);
|
||||
}
|
||||
|
||||
return questions as TSurveyQuestion[];
|
||||
};
|
||||
|
||||
export const validateSurveyInput = (input: {
|
||||
questions?: TSurveyQuestion[];
|
||||
blocks?: TSurveyBlock[];
|
||||
updateOnly?: boolean;
|
||||
}): Result<{ hasQuestions: boolean; hasBlocks: boolean }, InvalidInputError> => {
|
||||
const hasQuestions = Boolean(input.questions && input.questions.length > 0);
|
||||
const hasBlocks = Boolean(input.blocks && input.blocks.length > 0);
|
||||
|
||||
if (hasQuestions && hasBlocks) {
|
||||
return err(
|
||||
new InvalidInputError(
|
||||
"Cannot provide both questions and blocks. Please provide only one of these fields."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasQuestions && !hasBlocks && !input.updateOnly) {
|
||||
return err(new InvalidInputError("Must provide either questions or blocks. Both cannot be empty."));
|
||||
}
|
||||
|
||||
return ok({ hasQuestions, hasBlocks });
|
||||
};
|
||||
@@ -0,0 +1,308 @@
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import type { TFunction } from "i18next";
|
||||
import type { TSurveyBlock, TSurveyBlockLogic } from "@formbricks/types/surveys/blocks";
|
||||
import type {
|
||||
TSurveyCTAElement,
|
||||
TSurveyConsentElement,
|
||||
TSurveyElement,
|
||||
TSurveyMultipleChoiceElement,
|
||||
TSurveyNPSElement,
|
||||
TSurveyOpenTextElement,
|
||||
TSurveyOpenTextElementInputType,
|
||||
TSurveyRatingElement,
|
||||
} from "@formbricks/types/surveys/elements";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import type { TShuffleOption } from "@formbricks/types/surveys/types";
|
||||
import { createI18nString } from "@/lib/i18n/utils";
|
||||
|
||||
const getDefaultButtonLabel = (label: string | undefined, t: TFunction) =>
|
||||
createI18nString(label || t("common.next"), []);
|
||||
|
||||
const getDefaultBackButtonLabel = (label: string | undefined, t: TFunction) =>
|
||||
createI18nString(label || t("common.back"), []);
|
||||
|
||||
export const buildMultipleChoiceElement = ({
|
||||
id,
|
||||
headline,
|
||||
type,
|
||||
subheader,
|
||||
choices,
|
||||
choiceIds,
|
||||
shuffleOption,
|
||||
required,
|
||||
containsOther = false,
|
||||
}: {
|
||||
id?: string;
|
||||
headline: string;
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceMulti | TSurveyElementTypeEnum.MultipleChoiceSingle;
|
||||
subheader?: string;
|
||||
choices: string[];
|
||||
choiceIds?: string[];
|
||||
shuffleOption?: TShuffleOption;
|
||||
required?: boolean;
|
||||
containsOther?: boolean;
|
||||
}): TSurveyMultipleChoiceElement => {
|
||||
return {
|
||||
id: id ?? createId(),
|
||||
type,
|
||||
subheader: subheader ? createI18nString(subheader, []) : undefined,
|
||||
headline: createI18nString(headline, []),
|
||||
choices: choices.map((choice, index) => {
|
||||
const isLastIndex = index === choices.length - 1;
|
||||
let choiceId: string;
|
||||
if (containsOther && isLastIndex) {
|
||||
choiceId = "other";
|
||||
} else if (choiceIds) {
|
||||
choiceId = choiceIds[index];
|
||||
} else {
|
||||
choiceId = createId();
|
||||
}
|
||||
return { id: choiceId, label: createI18nString(choice, []) };
|
||||
}),
|
||||
shuffleOption: shuffleOption || "none",
|
||||
required: required ?? false,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildOpenTextElement = ({
|
||||
id,
|
||||
headline,
|
||||
subheader,
|
||||
placeholder,
|
||||
inputType,
|
||||
required,
|
||||
longAnswer,
|
||||
}: {
|
||||
id?: string;
|
||||
headline: string;
|
||||
subheader?: string;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
inputType: TSurveyOpenTextElementInputType;
|
||||
longAnswer?: boolean;
|
||||
}): TSurveyOpenTextElement => {
|
||||
return {
|
||||
id: id ?? createId(),
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
inputType,
|
||||
subheader: subheader ? createI18nString(subheader, []) : undefined,
|
||||
placeholder: placeholder ? createI18nString(placeholder, []) : undefined,
|
||||
headline: createI18nString(headline, []),
|
||||
required: required ?? false,
|
||||
longAnswer,
|
||||
charLimit: {
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const buildRatingElement = ({
|
||||
id,
|
||||
headline,
|
||||
subheader,
|
||||
scale,
|
||||
range,
|
||||
lowerLabel,
|
||||
upperLabel,
|
||||
required,
|
||||
isColorCodingEnabled = false,
|
||||
}: {
|
||||
id?: string;
|
||||
headline: string;
|
||||
scale: TSurveyRatingElement["scale"];
|
||||
range: TSurveyRatingElement["range"];
|
||||
lowerLabel?: string;
|
||||
upperLabel?: string;
|
||||
subheader?: string;
|
||||
required?: boolean;
|
||||
isColorCodingEnabled?: boolean;
|
||||
}): TSurveyRatingElement => {
|
||||
return {
|
||||
id: id ?? createId(),
|
||||
type: TSurveyElementTypeEnum.Rating,
|
||||
subheader: subheader ? createI18nString(subheader, []) : undefined,
|
||||
headline: createI18nString(headline, []),
|
||||
scale,
|
||||
range,
|
||||
required: required ?? false,
|
||||
isColorCodingEnabled,
|
||||
lowerLabel: lowerLabel ? createI18nString(lowerLabel, []) : undefined,
|
||||
upperLabel: upperLabel ? createI18nString(upperLabel, []) : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildConsentElement = ({
|
||||
id,
|
||||
headline,
|
||||
subheader,
|
||||
label,
|
||||
required,
|
||||
}: {
|
||||
id?: string;
|
||||
headline: string;
|
||||
subheader: string;
|
||||
required?: boolean;
|
||||
label: string;
|
||||
}): TSurveyConsentElement => {
|
||||
return {
|
||||
id: id ?? createId(),
|
||||
type: TSurveyElementTypeEnum.Consent,
|
||||
subheader: createI18nString(subheader, []),
|
||||
headline: createI18nString(headline, []),
|
||||
required: required ?? false,
|
||||
label: createI18nString(label, []),
|
||||
};
|
||||
};
|
||||
|
||||
export const buildCTAElement = ({
|
||||
id,
|
||||
headline,
|
||||
subheader,
|
||||
buttonExternal,
|
||||
required,
|
||||
ctaButtonLabel,
|
||||
buttonUrl,
|
||||
}: {
|
||||
id?: string;
|
||||
headline: string;
|
||||
buttonExternal?: boolean;
|
||||
subheader: string;
|
||||
required?: boolean;
|
||||
ctaButtonLabel?: string;
|
||||
buttonUrl?: string;
|
||||
}): TSurveyCTAElement => {
|
||||
return {
|
||||
id: id ?? createId(),
|
||||
type: TSurveyElementTypeEnum.CTA,
|
||||
subheader: createI18nString(subheader, []),
|
||||
headline: createI18nString(headline, []),
|
||||
ctaButtonLabel: ctaButtonLabel ? createI18nString(ctaButtonLabel, []) : undefined,
|
||||
required: required ?? false,
|
||||
buttonExternal: buttonExternal ?? false,
|
||||
buttonUrl,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildNPSElement = ({
|
||||
id,
|
||||
headline,
|
||||
subheader,
|
||||
lowerLabel,
|
||||
upperLabel,
|
||||
required,
|
||||
isColorCodingEnabled = false,
|
||||
}: {
|
||||
id?: string;
|
||||
headline: string;
|
||||
subheader?: string;
|
||||
lowerLabel?: string;
|
||||
upperLabel?: string;
|
||||
required?: boolean;
|
||||
isColorCodingEnabled?: boolean;
|
||||
}): TSurveyNPSElement => {
|
||||
return {
|
||||
id: id ?? createId(),
|
||||
type: TSurveyElementTypeEnum.NPS,
|
||||
subheader: subheader ? createI18nString(subheader, []) : undefined,
|
||||
headline: createI18nString(headline, []),
|
||||
required: required ?? false,
|
||||
isColorCodingEnabled,
|
||||
lowerLabel: lowerLabel ? createI18nString(lowerLabel, []) : undefined,
|
||||
upperLabel: upperLabel ? createI18nString(upperLabel, []) : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
// Helper function to create block-level jump logic based on operator
|
||||
export const createBlockJumpLogic = (
|
||||
sourceElementId: string,
|
||||
targetBlockId: string,
|
||||
operator: "isSkipped" | "isSubmitted" | "isClicked"
|
||||
): TSurveyBlockLogic => ({
|
||||
id: createId(),
|
||||
conditions: {
|
||||
id: createId(),
|
||||
connector: "and",
|
||||
conditions: [
|
||||
{
|
||||
id: createId(),
|
||||
leftOperand: {
|
||||
value: sourceElementId,
|
||||
type: "element",
|
||||
},
|
||||
operator: operator,
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: createId(),
|
||||
objective: "jumpToBlock",
|
||||
target: targetBlockId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Helper function to create block-level jump logic based on choice selection
|
||||
export const createBlockChoiceJumpLogic = (
|
||||
sourceElementId: string,
|
||||
choiceId: string | number,
|
||||
targetBlockId: string
|
||||
): TSurveyBlockLogic => ({
|
||||
id: createId(),
|
||||
conditions: {
|
||||
id: createId(),
|
||||
connector: "and",
|
||||
conditions: [
|
||||
{
|
||||
id: createId(),
|
||||
leftOperand: {
|
||||
value: sourceElementId,
|
||||
type: "element",
|
||||
},
|
||||
operator: "equals",
|
||||
rightOperand: {
|
||||
type: "static",
|
||||
value: choiceId,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: createId(),
|
||||
objective: "jumpToBlock",
|
||||
target: targetBlockId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Block builder function
|
||||
export const buildBlock = ({
|
||||
id,
|
||||
name,
|
||||
elements,
|
||||
logic,
|
||||
logicFallback,
|
||||
buttonLabel,
|
||||
backButtonLabel,
|
||||
t,
|
||||
}: {
|
||||
id?: string;
|
||||
name: string;
|
||||
elements: TSurveyElement[];
|
||||
logic?: TSurveyBlockLogic[];
|
||||
logicFallback?: string;
|
||||
buttonLabel?: string;
|
||||
backButtonLabel?: string;
|
||||
t: TFunction;
|
||||
}): TSurveyBlock => {
|
||||
return {
|
||||
id: id ?? createId(),
|
||||
name,
|
||||
elements,
|
||||
logic,
|
||||
logicFallback,
|
||||
buttonLabel: buttonLabel ? getDefaultButtonLabel(buttonLabel, t) : undefined,
|
||||
backButtonLabel: backButtonLabel ? getDefaultBackButtonLabel(backButtonLabel, t) : undefined,
|
||||
};
|
||||
};
|
||||
@@ -1,15 +1,6 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { TShuffleOption, TSurveyLogic, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
buildCTAQuestion,
|
||||
buildConsentQuestion,
|
||||
buildMultipleChoiceQuestion,
|
||||
buildNPSQuestion,
|
||||
buildOpenTextQuestion,
|
||||
buildRatingQuestion,
|
||||
buildSurvey,
|
||||
createChoiceJumpLogic,
|
||||
createJumpLogic,
|
||||
getDefaultEndingCard,
|
||||
getDefaultSurveyPreset,
|
||||
getDefaultWelcomeCard,
|
||||
@@ -19,595 +10,81 @@ import {
|
||||
const mockT = (props: any): string => (typeof props === "string" ? props : props.key);
|
||||
|
||||
describe("Survey Builder", () => {
|
||||
describe("buildMultipleChoiceQuestion", () => {
|
||||
test("creates a single choice question with required fields", () => {
|
||||
const question = buildMultipleChoiceQuestion({
|
||||
headline: "Test Question",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
choices: ["Option 1", "Option 2", "Option 3"],
|
||||
t: mockT,
|
||||
});
|
||||
|
||||
expect(question).toMatchObject({
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Test Question" },
|
||||
choices: expect.arrayContaining([
|
||||
expect.objectContaining({ label: { default: "Option 1" } }),
|
||||
expect.objectContaining({ label: { default: "Option 2" } }),
|
||||
expect.objectContaining({ label: { default: "Option 3" } }),
|
||||
]),
|
||||
buttonLabel: { default: "common.next" },
|
||||
backButtonLabel: { default: "common.back" },
|
||||
shuffleOption: "none",
|
||||
required: false,
|
||||
});
|
||||
expect(question.choices.length).toBe(3);
|
||||
expect(question.id).toBeDefined();
|
||||
describe("Helper Functions", () => {
|
||||
test("getDefaultSurveyPreset returns expected default survey preset", () => {
|
||||
const preset = getDefaultSurveyPreset(mockT);
|
||||
expect(preset.name).toBe("New Survey");
|
||||
// test welcomeCard and endings
|
||||
expect(preset.welcomeCard).toHaveProperty("headline");
|
||||
expect(preset.endings).toHaveLength(1);
|
||||
expect(preset.endings[0]).toHaveProperty("headline");
|
||||
expect(preset.hiddenFields).toEqual(hiddenFieldsDefault);
|
||||
expect(preset.blocks).toEqual([]);
|
||||
});
|
||||
|
||||
test("creates a multiple choice question with provided ID", () => {
|
||||
const customId = "custom-id-123";
|
||||
const question = buildMultipleChoiceQuestion({
|
||||
id: customId,
|
||||
headline: "Test Question",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceMulti,
|
||||
choices: ["Option 1", "Option 2"],
|
||||
t: mockT,
|
||||
test("getDefaultWelcomeCard returns expected welcome card", () => {
|
||||
const welcomeCard = getDefaultWelcomeCard(mockT);
|
||||
expect(welcomeCard).toMatchObject({
|
||||
enabled: false,
|
||||
headline: { default: "templates.default_welcome_card_headline" },
|
||||
timeToFinish: false,
|
||||
showResponseCount: false,
|
||||
});
|
||||
|
||||
expect(question.id).toBe(customId);
|
||||
expect(question.type).toBe(TSurveyQuestionTypeEnum.MultipleChoiceMulti);
|
||||
// Check that the welcome card is properly structured
|
||||
expect(welcomeCard).toHaveProperty("enabled");
|
||||
expect(welcomeCard).toHaveProperty("headline");
|
||||
expect(welcomeCard).toHaveProperty("showResponseCount");
|
||||
expect(welcomeCard).toHaveProperty("timeToFinish");
|
||||
});
|
||||
|
||||
test("handles 'other' option correctly", () => {
|
||||
const choices = ["Option 1", "Option 2", "Other"];
|
||||
const question = buildMultipleChoiceQuestion({
|
||||
headline: "Test Question",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
choices,
|
||||
containsOther: true,
|
||||
t: mockT,
|
||||
test("getDefaultEndingCard returns expected ending card", () => {
|
||||
const languages: string[] = [];
|
||||
const endingCard = getDefaultEndingCard(languages, mockT);
|
||||
expect(endingCard).toMatchObject({
|
||||
type: "endScreen",
|
||||
headline: { default: "templates.default_ending_card_headline" },
|
||||
subheader: { default: "templates.default_ending_card_subheader" },
|
||||
});
|
||||
|
||||
expect(question.choices.length).toBe(3);
|
||||
expect(question.choices[2].id).toBe("other");
|
||||
expect(endingCard.id).toBeDefined();
|
||||
expect(endingCard).toHaveProperty("buttonLabel");
|
||||
expect(endingCard).toHaveProperty("buttonLink");
|
||||
});
|
||||
|
||||
test("uses provided choice IDs when available", () => {
|
||||
const choiceIds = ["id1", "id2", "id3"];
|
||||
const question = buildMultipleChoiceQuestion({
|
||||
headline: "Test Question",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
choices: ["Option 1", "Option 2", "Option 3"],
|
||||
choiceIds,
|
||||
t: mockT,
|
||||
test("hiddenFieldsDefault has expected structure", () => {
|
||||
expect(hiddenFieldsDefault).toMatchObject({
|
||||
enabled: true,
|
||||
fieldIds: [],
|
||||
});
|
||||
|
||||
expect(question.choices[0].id).toBe(choiceIds[0]);
|
||||
expect(question.choices[1].id).toBe(choiceIds[1]);
|
||||
expect(question.choices[2].id).toBe(choiceIds[2]);
|
||||
});
|
||||
|
||||
test("applies all optional parameters correctly", () => {
|
||||
const logic: TSurveyLogic[] = [
|
||||
{
|
||||
id: "logic-1",
|
||||
conditions: {
|
||||
id: "cond-1",
|
||||
connector: "and",
|
||||
conditions: [],
|
||||
},
|
||||
actions: [],
|
||||
},
|
||||
];
|
||||
test("buildSurvey returns built survey with overridden preset properties", () => {
|
||||
const config = {
|
||||
name: "Custom Survey",
|
||||
role: "productManager" as const,
|
||||
industries: ["saas" as const],
|
||||
channels: ["link" as const],
|
||||
description: "A custom survey description",
|
||||
blocks: [],
|
||||
endings: [getDefaultEndingCard([], mockT)],
|
||||
hiddenFields: hiddenFieldsDefault,
|
||||
};
|
||||
|
||||
const shuffleOption: TShuffleOption = "all";
|
||||
const survey = buildSurvey(config, mockT);
|
||||
|
||||
const question = buildMultipleChoiceQuestion({
|
||||
headline: "Test Question",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
subheader: "This is a subheader",
|
||||
choices: ["Option 1", "Option 2"],
|
||||
buttonLabel: "Custom Next",
|
||||
backButtonLabel: "Custom Back",
|
||||
shuffleOption,
|
||||
required: false,
|
||||
logic,
|
||||
t: mockT,
|
||||
});
|
||||
// role, industries, channels, description
|
||||
expect(survey.role).toBe(config.role);
|
||||
expect(survey.industries).toEqual(config.industries);
|
||||
expect(survey.channels).toEqual(config.channels);
|
||||
expect(survey.description).toBe(config.description);
|
||||
|
||||
expect(question.subheader).toEqual({ default: "This is a subheader" });
|
||||
expect(question.buttonLabel).toEqual({ default: "Custom Next" });
|
||||
expect(question.backButtonLabel).toEqual({ default: "Custom Back" });
|
||||
expect(question.shuffleOption).toBe("all");
|
||||
expect(question.required).toBe(false);
|
||||
expect(question.logic).toBe(logic);
|
||||
});
|
||||
});
|
||||
// preset overrides
|
||||
expect(survey.preset.name).toBe(config.name);
|
||||
expect(survey.preset.endings).toEqual(config.endings);
|
||||
expect(survey.preset.hiddenFields).toEqual(config.hiddenFields);
|
||||
expect(survey.preset.blocks).toEqual(config.blocks);
|
||||
|
||||
describe("buildOpenTextQuestion", () => {
|
||||
test("creates an open text question with required fields", () => {
|
||||
const question = buildOpenTextQuestion({
|
||||
headline: "Open Question",
|
||||
inputType: "text",
|
||||
t: mockT,
|
||||
});
|
||||
|
||||
expect(question).toMatchObject({
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Open Question" },
|
||||
inputType: "text",
|
||||
buttonLabel: { default: "common.next" },
|
||||
backButtonLabel: { default: "common.back" },
|
||||
required: false,
|
||||
charLimit: {
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
expect(question.id).toBeDefined();
|
||||
});
|
||||
|
||||
test("applies all optional parameters correctly", () => {
|
||||
const logic: TSurveyLogic[] = [
|
||||
{
|
||||
id: "logic-1",
|
||||
conditions: {
|
||||
id: "cond-1",
|
||||
connector: "and",
|
||||
conditions: [],
|
||||
},
|
||||
actions: [],
|
||||
},
|
||||
];
|
||||
|
||||
const question = buildOpenTextQuestion({
|
||||
id: "custom-id",
|
||||
headline: "Open Question",
|
||||
subheader: "Answer this question",
|
||||
placeholder: "Type here",
|
||||
buttonLabel: "Submit",
|
||||
backButtonLabel: "Previous",
|
||||
required: false,
|
||||
longAnswer: true,
|
||||
inputType: "email",
|
||||
logic,
|
||||
t: mockT,
|
||||
});
|
||||
|
||||
expect(question.id).toBe("custom-id");
|
||||
expect(question.subheader).toEqual({ default: "Answer this question" });
|
||||
expect(question.placeholder).toEqual({ default: "Type here" });
|
||||
expect(question.buttonLabel).toEqual({ default: "Submit" });
|
||||
expect(question.backButtonLabel).toEqual({ default: "Previous" });
|
||||
expect(question.required).toBe(false);
|
||||
expect(question.longAnswer).toBe(true);
|
||||
expect(question.inputType).toBe("email");
|
||||
expect(question.logic).toBe(logic);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildRatingQuestion", () => {
|
||||
test("creates a rating question with required fields", () => {
|
||||
const question = buildRatingQuestion({
|
||||
headline: "Rating Question",
|
||||
scale: "number",
|
||||
range: 5,
|
||||
t: mockT,
|
||||
});
|
||||
|
||||
expect(question).toMatchObject({
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
headline: { default: "Rating Question" },
|
||||
scale: "number",
|
||||
range: 5,
|
||||
buttonLabel: { default: "common.next" },
|
||||
backButtonLabel: { default: "common.back" },
|
||||
required: false,
|
||||
isColorCodingEnabled: false,
|
||||
});
|
||||
expect(question.id).toBeDefined();
|
||||
});
|
||||
|
||||
test("applies all optional parameters correctly", () => {
|
||||
const logic: TSurveyLogic[] = [
|
||||
{
|
||||
id: "logic-1",
|
||||
conditions: {
|
||||
id: "cond-1",
|
||||
connector: "and",
|
||||
conditions: [],
|
||||
},
|
||||
actions: [],
|
||||
},
|
||||
];
|
||||
|
||||
const question = buildRatingQuestion({
|
||||
id: "custom-id",
|
||||
headline: "Rating Question",
|
||||
subheader: "Rate us",
|
||||
scale: "star",
|
||||
range: 10,
|
||||
lowerLabel: "Poor",
|
||||
upperLabel: "Excellent",
|
||||
buttonLabel: "Submit",
|
||||
backButtonLabel: "Previous",
|
||||
required: false,
|
||||
isColorCodingEnabled: true,
|
||||
logic,
|
||||
t: mockT,
|
||||
});
|
||||
|
||||
expect(question.id).toBe("custom-id");
|
||||
expect(question.subheader).toEqual({ default: "Rate us" });
|
||||
expect(question.scale).toBe("star");
|
||||
expect(question.range).toBe(10);
|
||||
expect(question.lowerLabel).toEqual({ default: "Poor" });
|
||||
expect(question.upperLabel).toEqual({ default: "Excellent" });
|
||||
expect(question.buttonLabel).toEqual({ default: "Submit" });
|
||||
expect(question.backButtonLabel).toEqual({ default: "Previous" });
|
||||
expect(question.required).toBe(false);
|
||||
expect(question.isColorCodingEnabled).toBe(true);
|
||||
expect(question.logic).toBe(logic);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildNPSQuestion", () => {
|
||||
test("creates an NPS question with required fields", () => {
|
||||
const question = buildNPSQuestion({
|
||||
headline: "NPS Question",
|
||||
t: mockT,
|
||||
});
|
||||
|
||||
expect(question).toMatchObject({
|
||||
type: TSurveyQuestionTypeEnum.NPS,
|
||||
headline: { default: "NPS Question" },
|
||||
buttonLabel: { default: "common.next" },
|
||||
backButtonLabel: { default: "common.back" },
|
||||
required: false,
|
||||
isColorCodingEnabled: false,
|
||||
});
|
||||
expect(question.id).toBeDefined();
|
||||
});
|
||||
|
||||
test("applies all optional parameters correctly", () => {
|
||||
const logic: TSurveyLogic[] = [
|
||||
{
|
||||
id: "logic-1",
|
||||
conditions: {
|
||||
id: "cond-1",
|
||||
connector: "and",
|
||||
conditions: [],
|
||||
},
|
||||
actions: [],
|
||||
},
|
||||
];
|
||||
|
||||
const question = buildNPSQuestion({
|
||||
id: "custom-id",
|
||||
headline: "NPS Question",
|
||||
subheader: "How likely are you to recommend us?",
|
||||
lowerLabel: "Not likely",
|
||||
upperLabel: "Very likely",
|
||||
buttonLabel: "Submit",
|
||||
backButtonLabel: "Previous",
|
||||
required: false,
|
||||
isColorCodingEnabled: true,
|
||||
logic,
|
||||
t: mockT,
|
||||
});
|
||||
|
||||
expect(question.id).toBe("custom-id");
|
||||
expect(question.subheader).toEqual({ default: "How likely are you to recommend us?" });
|
||||
expect(question.lowerLabel).toEqual({ default: "Not likely" });
|
||||
expect(question.upperLabel).toEqual({ default: "Very likely" });
|
||||
expect(question.buttonLabel).toEqual({ default: "Submit" });
|
||||
expect(question.backButtonLabel).toEqual({ default: "Previous" });
|
||||
expect(question.required).toBe(false);
|
||||
expect(question.isColorCodingEnabled).toBe(true);
|
||||
expect(question.logic).toBe(logic);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildConsentQuestion", () => {
|
||||
test("creates a consent question with required fields", () => {
|
||||
const question = buildConsentQuestion({
|
||||
headline: "Consent Question",
|
||||
subheader: "",
|
||||
label: "I agree to terms",
|
||||
t: mockT,
|
||||
});
|
||||
|
||||
expect(question).toMatchObject({
|
||||
type: TSurveyQuestionTypeEnum.Consent,
|
||||
headline: { default: "Consent Question" },
|
||||
subheader: { default: "" },
|
||||
label: { default: "I agree to terms" },
|
||||
buttonLabel: { default: "common.next" },
|
||||
backButtonLabel: { default: "common.back" },
|
||||
required: false,
|
||||
});
|
||||
expect(question.id).toBeDefined();
|
||||
});
|
||||
|
||||
test("applies all optional parameters correctly", () => {
|
||||
const logic: TSurveyLogic[] = [
|
||||
{
|
||||
id: "logic-1",
|
||||
conditions: {
|
||||
id: "cond-1",
|
||||
connector: "and",
|
||||
conditions: [],
|
||||
},
|
||||
actions: [],
|
||||
},
|
||||
];
|
||||
|
||||
const question = buildConsentQuestion({
|
||||
id: "custom-id",
|
||||
headline: "Consent Question",
|
||||
subheader: "Please read the terms",
|
||||
label: "I agree to terms",
|
||||
buttonLabel: "Submit",
|
||||
backButtonLabel: "Previous",
|
||||
required: false,
|
||||
logic,
|
||||
t: mockT,
|
||||
});
|
||||
|
||||
expect(question.id).toBe("custom-id");
|
||||
expect(question.subheader).toEqual({ default: "Please read the terms" });
|
||||
expect(question.label).toEqual({ default: "I agree to terms" });
|
||||
expect(question.buttonLabel).toEqual({ default: "Submit" });
|
||||
expect(question.backButtonLabel).toEqual({ default: "Previous" });
|
||||
expect(question.required).toBe(false);
|
||||
expect(question.logic).toBe(logic);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildCTAQuestion", () => {
|
||||
test("creates a CTA question with required fields", () => {
|
||||
const question = buildCTAQuestion({
|
||||
headline: "CTA Question",
|
||||
subheader: "",
|
||||
buttonExternal: false,
|
||||
t: mockT,
|
||||
});
|
||||
|
||||
expect(question).toMatchObject({
|
||||
type: TSurveyQuestionTypeEnum.CTA,
|
||||
headline: { default: "CTA Question" },
|
||||
subheader: { default: "" },
|
||||
buttonLabel: { default: "common.next" },
|
||||
backButtonLabel: { default: "common.back" },
|
||||
required: false,
|
||||
buttonExternal: false,
|
||||
});
|
||||
expect(question.id).toBeDefined();
|
||||
});
|
||||
|
||||
test("applies all optional parameters correctly", () => {
|
||||
const logic: TSurveyLogic[] = [
|
||||
{
|
||||
id: "logic-1",
|
||||
conditions: {
|
||||
id: "cond-1",
|
||||
connector: "and",
|
||||
conditions: [],
|
||||
},
|
||||
actions: [],
|
||||
},
|
||||
];
|
||||
|
||||
const question = buildCTAQuestion({
|
||||
id: "custom-id",
|
||||
headline: "CTA Question",
|
||||
subheader: "<p>Click the button</p>",
|
||||
buttonLabel: "Click me",
|
||||
buttonExternal: true,
|
||||
buttonUrl: "https://example.com",
|
||||
backButtonLabel: "Previous",
|
||||
required: false,
|
||||
dismissButtonLabel: "No thanks",
|
||||
logic,
|
||||
t: mockT,
|
||||
});
|
||||
|
||||
expect(question.id).toBe("custom-id");
|
||||
expect(question.subheader).toEqual({ default: "<p>Click the button</p>" });
|
||||
expect(question.buttonLabel).toEqual({ default: "Click me" });
|
||||
expect(question.buttonExternal).toBe(true);
|
||||
expect(question.buttonUrl).toBe("https://example.com");
|
||||
expect(question.backButtonLabel).toEqual({ default: "Previous" });
|
||||
expect(question.required).toBe(false);
|
||||
expect(question.dismissButtonLabel).toEqual({ default: "No thanks" });
|
||||
expect(question.logic).toBe(logic);
|
||||
});
|
||||
|
||||
test("handles external button with URL", () => {
|
||||
const question = buildCTAQuestion({
|
||||
headline: "CTA Question",
|
||||
subheader: "",
|
||||
buttonExternal: true,
|
||||
buttonUrl: "https://formbricks.com",
|
||||
t: mockT,
|
||||
});
|
||||
|
||||
expect(question.buttonExternal).toBe(true);
|
||||
expect(question.buttonUrl).toBe("https://formbricks.com");
|
||||
});
|
||||
});
|
||||
|
||||
// Test combinations of parameters for edge cases
|
||||
describe("Edge cases", () => {
|
||||
test("multiple choice question with empty choices array", () => {
|
||||
const question = buildMultipleChoiceQuestion({
|
||||
headline: "Test Question",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
choices: [],
|
||||
t: mockT,
|
||||
});
|
||||
|
||||
expect(question.choices).toEqual([]);
|
||||
});
|
||||
|
||||
test("open text question with all parameters", () => {
|
||||
const question = buildOpenTextQuestion({
|
||||
id: "custom-id",
|
||||
headline: "Open Question",
|
||||
subheader: "Answer this question",
|
||||
placeholder: "Type here",
|
||||
buttonLabel: "Submit",
|
||||
backButtonLabel: "Previous",
|
||||
required: false,
|
||||
longAnswer: true,
|
||||
inputType: "email",
|
||||
logic: [],
|
||||
t: mockT,
|
||||
});
|
||||
|
||||
expect(question).toMatchObject({
|
||||
id: "custom-id",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Open Question" },
|
||||
subheader: { default: "Answer this question" },
|
||||
placeholder: { default: "Type here" },
|
||||
buttonLabel: { default: "Submit" },
|
||||
backButtonLabel: { default: "Previous" },
|
||||
required: false,
|
||||
longAnswer: true,
|
||||
inputType: "email",
|
||||
logic: [],
|
||||
});
|
||||
// default values from getDefaultSurveyPreset
|
||||
expect(survey.preset.welcomeCard).toHaveProperty("headline");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Helper Functions", () => {
|
||||
test("createJumpLogic returns valid jump logic", () => {
|
||||
const sourceId = "q1";
|
||||
const targetId = "q2";
|
||||
const operator: "isClicked" = "isClicked";
|
||||
const logic = createJumpLogic(sourceId, targetId, operator);
|
||||
|
||||
// Check structure
|
||||
expect(logic).toHaveProperty("id");
|
||||
expect(logic).toHaveProperty("conditions");
|
||||
expect(logic.conditions).toHaveProperty("conditions");
|
||||
expect(Array.isArray(logic.conditions.conditions)).toBe(true);
|
||||
|
||||
// Check one of the inner conditions
|
||||
const condition = logic.conditions.conditions[0];
|
||||
// Need to use type checking to ensure condition is a TSingleCondition not a TConditionGroup
|
||||
if (!("connector" in condition)) {
|
||||
expect(condition.leftOperand.value).toBe(sourceId);
|
||||
expect(condition.operator).toBe(operator);
|
||||
}
|
||||
|
||||
// Check actions
|
||||
expect(Array.isArray(logic.actions)).toBe(true);
|
||||
const action = logic.actions[0];
|
||||
if (action.objective === "jumpToQuestion") {
|
||||
expect(action.target).toBe(targetId);
|
||||
}
|
||||
});
|
||||
|
||||
test("createChoiceJumpLogic returns valid jump logic based on choice selection", () => {
|
||||
const sourceId = "q1";
|
||||
const choiceId = "choice1";
|
||||
const targetId = "q2";
|
||||
const logic = createChoiceJumpLogic(sourceId, choiceId, targetId);
|
||||
|
||||
expect(logic).toHaveProperty("id");
|
||||
expect(logic.conditions).toHaveProperty("conditions");
|
||||
|
||||
const condition = logic.conditions.conditions[0];
|
||||
if (!("connector" in condition)) {
|
||||
expect(condition.leftOperand.value).toBe(sourceId);
|
||||
expect(condition.operator).toBe("equals");
|
||||
expect(condition.rightOperand?.value).toBe(choiceId);
|
||||
}
|
||||
|
||||
const action = logic.actions[0];
|
||||
if (action.objective === "jumpToQuestion") {
|
||||
expect(action.target).toBe(targetId);
|
||||
}
|
||||
});
|
||||
|
||||
test("getDefaultWelcomeCard returns expected welcome card", () => {
|
||||
const card = getDefaultWelcomeCard(mockT);
|
||||
expect(card.enabled).toBe(false);
|
||||
expect(card.headline).toEqual({ default: "templates.default_welcome_card_headline" });
|
||||
expect(card.subheader).toEqual({ default: "templates.default_welcome_card_html" });
|
||||
expect(card.buttonLabel).toEqual({ default: "templates.default_welcome_card_button_label" });
|
||||
// boolean flags
|
||||
expect(card.timeToFinish).toBe(false);
|
||||
expect(card.showResponseCount).toBe(false);
|
||||
});
|
||||
|
||||
test("getDefaultEndingCard returns expected end screen card", () => {
|
||||
// Pass empty languages array to simulate no languages
|
||||
const card = getDefaultEndingCard([], mockT);
|
||||
expect(card).toHaveProperty("id");
|
||||
expect(card.type).toBe("endScreen");
|
||||
expect(card.headline).toEqual({ default: "templates.default_ending_card_headline" });
|
||||
expect(card.subheader).toEqual({ default: "templates.default_ending_card_subheader" });
|
||||
expect(card.buttonLabel).toEqual({ default: "templates.default_ending_card_button_label" });
|
||||
expect(card.buttonLink).toBe("https://formbricks.com");
|
||||
});
|
||||
|
||||
test("getDefaultSurveyPreset returns expected default survey preset", () => {
|
||||
const preset = getDefaultSurveyPreset(mockT);
|
||||
expect(preset.name).toBe("New Survey");
|
||||
expect(preset.questions).toEqual([]);
|
||||
// test welcomeCard and endings
|
||||
expect(preset.welcomeCard).toHaveProperty("headline");
|
||||
expect(Array.isArray(preset.endings)).toBe(true);
|
||||
expect(preset.hiddenFields).toEqual(hiddenFieldsDefault);
|
||||
});
|
||||
|
||||
test("buildSurvey returns built survey with overridden preset properties", () => {
|
||||
const config = {
|
||||
name: "Custom Survey",
|
||||
industries: ["eCommerce"] as string[],
|
||||
channels: ["link"],
|
||||
description: "Test survey",
|
||||
questions: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText, // changed from "OpenText"
|
||||
headline: { default: "Question 1" },
|
||||
inputType: "text",
|
||||
buttonLabel: { default: "Next" },
|
||||
backButtonLabel: { default: "Back" },
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
endings: [
|
||||
{
|
||||
id: "end1",
|
||||
type: "endScreen",
|
||||
headline: { default: "End Screen" },
|
||||
subheader: { default: "Thanks" },
|
||||
buttonLabel: { default: "Finish" },
|
||||
buttonLink: "https://formbricks.com",
|
||||
},
|
||||
],
|
||||
hiddenFields: { enabled: false, fieldIds: ["f1"] },
|
||||
};
|
||||
|
||||
const survey = buildSurvey(config as any, mockT);
|
||||
expect(survey.name).toBe(config.name);
|
||||
expect(survey.industries).toEqual(config.industries);
|
||||
expect(survey.channels).toEqual(config.channels);
|
||||
expect(survey.description).toBe(config.description);
|
||||
// preset overrides
|
||||
expect(survey.preset.name).toBe(config.name);
|
||||
expect(survey.preset.questions).toEqual(config.questions);
|
||||
expect(survey.preset.endings).toEqual(config.endings);
|
||||
expect(survey.preset.hiddenFields).toEqual(config.hiddenFields);
|
||||
});
|
||||
|
||||
test("hiddenFieldsDefault has expected default configuration", () => {
|
||||
expect(hiddenFieldsDefault).toEqual({ enabled: true, fieldIds: [] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,284 +1,17 @@
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { TFunction } from "i18next";
|
||||
import {
|
||||
TShuffleOption,
|
||||
TSurveyCTAQuestion,
|
||||
TSurveyConsentQuestion,
|
||||
import type { TFunction } from "i18next";
|
||||
import type { TSurveyBlock } from "@formbricks/types/surveys/blocks";
|
||||
import type {
|
||||
TSurveyEndScreenCard,
|
||||
TSurveyEnding,
|
||||
TSurveyHiddenFields,
|
||||
TSurveyLanguage,
|
||||
TSurveyLogic,
|
||||
TSurveyMultipleChoiceQuestion,
|
||||
TSurveyNPSQuestion,
|
||||
TSurveyOpenTextQuestion,
|
||||
TSurveyOpenTextQuestionInputType,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
TSurveyRatingQuestion,
|
||||
TSurveyWelcomeCard,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TTemplate, TTemplateRole } from "@formbricks/types/templates";
|
||||
import type { TTemplate, TTemplateRole } from "@formbricks/types/templates";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
|
||||
const getDefaultButtonLabel = (label: string | undefined, t: TFunction) =>
|
||||
createI18nString(label || t("common.next"), []);
|
||||
|
||||
const getDefaultBackButtonLabel = (label: string | undefined, t: TFunction) =>
|
||||
createI18nString(label || t("common.back"), []);
|
||||
|
||||
export const buildMultipleChoiceQuestion = ({
|
||||
id,
|
||||
headline,
|
||||
type,
|
||||
subheader,
|
||||
choices,
|
||||
choiceIds,
|
||||
buttonLabel,
|
||||
backButtonLabel,
|
||||
shuffleOption,
|
||||
required,
|
||||
logic,
|
||||
containsOther = false,
|
||||
t,
|
||||
}: {
|
||||
id?: string;
|
||||
headline: string;
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceMulti | TSurveyQuestionTypeEnum.MultipleChoiceSingle;
|
||||
subheader?: string;
|
||||
choices: string[];
|
||||
choiceIds?: string[];
|
||||
buttonLabel?: string;
|
||||
backButtonLabel?: string;
|
||||
shuffleOption?: TShuffleOption;
|
||||
required?: boolean;
|
||||
logic?: TSurveyLogic[];
|
||||
containsOther?: boolean;
|
||||
t: TFunction;
|
||||
}): TSurveyMultipleChoiceQuestion => {
|
||||
return {
|
||||
id: id ?? createId(),
|
||||
type,
|
||||
subheader: subheader ? createI18nString(subheader, []) : undefined,
|
||||
headline: createI18nString(headline, []),
|
||||
choices: choices.map((choice, index) => {
|
||||
const isLastIndex = index === choices.length - 1;
|
||||
const id = containsOther && isLastIndex ? "other" : choiceIds ? choiceIds[index] : createId();
|
||||
return { id, label: createI18nString(choice, []) };
|
||||
}),
|
||||
buttonLabel: getDefaultButtonLabel(buttonLabel, t),
|
||||
backButtonLabel: getDefaultBackButtonLabel(backButtonLabel, t),
|
||||
shuffleOption: shuffleOption || "none",
|
||||
required: required ?? false,
|
||||
logic,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildOpenTextQuestion = ({
|
||||
id,
|
||||
headline,
|
||||
subheader,
|
||||
placeholder,
|
||||
inputType,
|
||||
buttonLabel,
|
||||
backButtonLabel,
|
||||
required,
|
||||
logic,
|
||||
longAnswer,
|
||||
t,
|
||||
}: {
|
||||
id?: string;
|
||||
headline: string;
|
||||
subheader?: string;
|
||||
placeholder?: string;
|
||||
buttonLabel?: string;
|
||||
backButtonLabel?: string;
|
||||
required?: boolean;
|
||||
logic?: TSurveyLogic[];
|
||||
inputType: TSurveyOpenTextQuestionInputType;
|
||||
longAnswer?: boolean;
|
||||
t: TFunction;
|
||||
}): TSurveyOpenTextQuestion => {
|
||||
return {
|
||||
id: id ?? createId(),
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
inputType,
|
||||
subheader: subheader ? createI18nString(subheader, []) : undefined,
|
||||
placeholder: placeholder ? createI18nString(placeholder, []) : undefined,
|
||||
headline: createI18nString(headline, []),
|
||||
buttonLabel: getDefaultButtonLabel(buttonLabel, t),
|
||||
backButtonLabel: getDefaultBackButtonLabel(backButtonLabel, t),
|
||||
required: required ?? false,
|
||||
longAnswer,
|
||||
logic,
|
||||
charLimit: {
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const buildRatingQuestion = ({
|
||||
id,
|
||||
headline,
|
||||
subheader,
|
||||
scale,
|
||||
range,
|
||||
lowerLabel,
|
||||
upperLabel,
|
||||
buttonLabel,
|
||||
backButtonLabel,
|
||||
required,
|
||||
logic,
|
||||
isColorCodingEnabled = false,
|
||||
t,
|
||||
}: {
|
||||
id?: string;
|
||||
headline: string;
|
||||
scale: TSurveyRatingQuestion["scale"];
|
||||
range: TSurveyRatingQuestion["range"];
|
||||
lowerLabel?: string;
|
||||
upperLabel?: string;
|
||||
subheader?: string;
|
||||
placeholder?: string;
|
||||
buttonLabel?: string;
|
||||
backButtonLabel?: string;
|
||||
required?: boolean;
|
||||
logic?: TSurveyLogic[];
|
||||
isColorCodingEnabled?: boolean;
|
||||
t: TFunction;
|
||||
}): TSurveyRatingQuestion => {
|
||||
return {
|
||||
id: id ?? createId(),
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
subheader: subheader ? createI18nString(subheader, []) : undefined,
|
||||
headline: createI18nString(headline, []),
|
||||
scale,
|
||||
range,
|
||||
buttonLabel: getDefaultButtonLabel(buttonLabel, t),
|
||||
backButtonLabel: getDefaultBackButtonLabel(backButtonLabel, t),
|
||||
required: required ?? false,
|
||||
isColorCodingEnabled,
|
||||
lowerLabel: lowerLabel ? createI18nString(lowerLabel, []) : undefined,
|
||||
upperLabel: upperLabel ? createI18nString(upperLabel, []) : undefined,
|
||||
logic,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildNPSQuestion = ({
|
||||
id,
|
||||
headline,
|
||||
subheader,
|
||||
lowerLabel,
|
||||
upperLabel,
|
||||
buttonLabel,
|
||||
backButtonLabel,
|
||||
required,
|
||||
logic,
|
||||
isColorCodingEnabled = false,
|
||||
t,
|
||||
}: {
|
||||
id?: string;
|
||||
headline: string;
|
||||
lowerLabel?: string;
|
||||
upperLabel?: string;
|
||||
subheader?: string;
|
||||
placeholder?: string;
|
||||
buttonLabel?: string;
|
||||
backButtonLabel?: string;
|
||||
required?: boolean;
|
||||
logic?: TSurveyLogic[];
|
||||
isColorCodingEnabled?: boolean;
|
||||
t: TFunction;
|
||||
}): TSurveyNPSQuestion => {
|
||||
return {
|
||||
id: id ?? createId(),
|
||||
type: TSurveyQuestionTypeEnum.NPS,
|
||||
subheader: subheader ? createI18nString(subheader, []) : undefined,
|
||||
headline: createI18nString(headline, []),
|
||||
buttonLabel: getDefaultButtonLabel(buttonLabel, t),
|
||||
backButtonLabel: getDefaultBackButtonLabel(backButtonLabel, t),
|
||||
required: required ?? false,
|
||||
isColorCodingEnabled,
|
||||
lowerLabel: lowerLabel ? createI18nString(lowerLabel, []) : undefined,
|
||||
upperLabel: upperLabel ? createI18nString(upperLabel, []) : undefined,
|
||||
logic,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildConsentQuestion = ({
|
||||
id,
|
||||
headline,
|
||||
subheader,
|
||||
label,
|
||||
buttonLabel,
|
||||
backButtonLabel,
|
||||
required,
|
||||
logic,
|
||||
t,
|
||||
}: {
|
||||
id?: string;
|
||||
headline: string;
|
||||
subheader: string;
|
||||
buttonLabel?: string;
|
||||
backButtonLabel?: string;
|
||||
required?: boolean;
|
||||
logic?: TSurveyLogic[];
|
||||
label: string;
|
||||
t: TFunction;
|
||||
}): TSurveyConsentQuestion => {
|
||||
return {
|
||||
id: id ?? createId(),
|
||||
type: TSurveyQuestionTypeEnum.Consent,
|
||||
subheader: createI18nString(subheader, []),
|
||||
headline: createI18nString(headline, []),
|
||||
buttonLabel: getDefaultButtonLabel(buttonLabel, t),
|
||||
backButtonLabel: getDefaultBackButtonLabel(backButtonLabel, t),
|
||||
required: required ?? false,
|
||||
label: createI18nString(label, []),
|
||||
logic,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildCTAQuestion = ({
|
||||
id,
|
||||
headline,
|
||||
subheader,
|
||||
buttonLabel,
|
||||
buttonExternal,
|
||||
backButtonLabel,
|
||||
required,
|
||||
logic,
|
||||
dismissButtonLabel,
|
||||
buttonUrl,
|
||||
t,
|
||||
}: {
|
||||
id?: string;
|
||||
headline: string;
|
||||
buttonExternal: boolean;
|
||||
subheader: string;
|
||||
buttonLabel?: string;
|
||||
backButtonLabel?: string;
|
||||
required?: boolean;
|
||||
logic?: TSurveyLogic[];
|
||||
dismissButtonLabel?: string;
|
||||
buttonUrl?: string;
|
||||
t: TFunction;
|
||||
}): TSurveyCTAQuestion => {
|
||||
return {
|
||||
id: id ?? createId(),
|
||||
type: TSurveyQuestionTypeEnum.CTA,
|
||||
subheader: createI18nString(subheader, []),
|
||||
headline: createI18nString(headline, []),
|
||||
buttonLabel: getDefaultButtonLabel(buttonLabel, t),
|
||||
backButtonLabel: getDefaultBackButtonLabel(backButtonLabel, t),
|
||||
dismissButtonLabel: dismissButtonLabel ? createI18nString(dismissButtonLabel, []) : undefined,
|
||||
required: required ?? false,
|
||||
buttonExternal,
|
||||
buttonUrl,
|
||||
logic,
|
||||
};
|
||||
};
|
||||
|
||||
// Helper function to create standard jump logic based on operator
|
||||
export const createJumpLogic = (
|
||||
sourceQuestionId: string,
|
||||
@@ -294,7 +27,7 @@ export const createJumpLogic = (
|
||||
id: createId(),
|
||||
leftOperand: {
|
||||
value: sourceQuestionId,
|
||||
type: "question",
|
||||
type: "element",
|
||||
},
|
||||
operator: operator,
|
||||
},
|
||||
@@ -324,7 +57,7 @@ export const createChoiceJumpLogic = (
|
||||
id: createId(),
|
||||
leftOperand: {
|
||||
value: sourceQuestionId,
|
||||
type: "question",
|
||||
type: "element",
|
||||
},
|
||||
operator: "equals",
|
||||
rightOperand: {
|
||||
@@ -377,13 +110,13 @@ export const getDefaultSurveyPreset = (t: TFunction): TTemplate["preset"] => {
|
||||
welcomeCard: getDefaultWelcomeCard(t),
|
||||
endings: [getDefaultEndingCard([], t)],
|
||||
hiddenFields: hiddenFieldsDefault,
|
||||
questions: [],
|
||||
blocks: [],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic builder for survey.
|
||||
* @param config - The configuration for survey settings and questions.
|
||||
* @param config - The configuration for survey settings and blocks.
|
||||
* @param t - The translation function.
|
||||
*/
|
||||
export const buildSurvey = (
|
||||
@@ -393,9 +126,9 @@ export const buildSurvey = (
|
||||
channels: ("link" | "app" | "website")[];
|
||||
role: TTemplateRole;
|
||||
description: string;
|
||||
questions: TSurveyQuestion[];
|
||||
endings?: TSurveyEnding[];
|
||||
hiddenFields?: TSurveyHiddenFields;
|
||||
blocks: TSurveyBlock[];
|
||||
endings: TSurveyEnding[];
|
||||
hiddenFields: TSurveyHiddenFields;
|
||||
},
|
||||
t: TFunction
|
||||
): TTemplate => {
|
||||
@@ -409,7 +142,7 @@ export const buildSurvey = (
|
||||
preset: {
|
||||
...localSurvey,
|
||||
name: config.name,
|
||||
questions: config.questions,
|
||||
blocks: config.blocks ?? [],
|
||||
endings: config.endings ?? localSurvey.endings,
|
||||
hiddenFields: config.hiddenFields ?? hiddenFieldsDefault,
|
||||
},
|
||||
|
||||
@@ -2,19 +2,15 @@ import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { TLanguage } from "@formbricks/types/project";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyLanguage,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { type TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey, TSurveyLanguage } from "@formbricks/types/surveys/types";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import {
|
||||
DateRange,
|
||||
SelectedFilterValue,
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
||||
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
|
||||
import { generateQuestionAndFilterOptions, getFormattedFilters, getTodayDate } from "./surveys";
|
||||
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ElementsComboBox";
|
||||
import { generateElementAndFilterOptions, getFormattedFilters, getTodayDate } from "./surveys";
|
||||
|
||||
describe("surveys", () => {
|
||||
afterEach(() => {
|
||||
@@ -26,31 +22,42 @@ describe("surveys", () => {
|
||||
const survey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
questions: [
|
||||
blocks: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Open Text Question" },
|
||||
} as unknown as TSurveyQuestion,
|
||||
id: "block1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Open Text Question" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
charLimit: { enabled: false },
|
||||
} as TSurveyElement,
|
||||
],
|
||||
},
|
||||
],
|
||||
questions: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "env1",
|
||||
status: "draft",
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const result = generateQuestionAndFilterOptions(survey, undefined, {}, {}, {}, []);
|
||||
const result = generateElementAndFilterOptions(survey, undefined, {}, {}, {}, []);
|
||||
|
||||
expect(result.questionOptions.length).toBeGreaterThan(0);
|
||||
expect(result.questionOptions[0].header).toBe(OptionsType.QUESTIONS);
|
||||
expect(result.questionFilterOptions.length).toBe(1);
|
||||
expect(result.questionFilterOptions[0].id).toBe("q1");
|
||||
expect(result.elementOptions.length).toBeGreaterThan(0);
|
||||
expect(result.elementOptions[0].header).toBe(OptionsType.ELEMENTS);
|
||||
expect(result.elementFilterOptions.length).toBe(1);
|
||||
expect(result.elementFilterOptions[0].id).toBe("q1");
|
||||
});
|
||||
|
||||
test("should include tags in options when provided", () => {
|
||||
const survey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
blocks: [],
|
||||
questions: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
@@ -62,9 +69,9 @@ describe("surveys", () => {
|
||||
{ id: "tag1", name: "Tag 1", environmentId: "env1", createdAt: new Date(), updatedAt: new Date() },
|
||||
];
|
||||
|
||||
const result = generateQuestionAndFilterOptions(survey, tags, {}, {}, {}, []);
|
||||
const result = generateElementAndFilterOptions(survey, tags, {}, {}, {}, []);
|
||||
|
||||
const tagsHeader = result.questionOptions.find((opt) => opt.header === OptionsType.TAGS);
|
||||
const tagsHeader = result.elementOptions.find((opt) => opt.header === OptionsType.TAGS);
|
||||
expect(tagsHeader).toBeDefined();
|
||||
expect(tagsHeader?.option.length).toBe(1);
|
||||
expect(tagsHeader?.option[0].label).toBe("Tag 1");
|
||||
@@ -74,6 +81,7 @@ describe("surveys", () => {
|
||||
const survey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
blocks: [],
|
||||
questions: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
@@ -85,9 +93,9 @@ describe("surveys", () => {
|
||||
role: ["admin", "user"],
|
||||
};
|
||||
|
||||
const result = generateQuestionAndFilterOptions(survey, undefined, attributes, {}, {}, []);
|
||||
const result = generateElementAndFilterOptions(survey, undefined, attributes, {}, {}, []);
|
||||
|
||||
const attributesHeader = result.questionOptions.find((opt) => opt.header === OptionsType.ATTRIBUTES);
|
||||
const attributesHeader = result.elementOptions.find((opt) => opt.header === OptionsType.ATTRIBUTES);
|
||||
expect(attributesHeader).toBeDefined();
|
||||
expect(attributesHeader?.option.length).toBe(1);
|
||||
expect(attributesHeader?.option[0].label).toBe("role");
|
||||
@@ -97,6 +105,7 @@ describe("surveys", () => {
|
||||
const survey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
blocks: [],
|
||||
questions: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
@@ -108,9 +117,9 @@ describe("surveys", () => {
|
||||
source: ["web", "mobile"],
|
||||
};
|
||||
|
||||
const result = generateQuestionAndFilterOptions(survey, undefined, {}, meta, {}, []);
|
||||
const result = generateElementAndFilterOptions(survey, undefined, {}, meta, {}, []);
|
||||
|
||||
const metaHeader = result.questionOptions.find((opt) => opt.header === OptionsType.META);
|
||||
const metaHeader = result.elementOptions.find((opt) => opt.header === OptionsType.META);
|
||||
expect(metaHeader).toBeDefined();
|
||||
expect(metaHeader?.option.length).toBe(1);
|
||||
expect(metaHeader?.option[0].label).toBe("source");
|
||||
@@ -120,6 +129,7 @@ describe("surveys", () => {
|
||||
const survey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
blocks: [],
|
||||
questions: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
@@ -131,9 +141,9 @@ describe("surveys", () => {
|
||||
segment: ["free", "paid"],
|
||||
};
|
||||
|
||||
const result = generateQuestionAndFilterOptions(survey, undefined, {}, {}, hiddenFields, []);
|
||||
const result = generateElementAndFilterOptions(survey, undefined, {}, {}, hiddenFields, []);
|
||||
|
||||
const hiddenFieldsHeader = result.questionOptions.find(
|
||||
const hiddenFieldsHeader = result.elementOptions.find(
|
||||
(opt) => opt.header === OptionsType.HIDDEN_FIELDS
|
||||
);
|
||||
expect(hiddenFieldsHeader).toBeDefined();
|
||||
@@ -145,6 +155,7 @@ describe("surveys", () => {
|
||||
const survey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
blocks: [],
|
||||
questions: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
@@ -153,9 +164,9 @@ describe("surveys", () => {
|
||||
languages: [{ language: { code: "en" } as unknown as TLanguage } as unknown as TSurveyLanguage],
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const result = generateQuestionAndFilterOptions(survey, undefined, {}, {}, {}, []);
|
||||
const result = generateElementAndFilterOptions(survey, undefined, {}, {}, {}, []);
|
||||
|
||||
const othersHeader = result.questionOptions.find((opt) => opt.header === OptionsType.OTHERS);
|
||||
const othersHeader = result.elementOptions.find((opt) => opt.header === OptionsType.OTHERS);
|
||||
expect(othersHeader).toBeDefined();
|
||||
expect(othersHeader?.option.some((o) => o.label === "Language")).toBeTruthy();
|
||||
});
|
||||
@@ -164,78 +175,107 @@ describe("surveys", () => {
|
||||
const survey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
questions: [
|
||||
blocks: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Open Text" },
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "q2",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Multiple Choice Single" },
|
||||
choices: [{ id: "c1", label: "Choice 1" }],
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "q3",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceMulti,
|
||||
headline: { default: "Multiple Choice Multi" },
|
||||
choices: [
|
||||
{ id: "c1", label: "Choice 1" },
|
||||
{ id: "other", label: "Other" },
|
||||
],
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "q4",
|
||||
type: TSurveyQuestionTypeEnum.NPS,
|
||||
headline: { default: "NPS" },
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "q5",
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
headline: { default: "Rating" },
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "q6",
|
||||
type: TSurveyQuestionTypeEnum.CTA,
|
||||
headline: { default: "CTA" },
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "q7",
|
||||
type: TSurveyQuestionTypeEnum.PictureSelection,
|
||||
headline: { default: "Picture Selection" },
|
||||
choices: [
|
||||
{ id: "p1", imageUrl: "url1" },
|
||||
{ id: "p2", imageUrl: "url2" },
|
||||
],
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "q8",
|
||||
type: TSurveyQuestionTypeEnum.Matrix,
|
||||
headline: { default: "Matrix" },
|
||||
rows: [{ id: "r1", label: { default: "Row 1" } }],
|
||||
columns: [{ id: "c1", label: { default: "Column 1" } }],
|
||||
} as unknown as TSurveyQuestion,
|
||||
id: "block1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Open Text" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
charLimit: { enabled: false },
|
||||
},
|
||||
{
|
||||
id: "q2",
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Multiple Choice Single" },
|
||||
required: false,
|
||||
choices: [{ id: "c1", label: { default: "Choice 1" } }],
|
||||
shuffleOption: "none",
|
||||
},
|
||||
{
|
||||
id: "q3",
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceMulti,
|
||||
headline: { default: "Multiple Choice Multi" },
|
||||
required: false,
|
||||
choices: [
|
||||
{ id: "c1", label: { default: "Choice 1" } },
|
||||
{ id: "other", label: { default: "Other" } },
|
||||
],
|
||||
shuffleOption: "none",
|
||||
},
|
||||
{
|
||||
id: "q4",
|
||||
type: TSurveyElementTypeEnum.NPS,
|
||||
headline: { default: "NPS" },
|
||||
required: false,
|
||||
lowerLabel: { default: "Not likely" },
|
||||
upperLabel: { default: "Very likely" },
|
||||
},
|
||||
{
|
||||
id: "q5",
|
||||
type: TSurveyElementTypeEnum.Rating,
|
||||
headline: { default: "Rating" },
|
||||
required: false,
|
||||
scale: "number",
|
||||
range: 5,
|
||||
lowerLabel: { default: "Low" },
|
||||
upperLabel: { default: "High" },
|
||||
},
|
||||
{
|
||||
id: "q6",
|
||||
type: TSurveyElementTypeEnum.CTA,
|
||||
headline: { default: "CTA" },
|
||||
required: false,
|
||||
buttonLabel: { default: "Click me" },
|
||||
buttonExternal: false,
|
||||
},
|
||||
{
|
||||
id: "q7",
|
||||
type: TSurveyElementTypeEnum.PictureSelection,
|
||||
headline: { default: "Picture Selection" },
|
||||
required: false,
|
||||
allowMultiple: false,
|
||||
choices: [
|
||||
{ id: "p1", imageUrl: "url1" },
|
||||
{ id: "p2", imageUrl: "url2" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "q8",
|
||||
type: TSurveyElementTypeEnum.Matrix,
|
||||
headline: { default: "Matrix" },
|
||||
required: false,
|
||||
rows: [{ id: "r1", label: { default: "Row 1" } }],
|
||||
columns: [{ id: "c1", label: { default: "Column 1" } }],
|
||||
},
|
||||
] as TSurveyElement[],
|
||||
},
|
||||
],
|
||||
questions: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "env1",
|
||||
status: "draft",
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const result = generateQuestionAndFilterOptions(survey, undefined, {}, {}, {}, []);
|
||||
const result = generateElementAndFilterOptions(survey, undefined, {}, {}, {}, []);
|
||||
|
||||
expect(result.questionFilterOptions.length).toBe(8);
|
||||
expect(result.questionFilterOptions.some((o) => o.id === "q1")).toBeTruthy();
|
||||
expect(result.questionFilterOptions.some((o) => o.id === "q2")).toBeTruthy();
|
||||
expect(result.questionFilterOptions.some((o) => o.id === "q7")).toBeTruthy();
|
||||
expect(result.questionFilterOptions.some((o) => o.id === "q8")).toBeTruthy();
|
||||
expect(result.elementFilterOptions.length).toBe(8);
|
||||
expect(result.elementFilterOptions.some((o) => o.id === "q1")).toBeTruthy();
|
||||
expect(result.elementFilterOptions.some((o) => o.id === "q2")).toBeTruthy();
|
||||
expect(result.elementFilterOptions.some((o) => o.id === "q7")).toBeTruthy();
|
||||
expect(result.elementFilterOptions.some((o) => o.id === "q8")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("should provide extended filter options for URL meta field", () => {
|
||||
const survey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
blocks: [],
|
||||
questions: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
@@ -248,10 +288,10 @@ describe("surveys", () => {
|
||||
source: ["web", "mobile"],
|
||||
};
|
||||
|
||||
const result = generateQuestionAndFilterOptions(survey, undefined, {}, meta, {}, []);
|
||||
const result = generateElementAndFilterOptions(survey, undefined, {}, meta, {}, []);
|
||||
|
||||
const urlFilterOption = result.questionFilterOptions.find((o) => o.id === "url");
|
||||
const sourceFilterOption = result.questionFilterOptions.find((o) => o.id === "source");
|
||||
const urlFilterOption = result.elementFilterOptions.find((o) => o.id === "url");
|
||||
const sourceFilterOption = result.elementFilterOptions.find((o) => o.id === "source");
|
||||
|
||||
expect(urlFilterOption).toBeDefined();
|
||||
expect(urlFilterOption?.filterOptions).toEqual([
|
||||
@@ -273,7 +313,7 @@ describe("surveys", () => {
|
||||
const survey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
questions: [],
|
||||
blocks: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "env1",
|
||||
@@ -282,9 +322,9 @@ describe("surveys", () => {
|
||||
|
||||
const quotas = [{ id: "quota1" }];
|
||||
|
||||
const result = generateQuestionAndFilterOptions(survey, undefined, {}, {}, {}, quotas as any);
|
||||
const result = generateElementAndFilterOptions(survey, undefined, {}, {}, {}, quotas as any);
|
||||
|
||||
const quotaFilterOption = result.questionFilterOptions.find((o) => o.id === "quota1");
|
||||
const quotaFilterOption = result.elementFilterOptions.find((o) => o.id === "quota1");
|
||||
expect(quotaFilterOption).toBeDefined();
|
||||
expect(quotaFilterOption?.type).toBe("Quotas");
|
||||
expect(quotaFilterOption?.filterOptions).toEqual(["Status"]);
|
||||
@@ -299,7 +339,7 @@ describe("surveys", () => {
|
||||
const survey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
questions: [],
|
||||
blocks: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "env1",
|
||||
@@ -308,10 +348,10 @@ describe("surveys", () => {
|
||||
|
||||
const quotas = [{ id: "quota1" }, { id: "quota2" }];
|
||||
|
||||
const result = generateQuestionAndFilterOptions(survey, undefined, {}, {}, {}, quotas as any);
|
||||
const result = generateElementAndFilterOptions(survey, undefined, {}, {}, {}, quotas as any);
|
||||
|
||||
const quota1 = result.questionFilterOptions.find((o) => o.id === "quota1");
|
||||
const quota2 = result.questionFilterOptions.find((o) => o.id === "quota2");
|
||||
const quota1 = result.elementFilterOptions.find((o) => o.id === "quota1");
|
||||
const quota2 = result.elementFilterOptions.find((o) => o.id === "quota2");
|
||||
|
||||
expect(quota1).toBeDefined();
|
||||
expect(quota2).toBeDefined();
|
||||
@@ -332,76 +372,121 @@ describe("surveys", () => {
|
||||
const survey = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
questions: [
|
||||
blocks: [
|
||||
{
|
||||
id: "openTextQ",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Open Text" },
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "mcSingleQ",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Multiple Choice Single" },
|
||||
choices: [{ id: "c1", label: "Choice 1" }],
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "mcMultiQ",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceMulti,
|
||||
headline: { default: "Multiple Choice Multi" },
|
||||
choices: [{ id: "c1", label: "Choice 1" }],
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "npsQ",
|
||||
type: TSurveyQuestionTypeEnum.NPS,
|
||||
headline: { default: "NPS" },
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "ratingQ",
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
headline: { default: "Rating" },
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "ctaQ",
|
||||
type: TSurveyQuestionTypeEnum.CTA,
|
||||
headline: { default: "CTA" },
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "consentQ",
|
||||
type: TSurveyQuestionTypeEnum.Consent,
|
||||
headline: { default: "Consent" },
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "pictureQ",
|
||||
type: TSurveyQuestionTypeEnum.PictureSelection,
|
||||
headline: { default: "Picture Selection" },
|
||||
choices: [
|
||||
{ id: "p1", imageUrl: "url1" },
|
||||
{ id: "p2", imageUrl: "url2" },
|
||||
],
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "matrixQ",
|
||||
type: TSurveyQuestionTypeEnum.Matrix,
|
||||
headline: { default: "Matrix" },
|
||||
rows: [{ id: "r1", label: "Row 1" }],
|
||||
columns: [{ id: "c1", label: "Column 1" }],
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "addressQ",
|
||||
type: TSurveyQuestionTypeEnum.Address,
|
||||
headline: { default: "Address" },
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "contactQ",
|
||||
type: TSurveyQuestionTypeEnum.ContactInfo,
|
||||
headline: { default: "Contact Info" },
|
||||
} as unknown as TSurveyQuestion,
|
||||
{
|
||||
id: "rankingQ",
|
||||
type: TSurveyQuestionTypeEnum.Ranking,
|
||||
headline: { default: "Ranking" },
|
||||
} as unknown as TSurveyQuestion,
|
||||
id: "block1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
{
|
||||
id: "openTextQ",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Open Text" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
charLimit: { enabled: false },
|
||||
},
|
||||
{
|
||||
id: "mcSingleQ",
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Multiple Choice Single" },
|
||||
required: false,
|
||||
choices: [{ id: "c1", label: { default: "Choice 1" } }],
|
||||
shuffleOption: "none",
|
||||
},
|
||||
{
|
||||
id: "mcMultiQ",
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceMulti,
|
||||
headline: { default: "Multiple Choice Multi" },
|
||||
required: false,
|
||||
choices: [{ id: "c1", label: { default: "Choice 1" } }],
|
||||
shuffleOption: "none",
|
||||
},
|
||||
{
|
||||
id: "npsQ",
|
||||
type: TSurveyElementTypeEnum.NPS,
|
||||
headline: { default: "NPS" },
|
||||
required: false,
|
||||
lowerLabel: { default: "Not likely" },
|
||||
upperLabel: { default: "Very likely" },
|
||||
},
|
||||
{
|
||||
id: "ratingQ",
|
||||
type: TSurveyElementTypeEnum.Rating,
|
||||
headline: { default: "Rating" },
|
||||
required: false,
|
||||
scale: "number",
|
||||
range: 5,
|
||||
lowerLabel: { default: "Low" },
|
||||
upperLabel: { default: "High" },
|
||||
},
|
||||
{
|
||||
id: "ctaQ",
|
||||
type: TSurveyElementTypeEnum.CTA,
|
||||
headline: { default: "CTA" },
|
||||
required: false,
|
||||
buttonLabel: { default: "Click me" },
|
||||
buttonExternal: false,
|
||||
},
|
||||
{
|
||||
id: "consentQ",
|
||||
type: TSurveyElementTypeEnum.Consent,
|
||||
headline: { default: "Consent" },
|
||||
required: false,
|
||||
label: { default: "I agree" },
|
||||
},
|
||||
{
|
||||
id: "pictureQ",
|
||||
type: TSurveyElementTypeEnum.PictureSelection,
|
||||
headline: { default: "Picture Selection" },
|
||||
required: false,
|
||||
allowMultiple: false,
|
||||
choices: [
|
||||
{ id: "p1", imageUrl: "url1" },
|
||||
{ id: "p2", imageUrl: "url2" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "matrixQ",
|
||||
type: TSurveyElementTypeEnum.Matrix,
|
||||
headline: { default: "Matrix" },
|
||||
required: false,
|
||||
rows: [{ id: "r1", label: { default: "Row 1" } }],
|
||||
columns: [{ id: "c1", label: { default: "Column 1" } }],
|
||||
},
|
||||
{
|
||||
id: "addressQ",
|
||||
type: TSurveyElementTypeEnum.Address,
|
||||
headline: { default: "Address" },
|
||||
required: false,
|
||||
zip: { show: true, required: false, placeholder: { default: "Zip" } },
|
||||
city: { show: true, required: false, placeholder: { default: "City" } },
|
||||
state: { show: true, required: false, placeholder: { default: "State" } },
|
||||
country: { show: true, required: false, placeholder: { default: "Country" } },
|
||||
addressLine1: { show: true, required: false, placeholder: { default: "Address Line 1" } },
|
||||
addressLine2: { show: true, required: false, placeholder: { default: "Address Line 2" } },
|
||||
},
|
||||
{
|
||||
id: "contactQ",
|
||||
type: TSurveyElementTypeEnum.ContactInfo,
|
||||
headline: { default: "Contact Info" },
|
||||
required: false,
|
||||
firstName: { show: true, required: false, placeholder: { default: "First Name" } },
|
||||
lastName: { show: true, required: false, placeholder: { default: "Last Name" } },
|
||||
email: { show: true, required: false, placeholder: { default: "Email" } },
|
||||
phone: { show: true, required: false, placeholder: { default: "Phone" } },
|
||||
company: { show: true, required: false, placeholder: { default: "Company" } },
|
||||
},
|
||||
{
|
||||
id: "rankingQ",
|
||||
type: TSurveyElementTypeEnum.Ranking,
|
||||
headline: { default: "Ranking" },
|
||||
required: false,
|
||||
choices: [{ id: "r1", label: { default: "Option 1" } }],
|
||||
},
|
||||
] as TSurveyElement[],
|
||||
},
|
||||
],
|
||||
questions: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: "env1",
|
||||
@@ -453,11 +538,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: { type: "Tags", label: "Tag 1", id: "tag1" },
|
||||
elementType: { type: "Tags", label: "Tag 1", id: "tag1" },
|
||||
filterType: { filterComboBoxValue: "Applied" },
|
||||
},
|
||||
{
|
||||
questionType: { type: "Tags", label: "Tag 2", id: "tag2" },
|
||||
elementType: { type: "Tags", label: "Tag 2", id: "tag2" },
|
||||
filterType: { filterComboBoxValue: "Not applied" },
|
||||
},
|
||||
] as any,
|
||||
@@ -474,11 +559,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "Open Text",
|
||||
id: "openTextQ",
|
||||
questionType: TSurveyQuestionTypeEnum.OpenText,
|
||||
elementType: TSurveyElementTypeEnum.OpenText,
|
||||
},
|
||||
filterType: { filterComboBoxValue: "Filled out" },
|
||||
},
|
||||
@@ -495,11 +580,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "Address",
|
||||
id: "addressQ",
|
||||
questionType: TSurveyQuestionTypeEnum.Address,
|
||||
elementType: TSurveyElementTypeEnum.Address,
|
||||
},
|
||||
filterType: { filterComboBoxValue: "Skipped" },
|
||||
},
|
||||
@@ -516,11 +601,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "Contact Info",
|
||||
id: "contactQ",
|
||||
questionType: TSurveyQuestionTypeEnum.ContactInfo,
|
||||
elementType: TSurveyElementTypeEnum.ContactInfo,
|
||||
},
|
||||
filterType: { filterComboBoxValue: "Filled out" },
|
||||
},
|
||||
@@ -537,11 +622,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "Ranking",
|
||||
id: "rankingQ",
|
||||
questionType: TSurveyQuestionTypeEnum.Ranking,
|
||||
elementType: TSurveyElementTypeEnum.Ranking,
|
||||
},
|
||||
filterType: { filterComboBoxValue: "Filled out" },
|
||||
},
|
||||
@@ -558,11 +643,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "MC Single",
|
||||
id: "mcSingleQ",
|
||||
questionType: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
elementType: TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||
},
|
||||
filterType: { filterValue: "Includes either", filterComboBoxValue: ["Choice 1"] },
|
||||
},
|
||||
@@ -579,11 +664,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "MC Multi",
|
||||
id: "mcMultiQ",
|
||||
questionType: TSurveyQuestionTypeEnum.MultipleChoiceMulti,
|
||||
elementType: TSurveyElementTypeEnum.MultipleChoiceMulti,
|
||||
},
|
||||
filterType: { filterValue: "Includes all", filterComboBoxValue: ["Choice 1", "Choice 2"] },
|
||||
},
|
||||
@@ -600,11 +685,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "NPS",
|
||||
id: "npsQ",
|
||||
questionType: TSurveyQuestionTypeEnum.NPS,
|
||||
elementType: TSurveyElementTypeEnum.NPS,
|
||||
},
|
||||
filterType: { filterValue: "Is equal to", filterComboBoxValue: "7" },
|
||||
},
|
||||
@@ -621,11 +706,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "Rating",
|
||||
id: "ratingQ",
|
||||
questionType: TSurveyQuestionTypeEnum.Rating,
|
||||
elementType: TSurveyElementTypeEnum.Rating,
|
||||
},
|
||||
filterType: { filterValue: "Is less than", filterComboBoxValue: "4" },
|
||||
},
|
||||
@@ -642,11 +727,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "CTA",
|
||||
id: "ctaQ",
|
||||
questionType: TSurveyQuestionTypeEnum.CTA,
|
||||
elementType: TSurveyElementTypeEnum.CTA,
|
||||
},
|
||||
filterType: { filterComboBoxValue: "Clicked" },
|
||||
},
|
||||
@@ -663,11 +748,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "Consent",
|
||||
id: "consentQ",
|
||||
questionType: TSurveyQuestionTypeEnum.Consent,
|
||||
elementType: TSurveyElementTypeEnum.Consent,
|
||||
},
|
||||
filterType: { filterComboBoxValue: "Accepted" },
|
||||
},
|
||||
@@ -684,11 +769,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "Picture",
|
||||
id: "pictureQ",
|
||||
questionType: TSurveyQuestionTypeEnum.PictureSelection,
|
||||
elementType: TSurveyElementTypeEnum.PictureSelection,
|
||||
},
|
||||
filterType: { filterValue: "Includes either", filterComboBoxValue: ["Picture 1"] },
|
||||
},
|
||||
@@ -705,11 +790,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "Matrix",
|
||||
id: "matrixQ",
|
||||
questionType: TSurveyQuestionTypeEnum.Matrix,
|
||||
elementType: TSurveyElementTypeEnum.Matrix,
|
||||
},
|
||||
filterType: { filterValue: "Row 1", filterComboBoxValue: "Column 1" },
|
||||
},
|
||||
@@ -726,7 +811,7 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: { type: "Hidden Fields", label: "plan", id: "plan" },
|
||||
elementType: { type: "Hidden Fields", label: "plan", id: "plan" },
|
||||
filterType: { filterValue: "Equals", filterComboBoxValue: "pro" },
|
||||
},
|
||||
],
|
||||
@@ -742,7 +827,7 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: { type: "Attributes", label: "role", id: "role" },
|
||||
elementType: { type: "Attributes", label: "role", id: "role" },
|
||||
filterType: { filterValue: "Not equals", filterComboBoxValue: "admin" },
|
||||
},
|
||||
],
|
||||
@@ -758,7 +843,7 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: { type: "Other Filters", label: "Language", id: "language" },
|
||||
elementType: { type: "Other Filters", label: "Language", id: "language" },
|
||||
filterType: { filterValue: "Equals", filterComboBoxValue: "en" },
|
||||
},
|
||||
],
|
||||
@@ -774,7 +859,7 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: { type: "Meta", label: "source", id: "source" },
|
||||
elementType: { type: "Meta", label: "source", id: "source" },
|
||||
filterType: { filterValue: "Not equals", filterComboBoxValue: "web" },
|
||||
},
|
||||
],
|
||||
@@ -790,16 +875,16 @@ describe("surveys", () => {
|
||||
responseStatus: "complete",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "NPS",
|
||||
id: "npsQ",
|
||||
questionType: TSurveyQuestionTypeEnum.NPS,
|
||||
elementType: TSurveyElementTypeEnum.NPS,
|
||||
},
|
||||
filterType: { filterValue: "Is more than", filterComboBoxValue: "7" },
|
||||
},
|
||||
{
|
||||
questionType: { type: "Tags", label: "Tag 1", id: "tag1" },
|
||||
elementType: { type: "Tags", label: "Tag 1", id: "tag1" },
|
||||
filterType: { filterComboBoxValue: "Applied" },
|
||||
},
|
||||
],
|
||||
@@ -818,7 +903,7 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: { type: "Meta", label: "url", id: "url" },
|
||||
elementType: { type: "Meta", label: "url", id: "url" },
|
||||
filterType: { filterValue: "Contains", filterComboBoxValue: "example.com" },
|
||||
},
|
||||
],
|
||||
@@ -846,7 +931,7 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: { type: "Meta", label: "url", id: "url" },
|
||||
elementType: { type: "Meta", label: "url", id: "url" },
|
||||
filterType: { filterValue, filterComboBoxValue: expected.value },
|
||||
},
|
||||
],
|
||||
@@ -862,7 +947,7 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: { type: "Meta", label: "url", id: "url" },
|
||||
elementType: { type: "Meta", label: "url", id: "url" },
|
||||
filterType: { filterValue: "Contains", filterComboBoxValue: "" },
|
||||
},
|
||||
],
|
||||
@@ -878,7 +963,7 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: { type: "Meta", label: "url", id: "url" },
|
||||
elementType: { type: "Meta", label: "url", id: "url" },
|
||||
filterType: { filterValue: "Contains", filterComboBoxValue: " " },
|
||||
},
|
||||
],
|
||||
@@ -894,7 +979,7 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: { type: "Meta", label: "source", id: "source" },
|
||||
elementType: { type: "Meta", label: "source", id: "source" },
|
||||
filterType: { filterValue: "Equals", filterComboBoxValue: ["google"] },
|
||||
},
|
||||
],
|
||||
@@ -910,11 +995,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: { type: "Meta", label: "url", id: "url" },
|
||||
elementType: { type: "Meta", label: "url", id: "url" },
|
||||
filterType: { filterValue: "Contains", filterComboBoxValue: "formbricks.com" },
|
||||
},
|
||||
{
|
||||
questionType: { type: "Meta", label: "source", id: "source" },
|
||||
elementType: { type: "Meta", label: "source", id: "source" },
|
||||
filterType: { filterValue: "Equals", filterComboBoxValue: ["newsletter"] },
|
||||
},
|
||||
],
|
||||
@@ -931,7 +1016,7 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: { type: "Quotas", label: "Quota 1", id: "quota1" },
|
||||
elementType: { type: "Quotas", label: "Quota 1", id: "quota1" },
|
||||
filterType: { filterComboBoxValue: "Screened in" },
|
||||
},
|
||||
],
|
||||
@@ -947,7 +1032,7 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: { type: "Quotas", label: "Quota 1", id: "quota1" },
|
||||
elementType: { type: "Quotas", label: "Quota 1", id: "quota1" },
|
||||
filterType: { filterComboBoxValue: "Screened out (overquota)" },
|
||||
},
|
||||
],
|
||||
@@ -963,7 +1048,7 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: { type: "Quotas", label: "Quota 1", id: "quota1" },
|
||||
elementType: { type: "Quotas", label: "Quota 1", id: "quota1" },
|
||||
filterType: { filterComboBoxValue: "Not in quota" },
|
||||
},
|
||||
],
|
||||
@@ -979,11 +1064,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: { type: "Quotas", label: "Quota 1", id: "quota1" },
|
||||
elementType: { type: "Quotas", label: "Quota 1", id: "quota1" },
|
||||
filterType: { filterComboBoxValue: "Screened in" },
|
||||
},
|
||||
{
|
||||
questionType: { type: "Quotas", label: "Quota 2", id: "quota2" },
|
||||
elementType: { type: "Quotas", label: "Quota 2", id: "quota2" },
|
||||
filterType: { filterComboBoxValue: "Not in quota" },
|
||||
},
|
||||
],
|
||||
|
||||
+397
-318
@@ -5,7 +5,8 @@ import {
|
||||
TSurveyContactAttributes,
|
||||
TSurveyMetaFieldFilter,
|
||||
} from "@formbricks/types/responses";
|
||||
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import {
|
||||
@@ -14,15 +15,16 @@ import {
|
||||
SelectedFilterValue,
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
||||
import {
|
||||
ElementOption,
|
||||
ElementOptions,
|
||||
OptionsType,
|
||||
QuestionOption,
|
||||
QuestionOptions,
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
|
||||
import { QuestionFilterOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter";
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ElementsComboBox";
|
||||
import { ElementFilterOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { recallToHeadline } from "@/lib/utils/recall";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
|
||||
const conditionOptions = {
|
||||
const conditionOptions: Record<string, string[]> = {
|
||||
openText: ["is"],
|
||||
multipleChoiceSingle: ["Includes either"],
|
||||
multipleChoiceMulti: ["Includes all", "Includes either"],
|
||||
@@ -39,7 +41,7 @@ const conditionOptions = {
|
||||
contactInfo: ["is"],
|
||||
ranking: ["is"],
|
||||
};
|
||||
const filterOptions = {
|
||||
const filterOptions: Record<string, string[]> = {
|
||||
openText: ["Filled out", "Skipped"],
|
||||
rating: ["1", "2", "3", "4", "5"],
|
||||
nps: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"],
|
||||
@@ -51,6 +53,51 @@ const filterOptions = {
|
||||
ranking: ["Filled out", "Skipped"],
|
||||
};
|
||||
|
||||
// Helper function to get filter options for a specific element type
|
||||
const getElementFilterOption = (
|
||||
element: ReturnType<typeof getElementsFromBlocks>[number]
|
||||
): ElementFilterOptions | null => {
|
||||
if (!Object.keys(conditionOptions).includes(element.type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseOption = {
|
||||
type: element.type,
|
||||
filterOptions: conditionOptions[element.type],
|
||||
id: element.id,
|
||||
};
|
||||
|
||||
switch (element.type) {
|
||||
case TSurveyElementTypeEnum.MultipleChoiceSingle:
|
||||
return {
|
||||
...baseOption,
|
||||
filterComboBoxOptions: element.choices?.map((c) => c.label) ?? [""],
|
||||
};
|
||||
case TSurveyElementTypeEnum.MultipleChoiceMulti:
|
||||
return {
|
||||
...baseOption,
|
||||
filterComboBoxOptions: element.choices?.filter((c) => c.id !== "other").map((c) => c.label) ?? [""],
|
||||
};
|
||||
case TSurveyElementTypeEnum.PictureSelection:
|
||||
return {
|
||||
...baseOption,
|
||||
filterComboBoxOptions: element.choices?.map((_, idx) => `Picture ${idx + 1}`) ?? [""],
|
||||
};
|
||||
case TSurveyElementTypeEnum.Matrix:
|
||||
return {
|
||||
type: element.type,
|
||||
filterOptions: element.rows.map((row) => getLocalizedValue(row.label, "default")),
|
||||
filterComboBoxOptions: element.columns.map((column) => getLocalizedValue(column.label, "default")),
|
||||
id: element.id,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
...baseOption,
|
||||
filterComboBoxOptions: filterOptions[element.type],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// URL/meta text operators mapping
|
||||
const META_OP_MAP = {
|
||||
Equals: "equals",
|
||||
@@ -63,8 +110,7 @@ const META_OP_MAP = {
|
||||
"Does not end with": "doesNotEndWith",
|
||||
} as const;
|
||||
|
||||
// creating the options for the filtering to be selected there are 4 types questions, attributes, tags and metadata
|
||||
export const generateQuestionAndFilterOptions = (
|
||||
export const generateElementAndFilterOptions = (
|
||||
survey: TSurvey,
|
||||
environmentTags: TTag[] | undefined,
|
||||
attributes: TSurveyContactAttributes,
|
||||
@@ -72,67 +118,32 @@ export const generateQuestionAndFilterOptions = (
|
||||
hiddenFields: TResponseHiddenFieldsFilter,
|
||||
quotas: TSurveyQuota[]
|
||||
): {
|
||||
questionOptions: QuestionOptions[];
|
||||
questionFilterOptions: QuestionFilterOptions[];
|
||||
elementOptions: ElementOptions[];
|
||||
elementFilterOptions: ElementFilterOptions[];
|
||||
} => {
|
||||
let questionOptions: QuestionOptions[] = [];
|
||||
let questionFilterOptions: QuestionFilterOptions[] = [];
|
||||
let elementOptions: ElementOptions[] = [];
|
||||
let elementFilterOptions: ElementFilterOptions[] = [];
|
||||
let elementsOptions: ElementOption[] = [];
|
||||
|
||||
let questionsOptions: QuestionOption[] = [];
|
||||
const elements = getElementsFromBlocks(survey.blocks);
|
||||
|
||||
survey.questions.forEach((q) => {
|
||||
elements.forEach((q) => {
|
||||
if (Object.keys(conditionOptions).includes(q.type)) {
|
||||
questionsOptions.push({
|
||||
elementsOptions.push({
|
||||
label: getTextContent(
|
||||
getLocalizedValue(recallToHeadline(q.headline, survey, false, "default"), "default")
|
||||
),
|
||||
questionType: q.type,
|
||||
type: OptionsType.QUESTIONS,
|
||||
elementType: q.type,
|
||||
type: OptionsType.ELEMENTS,
|
||||
id: q.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
questionOptions = [...questionOptions, { header: OptionsType.QUESTIONS, option: questionsOptions }];
|
||||
survey.questions.forEach((q) => {
|
||||
if (Object.keys(conditionOptions).includes(q.type)) {
|
||||
if (q.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle) {
|
||||
questionFilterOptions.push({
|
||||
type: q.type,
|
||||
filterOptions: conditionOptions[q.type],
|
||||
filterComboBoxOptions: q?.choices ? q?.choices?.map((c) => c?.label) : [""],
|
||||
id: q.id,
|
||||
});
|
||||
} else if (q.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti) {
|
||||
questionFilterOptions.push({
|
||||
type: q.type,
|
||||
filterOptions: conditionOptions[q.type],
|
||||
filterComboBoxOptions: q?.choices
|
||||
? q?.choices?.filter((c) => c.id !== "other")?.map((c) => c?.label)
|
||||
: [""],
|
||||
id: q.id,
|
||||
});
|
||||
} else if (q.type === TSurveyQuestionTypeEnum.PictureSelection) {
|
||||
questionFilterOptions.push({
|
||||
type: q.type,
|
||||
filterOptions: conditionOptions[q.type],
|
||||
filterComboBoxOptions: q?.choices ? q?.choices?.map((_, idx) => `Picture ${idx + 1}`) : [""],
|
||||
id: q.id,
|
||||
});
|
||||
} else if (q.type === TSurveyQuestionTypeEnum.Matrix) {
|
||||
questionFilterOptions.push({
|
||||
type: q.type,
|
||||
filterOptions: q.rows.map((row) => getLocalizedValue(row.label, "default")),
|
||||
filterComboBoxOptions: q.columns.map((column) => getLocalizedValue(column.label, "default")),
|
||||
id: q.id,
|
||||
});
|
||||
} else {
|
||||
questionFilterOptions.push({
|
||||
type: q.type,
|
||||
filterOptions: conditionOptions[q.type],
|
||||
filterComboBoxOptions: filterOptions[q.type],
|
||||
id: q.id,
|
||||
});
|
||||
}
|
||||
elementOptions = [...elementOptions, { header: OptionsType.ELEMENTS, option: elementsOptions }];
|
||||
elements.forEach((q) => {
|
||||
const filterOption = getElementFilterOption(q);
|
||||
if (filterOption) {
|
||||
elementFilterOptions.push(filterOption);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -140,9 +151,9 @@ export const generateQuestionAndFilterOptions = (
|
||||
return { label: t.name, type: OptionsType.TAGS, id: t.id };
|
||||
});
|
||||
if (tagsOptions && tagsOptions?.length > 0) {
|
||||
questionOptions = [...questionOptions, { header: OptionsType.TAGS, option: tagsOptions }];
|
||||
elementOptions = [...elementOptions, { header: OptionsType.TAGS, option: tagsOptions }];
|
||||
environmentTags?.forEach((t) => {
|
||||
questionFilterOptions.push({
|
||||
elementFilterOptions.push({
|
||||
type: "Tags",
|
||||
filterOptions: conditionOptions.tags,
|
||||
filterComboBoxOptions: filterOptions.tags,
|
||||
@@ -152,8 +163,8 @@ export const generateQuestionAndFilterOptions = (
|
||||
}
|
||||
|
||||
if (attributes) {
|
||||
questionOptions = [
|
||||
...questionOptions,
|
||||
elementOptions = [
|
||||
...elementOptions,
|
||||
{
|
||||
header: OptionsType.ATTRIBUTES,
|
||||
option: Object.keys(attributes).map((a) => {
|
||||
@@ -162,7 +173,7 @@ export const generateQuestionAndFilterOptions = (
|
||||
},
|
||||
];
|
||||
Object.keys(attributes).forEach((a) => {
|
||||
questionFilterOptions.push({
|
||||
elementFilterOptions.push({
|
||||
type: "Attributes",
|
||||
filterOptions: conditionOptions.userAttributes,
|
||||
filterComboBoxOptions: attributes[a],
|
||||
@@ -172,8 +183,8 @@ export const generateQuestionAndFilterOptions = (
|
||||
}
|
||||
|
||||
if (meta) {
|
||||
questionOptions = [
|
||||
...questionOptions,
|
||||
elementOptions = [
|
||||
...elementOptions,
|
||||
{
|
||||
header: OptionsType.META,
|
||||
option: Object.keys(meta).map((m) => {
|
||||
@@ -182,7 +193,7 @@ export const generateQuestionAndFilterOptions = (
|
||||
},
|
||||
];
|
||||
Object.keys(meta).forEach((m) => {
|
||||
questionFilterOptions.push({
|
||||
elementFilterOptions.push({
|
||||
type: "Meta",
|
||||
filterOptions: m === "url" ? Object.keys(META_OP_MAP) : ["Equals", "Not equals"],
|
||||
filterComboBoxOptions: meta[m],
|
||||
@@ -192,8 +203,8 @@ export const generateQuestionAndFilterOptions = (
|
||||
}
|
||||
|
||||
if (hiddenFields) {
|
||||
questionOptions = [
|
||||
...questionOptions,
|
||||
elementOptions = [
|
||||
...elementOptions,
|
||||
{
|
||||
header: OptionsType.HIDDEN_FIELDS,
|
||||
option: Object.keys(hiddenFields).map((hiddenField) => {
|
||||
@@ -202,7 +213,7 @@ export const generateQuestionAndFilterOptions = (
|
||||
},
|
||||
];
|
||||
Object.keys(hiddenFields).forEach((hiddenField) => {
|
||||
questionFilterOptions.push({
|
||||
elementFilterOptions.push({
|
||||
type: "Hidden Fields",
|
||||
filterOptions: ["Equals", "Not equals"],
|
||||
filterComboBoxOptions: hiddenFields[hiddenField],
|
||||
@@ -211,29 +222,29 @@ export const generateQuestionAndFilterOptions = (
|
||||
});
|
||||
}
|
||||
|
||||
let languageQuestion: QuestionOption[] = [];
|
||||
let languageElement: ElementOption[] = [];
|
||||
|
||||
//can be extended to include more properties
|
||||
if (survey.languages?.length > 0) {
|
||||
languageQuestion.push({ label: "Language", type: OptionsType.OTHERS, id: "language" });
|
||||
languageElement.push({ label: "Language", type: OptionsType.OTHERS, id: "language" });
|
||||
const languageOptions = survey.languages.map((sl) => sl.language.code);
|
||||
questionFilterOptions.push({
|
||||
elementFilterOptions.push({
|
||||
type: OptionsType.OTHERS,
|
||||
filterOptions: conditionOptions.languages,
|
||||
filterComboBoxOptions: languageOptions,
|
||||
id: "language",
|
||||
});
|
||||
}
|
||||
questionOptions = [...questionOptions, { header: OptionsType.OTHERS, option: languageQuestion }];
|
||||
elementOptions = [...elementOptions, { header: OptionsType.OTHERS, option: languageElement }];
|
||||
|
||||
if (quotas.length > 0) {
|
||||
const quotaOptions = quotas.map((quota) => {
|
||||
return { label: quota.name, type: OptionsType.QUOTAS, id: quota.id };
|
||||
});
|
||||
questionOptions = [...questionOptions, { header: OptionsType.QUOTAS, option: quotaOptions }];
|
||||
elementOptions = [...elementOptions, { header: OptionsType.QUOTAS, option: quotaOptions }];
|
||||
|
||||
quotas.forEach((quota) => {
|
||||
questionFilterOptions.push({
|
||||
elementFilterOptions.push({
|
||||
type: "Quotas",
|
||||
filterOptions: ["Status"],
|
||||
filterComboBoxOptions: ["Screened in", "Screened out (overquota)", "Not in quota"],
|
||||
@@ -242,7 +253,293 @@ export const generateQuestionAndFilterOptions = (
|
||||
});
|
||||
}
|
||||
|
||||
return { questionOptions: [...questionOptions], questionFilterOptions: [...questionFilterOptions] };
|
||||
return { elementOptions: [...elementOptions], elementFilterOptions: [...elementFilterOptions] };
|
||||
};
|
||||
|
||||
// Helper function to process filled out/skipped filters
|
||||
const processFilledOutSkippedFilter = (
|
||||
filterType: FilterValue["filterType"],
|
||||
elementId: string,
|
||||
filters: TResponseFilterCriteria
|
||||
) => {
|
||||
if (filterType.filterComboBoxValue === "Filled out") {
|
||||
filters.data![elementId] = { op: "filledOut" };
|
||||
} else if (filterType.filterComboBoxValue === "Skipped") {
|
||||
filters.data![elementId] = { op: "skipped" };
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to process ranking filters
|
||||
const processRankingFilter = (
|
||||
filterType: FilterValue["filterType"],
|
||||
elementId: string,
|
||||
filters: TResponseFilterCriteria
|
||||
) => {
|
||||
if (filterType.filterComboBoxValue === "Filled out") {
|
||||
filters.data![elementId] = { op: "submitted" };
|
||||
} else if (filterType.filterComboBoxValue === "Skipped") {
|
||||
filters.data![elementId] = { op: "skipped" };
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to process multiple choice filters
|
||||
const processMultipleChoiceFilter = (
|
||||
filterType: FilterValue["filterType"],
|
||||
elementId: string,
|
||||
filters: TResponseFilterCriteria
|
||||
) => {
|
||||
if (filterType.filterValue === "Includes either") {
|
||||
filters.data![elementId] = {
|
||||
op: "includesOne",
|
||||
value: filterType.filterComboBoxValue as string[],
|
||||
};
|
||||
} else if (filterType.filterValue === "Includes all") {
|
||||
filters.data![elementId] = {
|
||||
op: "includesAll",
|
||||
value: filterType.filterComboBoxValue as string[],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to process NPS/Rating filters
|
||||
const processNPSRatingFilter = (
|
||||
filterType: FilterValue["filterType"],
|
||||
elementId: string,
|
||||
filters: TResponseFilterCriteria
|
||||
) => {
|
||||
if (filterType.filterValue === "Is equal to") {
|
||||
filters.data![elementId] = {
|
||||
op: "equals",
|
||||
value: parseInt(filterType.filterComboBoxValue as string),
|
||||
};
|
||||
} else if (filterType.filterValue === "Is less than") {
|
||||
filters.data![elementId] = {
|
||||
op: "lessThan",
|
||||
value: parseInt(filterType.filterComboBoxValue as string),
|
||||
};
|
||||
} else if (filterType.filterValue === "Is more than") {
|
||||
filters.data![elementId] = {
|
||||
op: "greaterThan",
|
||||
value: parseInt(filterType.filterComboBoxValue as string),
|
||||
};
|
||||
} else if (filterType.filterValue === "Submitted") {
|
||||
filters.data![elementId] = { op: "submitted" };
|
||||
} else if (filterType.filterValue === "Skipped") {
|
||||
filters.data![elementId] = { op: "skipped" };
|
||||
} else if (filterType.filterValue === "Includes either") {
|
||||
filters.data![elementId] = {
|
||||
op: "includesOne",
|
||||
value: (filterType.filterComboBoxValue as string[]).map((value) => parseInt(value)),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to process CTA filters
|
||||
const processCTAFilter = (
|
||||
filterType: FilterValue["filterType"],
|
||||
elementId: string,
|
||||
filters: TResponseFilterCriteria
|
||||
) => {
|
||||
if (filterType.filterComboBoxValue === "Clicked") {
|
||||
filters.data![elementId] = { op: "clicked" };
|
||||
} else if (filterType.filterComboBoxValue === "Dismissed") {
|
||||
filters.data![elementId] = { op: "skipped" };
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to process Consent filters
|
||||
const processConsentFilter = (
|
||||
filterType: FilterValue["filterType"],
|
||||
elementId: string,
|
||||
filters: TResponseFilterCriteria
|
||||
) => {
|
||||
if (filterType.filterComboBoxValue === "Accepted") {
|
||||
filters.data![elementId] = { op: "accepted" };
|
||||
} else if (filterType.filterComboBoxValue === "Dismissed") {
|
||||
filters.data![elementId] = { op: "skipped" };
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to process Picture Selection filters
|
||||
const processPictureSelectionFilter = (
|
||||
filterType: FilterValue["filterType"],
|
||||
elementId: string,
|
||||
element: ReturnType<typeof getElementsFromBlocks>[number] | undefined,
|
||||
filters: TResponseFilterCriteria
|
||||
) => {
|
||||
if (
|
||||
element?.type !== TSurveyElementTypeEnum.PictureSelection ||
|
||||
!Array.isArray(filterType.filterComboBoxValue)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOptions = filterType.filterComboBoxValue.map((option) => {
|
||||
const index = parseInt(option.split(" ")[1]);
|
||||
return element?.choices[index - 1].id;
|
||||
});
|
||||
|
||||
if (filterType.filterValue === "Includes all") {
|
||||
filters.data![elementId] = { op: "includesAll", value: selectedOptions };
|
||||
} else if (filterType.filterValue === "Includes either") {
|
||||
filters.data![elementId] = { op: "includesOne", value: selectedOptions };
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to process Matrix filters
|
||||
const processMatrixFilter = (
|
||||
filterType: FilterValue["filterType"],
|
||||
elementId: string,
|
||||
filters: TResponseFilterCriteria
|
||||
) => {
|
||||
if (
|
||||
filterType.filterValue &&
|
||||
filterType.filterComboBoxValue &&
|
||||
typeof filterType.filterComboBoxValue === "string"
|
||||
) {
|
||||
filters.data![elementId] = {
|
||||
op: "matrix",
|
||||
value: { [filterType.filterValue]: filterType.filterComboBoxValue },
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to process element filters
|
||||
const processElementFilters = (
|
||||
elements: FilterValue[],
|
||||
survey: TSurvey,
|
||||
filters: TResponseFilterCriteria
|
||||
) => {
|
||||
if (!elements.length) return;
|
||||
|
||||
const surveyElements = getElementsFromBlocks(survey.blocks);
|
||||
filters.data = filters.data || {};
|
||||
|
||||
elements.forEach(({ filterType, elementType }) => {
|
||||
const elementId = elementType.id ?? "";
|
||||
const element = surveyElements.find((q) => q.id === elementId);
|
||||
|
||||
switch (elementType.elementType) {
|
||||
case TSurveyElementTypeEnum.OpenText:
|
||||
case TSurveyElementTypeEnum.Address:
|
||||
case TSurveyElementTypeEnum.ContactInfo:
|
||||
processFilledOutSkippedFilter(filterType, elementId, filters);
|
||||
break;
|
||||
case TSurveyElementTypeEnum.Ranking:
|
||||
processRankingFilter(filterType, elementId, filters);
|
||||
break;
|
||||
case TSurveyElementTypeEnum.MultipleChoiceSingle:
|
||||
case TSurveyElementTypeEnum.MultipleChoiceMulti:
|
||||
processMultipleChoiceFilter(filterType, elementId, filters);
|
||||
break;
|
||||
case TSurveyElementTypeEnum.NPS:
|
||||
case TSurveyElementTypeEnum.Rating:
|
||||
processNPSRatingFilter(filterType, elementId, filters);
|
||||
break;
|
||||
case TSurveyElementTypeEnum.CTA:
|
||||
processCTAFilter(filterType, elementId, filters);
|
||||
break;
|
||||
case TSurveyElementTypeEnum.Consent:
|
||||
processConsentFilter(filterType, elementId, filters);
|
||||
break;
|
||||
case TSurveyElementTypeEnum.PictureSelection:
|
||||
processPictureSelectionFilter(filterType, elementId, element, filters);
|
||||
break;
|
||||
case TSurveyElementTypeEnum.Matrix:
|
||||
processMatrixFilter(filterType, elementId, filters);
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to process equals/not equals filters (for hiddenFields, attributes, others)
|
||||
const processEqualsNotEqualsFilter = (
|
||||
filterType: FilterValue["filterType"],
|
||||
label: string | undefined,
|
||||
filters: TResponseFilterCriteria,
|
||||
targetKey: "data" | "contactAttributes" | "others"
|
||||
) => {
|
||||
if (!filterType.filterComboBoxValue) return;
|
||||
|
||||
if (targetKey === "data") {
|
||||
filters.data = filters.data || {};
|
||||
if (filterType.filterValue === "Equals") {
|
||||
filters.data[label ?? ""] = { op: "equals", value: filterType.filterComboBoxValue as string };
|
||||
} else if (filterType.filterValue === "Not equals") {
|
||||
filters.data[label ?? ""] = { op: "notEquals", value: filterType.filterComboBoxValue as string };
|
||||
}
|
||||
} else if (targetKey === "contactAttributes") {
|
||||
filters.contactAttributes = filters.contactAttributes || {};
|
||||
if (filterType.filterValue === "Equals") {
|
||||
filters.contactAttributes[label ?? ""] = {
|
||||
op: "equals",
|
||||
value: filterType.filterComboBoxValue as string,
|
||||
};
|
||||
} else if (filterType.filterValue === "Not equals") {
|
||||
filters.contactAttributes[label ?? ""] = {
|
||||
op: "notEquals",
|
||||
value: filterType.filterComboBoxValue as string,
|
||||
};
|
||||
}
|
||||
} else if (targetKey === "others") {
|
||||
filters.others = filters.others || {};
|
||||
if (filterType.filterValue === "Equals") {
|
||||
filters.others[label ?? ""] = { op: "equals", value: filterType.filterComboBoxValue as string };
|
||||
} else if (filterType.filterValue === "Not equals") {
|
||||
filters.others[label ?? ""] = { op: "notEquals", value: filterType.filterComboBoxValue as string };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to process meta filters
|
||||
const processMetaFilters = (meta: FilterValue[], filters: TResponseFilterCriteria) => {
|
||||
if (!meta.length) return;
|
||||
|
||||
filters.meta = filters.meta || {};
|
||||
|
||||
meta.forEach(({ filterType, elementType }) => {
|
||||
const label = elementType.label ?? "";
|
||||
const metaFilters = filters.meta!; // Safe because we initialized it above
|
||||
|
||||
// For text input cases (URL filtering)
|
||||
if (typeof filterType.filterComboBoxValue === "string" && filterType.filterComboBoxValue.length > 0) {
|
||||
const value = filterType.filterComboBoxValue.trim();
|
||||
const op = META_OP_MAP[filterType.filterValue as keyof typeof META_OP_MAP];
|
||||
if (op) {
|
||||
metaFilters[label] = { op, value };
|
||||
}
|
||||
}
|
||||
// For dropdown/select cases (existing metadata fields)
|
||||
else if (Array.isArray(filterType.filterComboBoxValue) && filterType.filterComboBoxValue.length > 0) {
|
||||
const value = filterType.filterComboBoxValue[0];
|
||||
if (filterType.filterValue === "Equals") {
|
||||
metaFilters[label] = { op: "equals", value };
|
||||
} else if (filterType.filterValue === "Not equals") {
|
||||
metaFilters[label] = { op: "notEquals", value };
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to process quota filters
|
||||
const processQuotaFilters = (quotas: FilterValue[], filters: TResponseFilterCriteria) => {
|
||||
if (!quotas.length) return;
|
||||
|
||||
filters.quotas = filters.quotas || {};
|
||||
|
||||
const statusMap: Record<string, "screenedIn" | "screenedOut" | "screenedOutNotInQuota"> = {
|
||||
"Screened in": "screenedIn",
|
||||
"Screened out (overquota)": "screenedOut",
|
||||
"Not in quota": "screenedOutNotInQuota",
|
||||
};
|
||||
|
||||
quotas.forEach(({ filterType, elementType }) => {
|
||||
const quotaId = elementType.id;
|
||||
if (!quotaId) return;
|
||||
|
||||
const op = statusMap[String(filterType.filterComboBoxValue)];
|
||||
if (op) filters.quotas![quotaId] = { op };
|
||||
});
|
||||
};
|
||||
|
||||
// get the formatted filter expression to fetch filtered responses
|
||||
@@ -253,7 +550,7 @@ export const getFormattedFilters = (
|
||||
): TResponseFilterCriteria => {
|
||||
const filters: TResponseFilterCriteria = {};
|
||||
|
||||
const questions: FilterValue[] = [];
|
||||
const elements: FilterValue[] = [];
|
||||
const tags: FilterValue[] = [];
|
||||
const attributes: FilterValue[] = [];
|
||||
const others: FilterValue[] = [];
|
||||
@@ -262,19 +559,19 @@ export const getFormattedFilters = (
|
||||
const quotas: FilterValue[] = [];
|
||||
|
||||
selectedFilter.filter.forEach((filter) => {
|
||||
if (filter.questionType?.type === "Questions") {
|
||||
questions.push(filter);
|
||||
} else if (filter.questionType?.type === "Tags") {
|
||||
if (filter.elementType?.type === "Elements") {
|
||||
elements.push(filter);
|
||||
} else if (filter.elementType?.type === "Tags") {
|
||||
tags.push(filter);
|
||||
} else if (filter.questionType?.type === "Attributes") {
|
||||
} else if (filter.elementType?.type === "Attributes") {
|
||||
attributes.push(filter);
|
||||
} else if (filter.questionType?.type === "Other Filters") {
|
||||
} else if (filter.elementType?.type === "Other Filters") {
|
||||
others.push(filter);
|
||||
} else if (filter.questionType?.type === "Meta") {
|
||||
} else if (filter.elementType?.type === "Meta") {
|
||||
meta.push(filter);
|
||||
} else if (filter.questionType?.type === "Hidden Fields") {
|
||||
} else if (filter.elementType?.type === "Hidden Fields") {
|
||||
hiddenFields.push(filter);
|
||||
} else if (filter.questionType?.type === "Quotas") {
|
||||
} else if (filter.elementType?.type === "Quotas") {
|
||||
quotas.push(filter);
|
||||
}
|
||||
});
|
||||
@@ -302,259 +599,41 @@ export const getFormattedFilters = (
|
||||
};
|
||||
tags.forEach((tag) => {
|
||||
if (tag.filterType.filterComboBoxValue === "Applied") {
|
||||
filters.tags?.applied?.push(tag.questionType.label ?? "");
|
||||
filters.tags?.applied?.push(tag.elementType.label ?? "");
|
||||
} else {
|
||||
filters.tags?.notApplied?.push(tag.questionType.label ?? "");
|
||||
filters.tags?.notApplied?.push(tag.elementType.label ?? "");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// for questions
|
||||
if (questions.length) {
|
||||
questions.forEach(({ filterType, questionType }) => {
|
||||
if (!filters.data) filters.data = {};
|
||||
switch (questionType.questionType) {
|
||||
case TSurveyQuestionTypeEnum.OpenText:
|
||||
case TSurveyQuestionTypeEnum.Address:
|
||||
case TSurveyQuestionTypeEnum.ContactInfo: {
|
||||
if (filterType.filterComboBoxValue === "Filled out") {
|
||||
filters.data[questionType.id ?? ""] = {
|
||||
op: "filledOut",
|
||||
};
|
||||
} else if (filterType.filterComboBoxValue === "Skipped") {
|
||||
filters.data[questionType.id ?? ""] = {
|
||||
op: "skipped",
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.Ranking: {
|
||||
if (filterType.filterComboBoxValue === "Filled out") {
|
||||
filters.data[questionType.id ?? ""] = {
|
||||
op: "submitted",
|
||||
};
|
||||
} else if (filterType.filterComboBoxValue === "Skipped") {
|
||||
filters.data[questionType.id ?? ""] = {
|
||||
op: "skipped",
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.MultipleChoiceSingle:
|
||||
case TSurveyQuestionTypeEnum.MultipleChoiceMulti: {
|
||||
if (filterType.filterValue === "Includes either") {
|
||||
filters.data[questionType.id ?? ""] = {
|
||||
op: "includesOne",
|
||||
value: filterType.filterComboBoxValue as string[],
|
||||
};
|
||||
} else if (filterType.filterValue === "Includes all") {
|
||||
filters.data[questionType.id ?? ""] = {
|
||||
op: "includesAll",
|
||||
value: filterType.filterComboBoxValue as string[],
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.NPS:
|
||||
case TSurveyQuestionTypeEnum.Rating: {
|
||||
if (filterType.filterValue === "Is equal to") {
|
||||
filters.data[questionType.id ?? ""] = {
|
||||
op: "equals",
|
||||
value: parseInt(filterType.filterComboBoxValue as string),
|
||||
};
|
||||
} else if (filterType.filterValue === "Is less than") {
|
||||
filters.data[questionType.id ?? ""] = {
|
||||
op: "lessThan",
|
||||
value: parseInt(filterType.filterComboBoxValue as string),
|
||||
};
|
||||
} else if (filterType.filterValue === "Is more than") {
|
||||
filters.data[questionType.id ?? ""] = {
|
||||
op: "greaterThan",
|
||||
value: parseInt(filterType.filterComboBoxValue as string),
|
||||
};
|
||||
} else if (filterType.filterValue === "Submitted") {
|
||||
filters.data[questionType.id ?? ""] = {
|
||||
op: "submitted",
|
||||
};
|
||||
} else if (filterType.filterValue === "Skipped") {
|
||||
filters.data[questionType.id ?? ""] = {
|
||||
op: "skipped",
|
||||
};
|
||||
} else if (filterType.filterValue === "Includes either") {
|
||||
filters.data[questionType.id ?? ""] = {
|
||||
op: "includesOne",
|
||||
value: (filterType.filterComboBoxValue as string[]).map((value) => parseInt(value)),
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.CTA: {
|
||||
if (filterType.filterComboBoxValue === "Clicked") {
|
||||
filters.data[questionType.id ?? ""] = {
|
||||
op: "clicked",
|
||||
};
|
||||
} else if (filterType.filterComboBoxValue === "Dismissed") {
|
||||
filters.data[questionType.id ?? ""] = {
|
||||
op: "skipped",
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.Consent: {
|
||||
if (filterType.filterComboBoxValue === "Accepted") {
|
||||
filters.data[questionType.id ?? ""] = {
|
||||
op: "accepted",
|
||||
};
|
||||
} else if (filterType.filterComboBoxValue === "Dismissed") {
|
||||
filters.data[questionType.id ?? ""] = {
|
||||
op: "skipped",
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.PictureSelection: {
|
||||
const questionId = questionType.id ?? "";
|
||||
const question = survey.questions.find((q) => q.id === questionId);
|
||||
|
||||
if (
|
||||
question?.type !== TSurveyQuestionTypeEnum.PictureSelection ||
|
||||
!Array.isArray(filterType.filterComboBoxValue)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOptions = filterType.filterComboBoxValue.map((option) => {
|
||||
const index = parseInt(option.split(" ")[1]);
|
||||
return question?.choices[index - 1].id;
|
||||
});
|
||||
|
||||
if (filterType.filterValue === "Includes all") {
|
||||
filters.data[questionId] = {
|
||||
op: "includesAll",
|
||||
value: selectedOptions,
|
||||
};
|
||||
} else if (filterType.filterValue === "Includes either") {
|
||||
filters.data[questionId] = {
|
||||
op: "includesOne",
|
||||
value: selectedOptions,
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.Matrix: {
|
||||
if (
|
||||
filterType.filterValue &&
|
||||
filterType.filterComboBoxValue &&
|
||||
typeof filterType.filterComboBoxValue === "string"
|
||||
) {
|
||||
filters.data[questionType.id ?? ""] = {
|
||||
op: "matrix",
|
||||
value: { [filterType.filterValue]: filterType.filterComboBoxValue },
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
processElementFilters(elements, survey, filters);
|
||||
|
||||
// for hidden fields
|
||||
if (hiddenFields.length) {
|
||||
hiddenFields.forEach(({ filterType, questionType }) => {
|
||||
if (!filters.data) filters.data = {};
|
||||
if (!filterType.filterComboBoxValue) return;
|
||||
if (filterType.filterValue === "Equals") {
|
||||
filters.data[questionType.label ?? ""] = {
|
||||
op: "equals",
|
||||
value: filterType.filterComboBoxValue as string,
|
||||
};
|
||||
} else if (filterType.filterValue === "Not equals") {
|
||||
filters.data[questionType.label ?? ""] = {
|
||||
op: "notEquals",
|
||||
value: filterType.filterComboBoxValue as string,
|
||||
};
|
||||
}
|
||||
filters.data = filters.data || {};
|
||||
hiddenFields.forEach(({ filterType, elementType }) => {
|
||||
processEqualsNotEqualsFilter(filterType, elementType.label, filters, "data");
|
||||
});
|
||||
}
|
||||
|
||||
// for attributes
|
||||
if (attributes.length) {
|
||||
attributes.forEach(({ filterType, questionType }) => {
|
||||
if (!filters.contactAttributes) filters.contactAttributes = {};
|
||||
if (!filterType.filterComboBoxValue) return;
|
||||
if (filterType.filterValue === "Equals") {
|
||||
filters.contactAttributes[questionType.label ?? ""] = {
|
||||
op: "equals",
|
||||
value: filterType.filterComboBoxValue as string,
|
||||
};
|
||||
} else if (filterType.filterValue === "Not equals") {
|
||||
filters.contactAttributes[questionType.label ?? ""] = {
|
||||
op: "notEquals",
|
||||
value: filterType.filterComboBoxValue as string,
|
||||
};
|
||||
}
|
||||
filters.contactAttributes = filters.contactAttributes || {};
|
||||
attributes.forEach(({ filterType, elementType }) => {
|
||||
processEqualsNotEqualsFilter(filterType, elementType.label, filters, "contactAttributes");
|
||||
});
|
||||
}
|
||||
|
||||
// for others
|
||||
if (others.length) {
|
||||
others.forEach(({ filterType, questionType }) => {
|
||||
if (!filters.others) filters.others = {};
|
||||
if (!filterType.filterComboBoxValue) return;
|
||||
if (filterType.filterValue === "Equals") {
|
||||
filters.others[questionType.label ?? ""] = {
|
||||
op: "equals",
|
||||
value: filterType.filterComboBoxValue as string,
|
||||
};
|
||||
} else if (filterType.filterValue === "Not equals") {
|
||||
filters.others[questionType.label ?? ""] = {
|
||||
op: "notEquals",
|
||||
value: filterType.filterComboBoxValue as string,
|
||||
};
|
||||
}
|
||||
filters.others = filters.others || {};
|
||||
others.forEach(({ filterType, elementType }) => {
|
||||
processEqualsNotEqualsFilter(filterType, elementType.label, filters, "others");
|
||||
});
|
||||
}
|
||||
|
||||
// for meta
|
||||
if (meta.length) {
|
||||
meta.forEach(({ filterType, questionType }) => {
|
||||
if (!filters.meta) filters.meta = {};
|
||||
|
||||
// For text input cases (URL filtering)
|
||||
if (typeof filterType.filterComboBoxValue === "string" && filterType.filterComboBoxValue.length > 0) {
|
||||
const value = filterType.filterComboBoxValue.trim();
|
||||
const op = META_OP_MAP[filterType.filterValue as keyof typeof META_OP_MAP];
|
||||
if (op) {
|
||||
filters.meta[questionType.label ?? ""] = { op, value };
|
||||
}
|
||||
}
|
||||
// For dropdown/select cases (existing metadata fields)
|
||||
else if (Array.isArray(filterType.filterComboBoxValue) && filterType.filterComboBoxValue.length > 0) {
|
||||
const value = filterType.filterComboBoxValue[0]; // Take first selected value
|
||||
if (filterType.filterValue === "Equals") {
|
||||
filters.meta[questionType.label ?? ""] = { op: "equals", value };
|
||||
} else if (filterType.filterValue === "Not equals") {
|
||||
filters.meta[questionType.label ?? ""] = { op: "notEquals", value };
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (quotas.length) {
|
||||
quotas.forEach(({ filterType, questionType }) => {
|
||||
filters.quotas ??= {};
|
||||
const quotaId = questionType.id;
|
||||
if (!quotaId) return;
|
||||
|
||||
const statusMap: Record<string, "screenedIn" | "screenedOut" | "screenedOutNotInQuota"> = {
|
||||
"Screened in": "screenedIn",
|
||||
"Screened out (overquota)": "screenedOut",
|
||||
"Not in quota": "screenedOutNotInQuota",
|
||||
};
|
||||
const op = statusMap[String(filterType.filterComboBoxValue)];
|
||||
if (op) filters.quotas[quotaId] = { op };
|
||||
});
|
||||
}
|
||||
processMetaFilters(meta, filters);
|
||||
processQuotaFilters(quotas, filters);
|
||||
|
||||
return filters;
|
||||
};
|
||||
|
||||
+3235
-2028
File diff suppressed because it is too large
Load Diff
+24
-23
@@ -1121,9 +1121,9 @@ checksums:
|
||||
environments/surveys/edit/add: 5196f5cd4ba3a6ac8edef91345e17f66
|
||||
environments/surveys/edit/add_a_delay_or_auto_close_the_survey: b5fa358bf3ff324014060eb0baf6dd2f
|
||||
environments/surveys/edit/add_a_four_digit_pin: 953cb3673d2135923e3b4474d33ffb2c
|
||||
environments/surveys/edit/add_a_new_question_to_your_survey: 65f3a4f0d5132eab7aeaed1ad28df56c
|
||||
environments/surveys/edit/add_a_variable_to_calculate: c202b50c12fc6f71f06eaf6f1b61e961
|
||||
environments/surveys/edit/add_action_below: 46cdbf9a77391aa89593908e508f7af0
|
||||
environments/surveys/edit/add_block: ae8fbf8fdb5c6be7e4951a6cdd486473
|
||||
environments/surveys/edit/add_choice_below: abf0416f7a78df61869de63d9766683c
|
||||
environments/surveys/edit/add_color_coding: db738f7be21e08c5dc878c09fdf95e44
|
||||
environments/surveys/edit/add_color_coding_description: da15c619aa00084ad18f30766906527f
|
||||
@@ -1144,8 +1144,8 @@ checksums:
|
||||
environments/surveys/edit/add_other: de75bd3d40f3b5effdbe1c8d536f936b
|
||||
environments/surveys/edit/add_photo_or_video: 7fd213e807ad060e415d1d4195397473
|
||||
environments/surveys/edit/add_pin: 1bc282dd7eaea51301655d3e8dd3a9fb
|
||||
environments/surveys/edit/add_question: 10336b52895385f7390540ad5bb4e208
|
||||
environments/surveys/edit/add_question_below: 58e64eb2e013f1175ea0dcf79149109f
|
||||
environments/surveys/edit/add_question_to_block: 8589b1042aa93531a836549d6036492c
|
||||
environments/surveys/edit/add_row: a613cef4caf1f0e05697c8de5164e2a3
|
||||
environments/surveys/edit/add_variable: 23f97e23aba763cc58934df4fa13ffc1
|
||||
environments/surveys/edit/address_fields: 9cabb97c3deaff4f6cb3afc3d5cfaf0a
|
||||
@@ -1171,12 +1171,14 @@ checksums:
|
||||
environments/surveys/edit/automatically_mark_the_survey_as_complete_after: c6ede2a5515a4ca72b36aec2583f43aa
|
||||
environments/surveys/edit/back_button_label: 25af945e77336724b5276de291cc92d9
|
||||
environments/surveys/edit/background_styling: 4e1e6fd2ec767bbff8767f6c0f68a731
|
||||
environments/surveys/edit/block_deleted: c682259eb138ad84f8b4441abfd9b572
|
||||
environments/surveys/edit/block_duplicated: dc9e9fab2b1cd91f6c265324b34c6376
|
||||
environments/surveys/edit/bold: 4d7306bc355ed2befd6a9237c5452ee6
|
||||
environments/surveys/edit/brand_color: 84ddb5736deb9f5c081ffe4962a6c63e
|
||||
environments/surveys/edit/brightness: 45425b6db1872225bfff71cf619d0e64
|
||||
environments/surveys/edit/button_external: d2de24e06574622baf1c0cdd1b718b1a
|
||||
environments/surveys/edit/button_external_description: cbd10d494a70b362bfee811e012c45b1
|
||||
environments/surveys/edit/button_label: db3cd7c74f393187bd780c5c3d8b9b4f
|
||||
environments/surveys/edit/button_to_continue_in_survey: 931d87aaf360ab7521f9dd75795a42d0
|
||||
environments/surveys/edit/button_to_link_to_external_url: 7c7cf54e8dc86240b86964133e802888
|
||||
environments/surveys/edit/button_url: 6f39f649a165a11873c11ea6403dba90
|
||||
environments/surveys/edit/cal_username: a4a9c739af909d975beb1bc4998feae9
|
||||
environments/surveys/edit/calculate: c5fcf8d3a38706ae2071b6f78339ec68
|
||||
@@ -1215,6 +1217,7 @@ checksums:
|
||||
environments/surveys/edit/character_limit_toggle_title: fdc45bcc6335e5116aec895fecda0d87
|
||||
environments/surveys/edit/checkbox_label: 12a07d6bdf38e283a2e95892ec49b7f8
|
||||
environments/surveys/edit/choose_the_actions_which_trigger_the_survey: 773b311a148a112243f3b139506b9987
|
||||
environments/surveys/edit/choose_the_first_question_on_your_block: bdece06ca04f89d0c445ba1554dd5b80
|
||||
environments/surveys/edit/choose_where_to_run_the_survey: ad87bcae97c445f1fd9ac110ea24f117
|
||||
environments/surveys/edit/city: 1831f32e1babbb29af27fac3053504a2
|
||||
environments/surveys/edit/close_survey_on_response_limit: 256d0bccdbcbb3d20e39aabc5b376e5e
|
||||
@@ -1238,10 +1241,12 @@ checksums:
|
||||
environments/surveys/edit/create_group: 4566e056e5217dc02a383105892fe18c
|
||||
environments/surveys/edit/create_your_own_survey: e3ddd53e0cfa409ca8dccfb3d77933e7
|
||||
environments/surveys/edit/css_selector: 615e9f1b74622df29de28a5b5614c6fe
|
||||
environments/surveys/edit/cta_button_label: ec070ffba38eae24751bb3a4c1e14c81
|
||||
environments/surveys/edit/custom_hostname: bc2b1c8de3f9b8ef145b45aeba6ab429
|
||||
environments/surveys/edit/darken_or_lighten_background_of_your_choice: 304a64a8050ebf501d195e948cd25b6f
|
||||
environments/surveys/edit/date_format: e95dfc41ac944874868487457ddc057a
|
||||
environments/surveys/edit/days_before_showing_this_survey_again: 354fb28c5ff076f022d82a20c749ee46
|
||||
environments/surveys/edit/delete_block: c00617cb0724557e486304276063807a
|
||||
environments/surveys/edit/delete_choice: fd750208d414b9ad8c980c161a0199e1
|
||||
environments/surveys/edit/disable_the_visibility_of_survey_progress: 2af631010114307ac2a91612559c9618
|
||||
environments/surveys/edit/display_an_estimate_of_completion_time_for_survey: 03f0a816569399c1c61d08dbc913de06
|
||||
@@ -1253,9 +1258,12 @@ checksums:
|
||||
environments/surveys/edit/does_not_include_all_of: c18c1a71e6d96c681a3e95c7bd6c9482
|
||||
environments/surveys/edit/does_not_include_one_of: 91090d2e0667faf654f6a81d9857440f
|
||||
environments/surveys/edit/does_not_start_with: 9395869b54cdfb353a51a7e0864f4fd7
|
||||
environments/surveys/edit/duplicate_block: d4ea4afb5fc5b18a81cbe0302fa05997
|
||||
environments/surveys/edit/duplicate_question: 910751de01fdd327165968214717711b
|
||||
environments/surveys/edit/edit_link: 40ba9e15beac77a46c5baf30be84ac54
|
||||
environments/surveys/edit/edit_recall: 38a4a7378d02453e35d06f2532eef318
|
||||
environments/surveys/edit/edit_translations: 2b21bea4b53e88342559272701e9fbf3
|
||||
environments/surveys/edit/element_not_found: 196777ff6811dd177971ffc8e27a72c1
|
||||
environments/surveys/edit/enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey: c70466147d49dcbb3686452f35c46428
|
||||
environments/surveys/edit/enable_recaptcha_to_protect_your_survey_from_spam: 4483a5763718d201ac97caa1e1216e13
|
||||
environments/surveys/edit/enable_spam_protection: e1fb0dd0723044bf040b92d8fc58015d
|
||||
@@ -1331,11 +1339,12 @@ checksums:
|
||||
environments/surveys/edit/hidden_field_used_in_recall: 70dee46bae18209e8861b654ff9a04ae
|
||||
environments/surveys/edit/hidden_field_used_in_recall_ending_card: a985d03d18e33d83521961c9c981d0ee
|
||||
environments/surveys/edit/hidden_field_used_in_recall_welcome: 22fef7001d5e60edbf877e7b435c1991
|
||||
environments/surveys/edit/hide_advanced_settings: ffa251d7762030b72c12e92f3c69a9b4
|
||||
environments/surveys/edit/hide_back_button: 9f355fb4a8e80485b9de521a952ffeb9
|
||||
environments/surveys/edit/hide_back_button_description: caaa30cf43c5611577933a1c9f44b9ee
|
||||
environments/surveys/edit/hide_block_settings: c24c3d3892c251792e297cdc036d2fde
|
||||
environments/surveys/edit/hide_logo: eef4de2e3fffe8cbe32bff4f6f7250d8
|
||||
environments/surveys/edit/hide_progress_bar: 7eefe7db6a051105bded521d94204933
|
||||
environments/surveys/edit/hide_question_settings: 99127cd016db2f7fc80333b36473c0ef
|
||||
environments/surveys/edit/hide_the_logo_in_this_specific_survey: 29d4c6c714886e57bc29ad292d0f5a00
|
||||
environments/surveys/edit/hostname: 9bdaa7692869999df51bb60d58d9ef62
|
||||
environments/surveys/edit/how_funky_do_you_want_your_cards_in_survey_type_derived_surveys: 3cb16b37510c01af20a80f51b598346e
|
||||
@@ -1362,6 +1371,7 @@ checksums:
|
||||
environments/surveys/edit/is_clicked: 8977b8cc9ff07d2b8bdb81bb41bb55cf
|
||||
environments/surveys/edit/is_completely_submitted: 8c8f0c0a9cf81dac16e486b2f5cdbb3b
|
||||
environments/surveys/edit/is_empty: dca87bc415341b1cdf9523f3b795a313
|
||||
environments/surveys/edit/is_not_clicked: 04ac5678998edbdf9f431af74bd480da
|
||||
environments/surveys/edit/is_not_empty: 8e53d702b296f172386b1277a8699050
|
||||
environments/surveys/edit/is_not_set: c1a6fd89387686d3a5426a768bb286e9
|
||||
environments/surveys/edit/is_partially_submitted: f5acf840b87d0d42c69d49a5714a86f3
|
||||
@@ -1369,7 +1379,7 @@ checksums:
|
||||
environments/surveys/edit/is_skipped: 9fb90b6578f603cca37d4e6c912bb401
|
||||
environments/surveys/edit/is_submitted: 13e774a97ad5f5609555e6f99514e70f
|
||||
environments/surveys/edit/italic: 555c60fb1d12ae305136202afa6deb3d
|
||||
environments/surveys/edit/jump_to_question: 742aabed8845190825418aa429f01b2d
|
||||
environments/surveys/edit/jump_to_block: 2fc00bd725c44f98861051c57bb2c392
|
||||
environments/surveys/edit/keep_current_order: a7c944ad6b3515f2c4f83a2c81f8fc26
|
||||
environments/surveys/edit/keep_showing_while_conditions_match: 2574802d87bd6da151c9145aacce7281
|
||||
environments/surveys/edit/key: 3d1065ab98a1c2f1210507fd5c7bf515
|
||||
@@ -1383,16 +1393,18 @@ checksums:
|
||||
environments/surveys/edit/logic_error_warning: 542fbb918ffdb29e6f9a4a6196ffb558
|
||||
environments/surveys/edit/logic_error_warning_text: f2afad8852a95ed169a39959efbf592c
|
||||
environments/surveys/edit/long_answer: 3a97f8d2e90aba6e679917a0c5670c53
|
||||
environments/surveys/edit/long_answer_toggle_description: 86bcdfeb74d9825c2f2d5a215e92d111
|
||||
environments/surveys/edit/lower_label: 45985bca022d4370bd6e013af75d5160
|
||||
environments/surveys/edit/manage_languages: 9c56d5afee8a73dfc283a452470f3a10
|
||||
environments/surveys/edit/matrix_all_fields: 187240509163b2f52a400a565e57c67f
|
||||
environments/surveys/edit/matrix_rows: 8f41f34e6ca28221cf1ebd948af4c151
|
||||
environments/surveys/edit/max_file_size: 3d35a22048f4d22e24da698fb5fb77d7
|
||||
environments/surveys/edit/max_file_size_limit_is: 78998639cde3587cecb272ba47e05f9e
|
||||
environments/surveys/edit/move_question_to_block: e8d7ef1e2f727921cb7f5788849492ad
|
||||
environments/surveys/edit/multiply: 89a0bb629167f97750ae1645a46ced0d
|
||||
environments/surveys/edit/needed_for_self_hosted_cal_com_instance: d241e72f0332177d32ce6c35070757dc
|
||||
environments/surveys/edit/next_block: 53eaa5b1c9333455ab1e99bedd222ba2
|
||||
environments/surveys/edit/next_button_label: e23522dd38f3eabeeccd3f48f32b73a8
|
||||
environments/surveys/edit/next_question: 2e0f1ea264fb4bfcb8378b2b0cf7c18f
|
||||
environments/surveys/edit/no_hidden_fields_yet_add_first_one_below: 9cc6cab3a6a42dbf835215897b5b8516
|
||||
environments/surveys/edit/no_images_found_for: 90f10f4611ed7b115a49595409b66ebe
|
||||
environments/surveys/edit/no_languages_found_add_first_one_to_get_started: 22d7782c8504daf693cab3cf7135d6e3
|
||||
@@ -1502,17 +1514,17 @@ checksums:
|
||||
environments/surveys/edit/set_the_global_placement_in_the_look_feel_settings: e34e579e778a918733702edb041ac929
|
||||
environments/surveys/edit/settings_saved_successfully: eb109269bc59dd67ae09fd9eb53652d2
|
||||
environments/surveys/edit/seven_points: 4ead50fdfda45e8710767e1b1a84bf42
|
||||
environments/surveys/edit/show_advanced_settings: b6f5bbbb84f34e51cd72ccd332e9613e
|
||||
environments/surveys/edit/show_block_settings: bad99d99c9908874e45f5c350a88cc79
|
||||
environments/surveys/edit/show_button: 6b364aac9d7ac71f34a438607c9693bc
|
||||
environments/surveys/edit/show_language_switch: b6915a7f26d7079f2d4d844d74440413
|
||||
environments/surveys/edit/show_multiple_times: 05239c532c9c05ef5d2990ba6ce12f60
|
||||
environments/surveys/edit/show_only_once: 31858baf60ebcf193c7e35d9084af0af
|
||||
environments/surveys/edit/show_question_settings: a84698a95df0833a35d653edcdbbe501
|
||||
environments/surveys/edit/show_survey_maximum_of: 721ed61b01a9fc8ce4becb72823bb72e
|
||||
environments/surveys/edit/show_survey_to_users: d5e90fd17babfea978fce826e9df89b0
|
||||
environments/surveys/edit/show_to_x_percentage_of_targeted_users: b745169011fa7e8ca475baa5500c5197
|
||||
environments/surveys/edit/simple: 65575bd903091299bc4a94b7517a6288
|
||||
environments/surveys/edit/six_points: c6c09b3f07171dc388cb5a610ea79af7
|
||||
environments/surveys/edit/skip_button_label: bfc8993b0f13e6f4fc9ef0c570b808e3
|
||||
environments/surveys/edit/smiley: e68e3b28fc3c04255e236c6a0feb662b
|
||||
environments/surveys/edit/spam_protection_note: 94059310d07c30f6704e216297036d05
|
||||
environments/surveys/edit/spam_protection_threshold_description: ed8b8c9c583077a88bf5dd3ec8b59e60
|
||||
@@ -1533,6 +1545,7 @@ checksums:
|
||||
environments/surveys/edit/survey_placement: 083c10f257337f9648bf9d435b18ec2c
|
||||
environments/surveys/edit/survey_trigger: f0c7014a684ca566698b87074fad5579
|
||||
environments/surveys/edit/switch_multi_lanugage_on_to_get_started: d2ca06684af26bd6b5121a4656bb6458
|
||||
environments/surveys/edit/target_block_not_found: 0a0c401017ab32364fec2fcbf815d832
|
||||
environments/surveys/edit/targeted: ca615f1fc3b490d5a2187b27fb4a2073
|
||||
environments/surveys/edit/ten_points: a1317b82003859f77fb3138c55450d63
|
||||
environments/surveys/edit/the_survey_will_be_shown_multiple_times_until_they_respond: 2d8d7d2351bd7533eb3788cce228c654
|
||||
@@ -1552,6 +1565,7 @@ checksums:
|
||||
environments/surveys/edit/unlock_targeting_title: 6098caf969cac64cd54e217471ae42d4
|
||||
environments/surveys/edit/unsaved_changes_warning: a164f276c9f7344022aa4640b32abcf9
|
||||
environments/surveys/edit/until_they_submit_a_response: 2a0fd5dcc6cc40a72ed9b974f22eaf68
|
||||
environments/surveys/edit/untitled_block: fdaa045139deff5cc65fa027df0cc22e
|
||||
environments/surveys/edit/upgrade_notice_description: 32b66a4f257ad8d38bc38dcc95fe23c4
|
||||
environments/surveys/edit/upgrade_notice_title: 40866066ebc558ad0c92a4f19f12090c
|
||||
environments/surveys/edit/upload: 4a6c84aa16db0f4e5697f49b45257bc7
|
||||
@@ -1923,7 +1937,6 @@ checksums:
|
||||
templates/card_abandonment_survey: 705c3dfcc7f6de3a445aaefe0d68c43f
|
||||
templates/card_abandonment_survey_description: a3db29212b51402a7659a76248299798
|
||||
templates/card_abandonment_survey_question_1_button_label: 6208ac076107506686eb8eae42ac4450
|
||||
templates/card_abandonment_survey_question_1_dismiss_button_label: 17961ce57f78e2cbfded4590014e5e06
|
||||
templates/card_abandonment_survey_question_1_headline: d19fc64f80ef192b124f4f9fb070bccc
|
||||
templates/card_abandonment_survey_question_1_html: 2a4cbf4a5cc305109d23baa9896a9010
|
||||
templates/card_abandonment_survey_question_2_choice_1: 7723bcd15400a40303409716854f88f9
|
||||
@@ -2010,12 +2023,10 @@ checksums:
|
||||
templates/churn_survey_question_2_button_label: 76a8497d7b546628b03bb81d5c1ce995
|
||||
templates/churn_survey_question_2_headline: 17d3e7e2ce62af5ef9332c0d208f9172
|
||||
templates/churn_survey_question_3_button_label: 43834ccf20c1c7cd49382468abe2edce
|
||||
templates/churn_survey_question_3_dismiss_button_label: b7f28dfa2f58b80b149bb82b392d0291
|
||||
templates/churn_survey_question_3_headline: 76444078de5c30666ff65f453f60b420
|
||||
templates/churn_survey_question_3_html: 4f723d2aea95570d6fc4559519611b8e
|
||||
templates/churn_survey_question_4_headline: c64605fecd9342dffe904d809e9e3762
|
||||
templates/churn_survey_question_5_button_label: 03e28ea8c2c970cd1b532fee14b22e2b
|
||||
templates/churn_survey_question_5_dismiss_button_label: b7f28dfa2f58b80b149bb82b392d0291
|
||||
templates/churn_survey_question_5_headline: bab9054d83ebc8c67a5bfe7edcb29c85
|
||||
templates/churn_survey_question_5_html: da3da01f91e3e922ea4d09c4bd836023
|
||||
templates/collect_feedback_description: 450c46ad8406e6ac92940a80ed24c000
|
||||
@@ -2122,6 +2133,7 @@ checksums:
|
||||
templates/csat_survey_question_3_headline: 25974b7f1692cad41908fe305830b6c0
|
||||
templates/csat_survey_question_3_placeholder: 3ec4d0b6bb1f26bb2c32e9e8bd377ae3
|
||||
templates/cta_description: bc94a2ddc965b286a8677b0642696c7e
|
||||
templates/custom_survey_block_1_name: 5e1b4dce0cb70662441b663507a69454
|
||||
templates/custom_survey_description: 0492afdea2ef1bd683eaf48a2bad2caa
|
||||
templates/custom_survey_name: 6fc756927ca9ea22c26368cccd64a67e
|
||||
templates/custom_survey_question_1_headline: 0abf9d41e0b5c5567c3833fd63048398
|
||||
@@ -2208,7 +2220,6 @@ checksums:
|
||||
templates/evaluate_a_product_idea_description: 734295caa08aac718e9ee01a99c3debe
|
||||
templates/evaluate_a_product_idea_name: b0d8039556d686b83dfcd455092b9d9c
|
||||
templates/evaluate_a_product_idea_question_1_button_label: 102449dc2f516eb6259c39fa4ed9c56a
|
||||
templates/evaluate_a_product_idea_question_1_dismiss_button_label: b7f28dfa2f58b80b149bb82b392d0291
|
||||
templates/evaluate_a_product_idea_question_1_headline: c94096ba66ad74fb3bbfaaa06bd709a0
|
||||
templates/evaluate_a_product_idea_question_1_html: bc0dcb887591e018dfeeb65a3a5c4bb9
|
||||
templates/evaluate_a_product_idea_question_2_headline: 10a50778c4559554336e7289a48d021c
|
||||
@@ -2217,7 +2228,6 @@ checksums:
|
||||
templates/evaluate_a_product_idea_question_3_headline: 69407cff7b3e2706bdc86cb425e88918
|
||||
templates/evaluate_a_product_idea_question_3_placeholder: 3ec4d0b6bb1f26bb2c32e9e8bd377ae3
|
||||
templates/evaluate_a_product_idea_question_4_button_label: 89ddbcf710eba274963494f312bdc8a9
|
||||
templates/evaluate_a_product_idea_question_4_dismiss_button_label: b7f28dfa2f58b80b149bb82b392d0291
|
||||
templates/evaluate_a_product_idea_question_4_headline: e7e5b5234f617f38f09b2cac639a7ef8
|
||||
templates/evaluate_a_product_idea_question_4_html: 8902a0d7738376818d2729644321438f
|
||||
templates/evaluate_a_product_idea_question_5_headline: 1d573c2338e6ba5d3cccb09c785bd8c3
|
||||
@@ -2267,7 +2277,6 @@ checksums:
|
||||
templates/feedback_box_question_2_headline: 878b8f17dc18877bfbc07823113cd5d5
|
||||
templates/feedback_box_question_2_subheader: 476ff43369a72225b01633e1bce59b95
|
||||
templates/feedback_box_question_3_button_label: c631d5b3f14b581c303b782221582fe7
|
||||
templates/feedback_box_question_3_dismiss_button_label: 0d5962c08cdca1a2804dfc4abc308a8f
|
||||
templates/feedback_box_question_3_headline: 5cfb173d156555227fbc2c97ad921e72
|
||||
templates/feedback_box_question_3_html: 7e5877860eec80971969ae83c89b30f6
|
||||
templates/feedback_box_question_4_button_label: 1050569a1ea31d070e0cee55bcab3494
|
||||
@@ -2292,7 +2301,6 @@ checksums:
|
||||
templates/identify_sign_up_barriers_description: 5b2fbee8c425d7a4d0706ec3628cea11
|
||||
templates/identify_sign_up_barriers_name: 3bbc5352dfa7a9c237bc2c6b21b608dd
|
||||
templates/identify_sign_up_barriers_question_1_button_label: 080fd22c580f56ffdcea6c3d60448b84
|
||||
templates/identify_sign_up_barriers_question_1_dismiss_button_label: 0d5962c08cdca1a2804dfc4abc308a8f
|
||||
templates/identify_sign_up_barriers_question_1_headline: c8c247363daf4697e1939aaf8dc5770c
|
||||
templates/identify_sign_up_barriers_question_1_html: 51029ae64c19101af608684b6f429eb8
|
||||
templates/identify_sign_up_barriers_question_2_headline: f768ea3053b07f6bbcba977f714ec3da
|
||||
@@ -2315,7 +2323,6 @@ checksums:
|
||||
templates/identify_sign_up_barriers_question_8_headline: 1f4ee5675d0d84bf049052be26549037
|
||||
templates/identify_sign_up_barriers_question_8_placeholder: 3ec4d0b6bb1f26bb2c32e9e8bd377ae3
|
||||
templates/identify_sign_up_barriers_question_9_button_label: 0dd2ae69be4618c1f9e615774a4509ca
|
||||
templates/identify_sign_up_barriers_question_9_dismiss_button_label: b8bf7f2b6e67a523dc4ff5ce009cdb72
|
||||
templates/identify_sign_up_barriers_question_9_headline: 54d02e5c8eeb10fed40e2e82f7399f8c
|
||||
templates/identify_sign_up_barriers_question_9_html: ed87aa8d325b6063d4150431e9f80ef0
|
||||
templates/identify_upsell_opportunities_description: ed6b8dcb162076a380955d7c98482b06
|
||||
@@ -2352,7 +2359,6 @@ checksums:
|
||||
templates/improve_newsletter_content_question_2_headline: abbea0e97841b617a878f1de2c968d0e
|
||||
templates/improve_newsletter_content_question_2_placeholder: 3ec4d0b6bb1f26bb2c32e9e8bd377ae3
|
||||
templates/improve_newsletter_content_question_3_button_label: 5d5352aba5272de9b1337909d49d4a4c
|
||||
templates/improve_newsletter_content_question_3_dismiss_button_label: 6a6d6f71da4a44cca4fe5ad09f83a9d2
|
||||
templates/improve_newsletter_content_question_3_headline: fcd056a1581f5a538aad57641cd0abad
|
||||
templates/improve_newsletter_content_question_3_html: 102e73f836fe99b6c333c88c730fa25b
|
||||
templates/improve_trial_conversion_description: 3187c4ac1de993326a988c6665d3d4ae
|
||||
@@ -2367,7 +2373,6 @@ checksums:
|
||||
templates/improve_trial_conversion_question_2_button_label: 89ddbcf710eba274963494f312bdc8a9
|
||||
templates/improve_trial_conversion_question_2_headline: 05dd4820f60b9d267a9affc7e662f029
|
||||
templates/improve_trial_conversion_question_4_button_label: d94a6a11cfdf4ebde4c5332e585e2e96
|
||||
templates/improve_trial_conversion_question_4_dismiss_button_label: b7f28dfa2f58b80b149bb82b392d0291
|
||||
templates/improve_trial_conversion_question_4_headline: 9b07341f65574c4165086ec107cebb45
|
||||
templates/improve_trial_conversion_question_4_html: 95d13979f92aa0e6c5bce6613ad3b417
|
||||
templates/improve_trial_conversion_question_5_button_label: 89ddbcf710eba274963494f312bdc8a9
|
||||
@@ -2531,7 +2536,6 @@ checksums:
|
||||
templates/preview_survey_question_2_choice_2_label: 1af148222f327f28cf0db6513de5989e
|
||||
templates/preview_survey_question_2_headline: 5cfb173d156555227fbc2c97ad921e72
|
||||
templates/preview_survey_welcome_card_headline: 8778dc41547a2778d0f9482da989fc00
|
||||
templates/preview_survey_welcome_card_html: 5fc24f7cfeba1af9a3fc3ddb6fb67de4
|
||||
templates/prioritize_features_description: 1eae41fad0e3947f803d8539081e59ec
|
||||
templates/prioritize_features_name: 4ca59ff1f9c319aaa68c3106d820fd6a
|
||||
templates/prioritize_features_question_1_choice_1: 7c0b2da44eacc271073d4f15caaa86c8
|
||||
@@ -2557,7 +2561,6 @@ checksums:
|
||||
templates/product_market_fit_superhuman: 48b1b2db74562dea0d00483b29942346
|
||||
templates/product_market_fit_superhuman_description: d14c8e7f4eb7c98919de171457d10a31
|
||||
templates/product_market_fit_superhuman_question_1_button_label: 5d5352aba5272de9b1337909d49d4a4c
|
||||
templates/product_market_fit_superhuman_question_1_dismiss_button_label: 17961ce57f78e2cbfded4590014e5e06
|
||||
templates/product_market_fit_superhuman_question_1_headline: 21a16bc7bc801fdd743ad37354eedbfb
|
||||
templates/product_market_fit_superhuman_question_1_html: fa12924d03a014c4a81e770c3eb2175a
|
||||
templates/product_market_fit_superhuman_question_2_choice_1: 074b2a608d4bba5706b5c55dae249edf
|
||||
@@ -2663,7 +2666,6 @@ checksums:
|
||||
templates/site_abandonment_survey_description: 46581a9b056f3cbf8c1dc9e630e716b5
|
||||
templates/site_abandonment_survey_question_1_html: eec37cddb0c530c72544067712e95670
|
||||
templates/site_abandonment_survey_question_2_button_label: 6208ac076107506686eb8eae42ac4450
|
||||
templates/site_abandonment_survey_question_2_dismiss_button_label: 17961ce57f78e2cbfded4590014e5e06
|
||||
templates/site_abandonment_survey_question_2_headline: e11a5c95e6a4ba0a3fe9bb0ad1da0b46
|
||||
templates/site_abandonment_survey_question_3_choice_1: c86306eb379a1b5f4039e27a0a12caca
|
||||
templates/site_abandonment_survey_question_3_choice_2: fee51e29951105d7650c3da72282db6d
|
||||
@@ -2688,7 +2690,6 @@ checksums:
|
||||
templates/site_abandonment_survey_question_7_label: c0d4407cabb5811192c17cbbb8c1a71e
|
||||
templates/site_abandonment_survey_question_8_headline: 9e82d6f51788351c7e2c8f73be66d005
|
||||
templates/site_abandonment_survey_question_9_headline: ef1289130df46b80d43119380095b579
|
||||
templates/skip: b7f28dfa2f58b80b149bb82b392d0291
|
||||
templates/smileys_survey_name: 6ef64e8182e7820efa53a2d1c81eb912
|
||||
templates/smileys_survey_question_1_headline: 6b15d118037b729138c2214cfef49a68
|
||||
templates/smileys_survey_question_1_lower_label: ff4681be0a94185111459994fe58478c
|
||||
|
||||
@@ -206,15 +206,13 @@ const getExistingFields = async (key: TIntegrationAirtableCredential, baseId: st
|
||||
export const writeData = async (
|
||||
key: TIntegrationAirtableCredential,
|
||||
configData: TIntegrationAirtableConfigData,
|
||||
values: string[][]
|
||||
responses: string[],
|
||||
elements: string[]
|
||||
) => {
|
||||
const responses = values[0];
|
||||
const questions = values[1];
|
||||
|
||||
// 1) Build the record payload
|
||||
const data: Record<string, string> = {};
|
||||
for (let i = 0; i < questions.length; i++) {
|
||||
data[questions[i]] =
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
data[elements[i]] =
|
||||
responses[i].length > AIRTABLE_MESSAGE_LIMIT
|
||||
? truncateText(responses[i], AIRTABLE_MESSAGE_LIMIT)
|
||||
: responses[i];
|
||||
@@ -222,7 +220,7 @@ export const writeData = async (
|
||||
|
||||
// 2) Figure out which fields need creating
|
||||
const existingFields = await getExistingFields(key, configData.baseId, configData.tableId);
|
||||
const fieldsToCreate = questions.filter((q) => !existingFields.has(q));
|
||||
const fieldsToCreate = elements.filter((q) => !existingFields.has(q));
|
||||
|
||||
// 3) Create any missing fields with throttling to respect Airtable's 5 req/sec per base limit
|
||||
if (fieldsToCreate.length > 0) {
|
||||
|
||||
@@ -22,31 +22,36 @@ const { google } = require("googleapis");
|
||||
export const writeData = async (
|
||||
integrationData: TIntegrationGoogleSheets,
|
||||
spreadsheetId: string,
|
||||
values: string[][]
|
||||
responses: string[],
|
||||
elements: string[]
|
||||
) => {
|
||||
validateInputs(
|
||||
[integrationData, ZIntegrationGoogleSheets],
|
||||
[spreadsheetId, ZString],
|
||||
[values, z.array(z.array(ZString))]
|
||||
[responses, z.array(ZString)],
|
||||
[elements, z.array(ZString)]
|
||||
);
|
||||
|
||||
try {
|
||||
const authClient = await authorize(integrationData);
|
||||
const sheets = google.sheets({ version: "v4", auth: authClient });
|
||||
const responses = {
|
||||
const responsesMapped = {
|
||||
values: [
|
||||
values[0].map((value) =>
|
||||
value.length > GOOGLE_SHEET_MESSAGE_LIMIT ? truncateText(value, GOOGLE_SHEET_MESSAGE_LIMIT) : value
|
||||
responses.map((response) =>
|
||||
response.length > GOOGLE_SHEET_MESSAGE_LIMIT
|
||||
? truncateText(response, GOOGLE_SHEET_MESSAGE_LIMIT)
|
||||
: response
|
||||
),
|
||||
],
|
||||
};
|
||||
const question = { values: [values[1]] };
|
||||
|
||||
const element = { values: [elements] };
|
||||
sheets.spreadsheets.values.update(
|
||||
{
|
||||
spreadsheetId: spreadsheetId,
|
||||
range: "A1",
|
||||
valueInputOption: "RAW",
|
||||
resource: question,
|
||||
resource: element,
|
||||
},
|
||||
(err: Error) => {
|
||||
if (err) {
|
||||
@@ -60,7 +65,7 @@ export const writeData = async (
|
||||
spreadsheetId: spreadsheetId,
|
||||
range: "A2",
|
||||
valueInputOption: "RAW",
|
||||
resource: responses,
|
||||
resource: responsesMapped,
|
||||
},
|
||||
(err: Error) => {
|
||||
if (err) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TSurveyCTAElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyCTAQuestion,
|
||||
TSurveyCalQuestion,
|
||||
TSurveyConsentQuestion,
|
||||
TSurveyDateQuestion,
|
||||
@@ -173,20 +173,17 @@ export const mockNpsQuestion: TSurveyNPSQuestion = {
|
||||
isColorCodingEnabled: false,
|
||||
};
|
||||
|
||||
export const mockCtaQuestion: TSurveyCTAQuestion = {
|
||||
export const mockCtaQuestion: TSurveyCTAElement = {
|
||||
required: true,
|
||||
headline: {
|
||||
default: "You are one of our power users!",
|
||||
},
|
||||
buttonLabel: {
|
||||
ctaButtonLabel: {
|
||||
default: "Book interview",
|
||||
},
|
||||
buttonExternal: false,
|
||||
dismissButtonLabel: {
|
||||
default: "Skip",
|
||||
},
|
||||
buttonExternal: true,
|
||||
id: "gwn15urom4ffnhfimwbz3vgc",
|
||||
type: TSurveyQuestionTypeEnum.CTA,
|
||||
type: TSurveyElementTypeEnum.CTA,
|
||||
isDraft: true,
|
||||
};
|
||||
|
||||
@@ -445,15 +442,13 @@ export const mockLegacyNpsQuestion = {
|
||||
export const mockTranslatedCtaQuestion = {
|
||||
...mockCtaQuestion,
|
||||
headline: { default: "You are one of our power users!", de: "" },
|
||||
buttonLabel: { default: "Book interview", de: "" },
|
||||
dismissButtonLabel: { default: "Skip", de: "" },
|
||||
ctaButtonLabel: { default: "Book interview", de: "" },
|
||||
};
|
||||
|
||||
export const mockLegacyCtaQuestion = {
|
||||
...mockCtaQuestion,
|
||||
headline: "You are one of our power users!",
|
||||
buttonLabel: "Book interview",
|
||||
dismissButtonLabel: "Skip",
|
||||
ctaButtonLabel: "Book interview",
|
||||
};
|
||||
|
||||
export const mockTranslatedConsentQuestion = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { iso639Languages } from "@formbricks/i18n-utils/src/utils";
|
||||
import { TI18nString } from "@formbricks/types/i18n";
|
||||
import { TLanguage } from "@formbricks/types/project";
|
||||
import { TI18nString, TSurveyLanguage } from "@formbricks/types/surveys/types";
|
||||
import { TSurveyLanguage } from "@formbricks/types/surveys/types";
|
||||
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
||||
|
||||
// Helper function to create an i18nString from a regular string.
|
||||
|
||||
@@ -47,8 +47,8 @@ describe("Integration Service", () => {
|
||||
spreadsheetName: "Test Spreadsheet",
|
||||
surveyId: "survey123",
|
||||
surveyName: "Test Survey",
|
||||
questionIds: ["q1", "q2"],
|
||||
questions: "Question 1, Question 2",
|
||||
elementIds: ["q1", "q2"],
|
||||
elements: "Question 1, Question 2",
|
||||
createdAt: new Date(),
|
||||
includeHiddenFields: false,
|
||||
includeMetadata: true,
|
||||
|
||||
@@ -15,8 +15,10 @@ import {
|
||||
ZResponseFilterCriteria,
|
||||
ZResponseUpdateInput,
|
||||
} from "@formbricks/types/responses";
|
||||
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { getElementsFromBlocks } from "@/lib/survey/utils";
|
||||
import { getIsQuotasEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { reduceQuotaLimits } from "@/modules/ee/quotas/lib/quotas";
|
||||
import { deleteFile } from "@/modules/storage/service";
|
||||
@@ -368,7 +370,7 @@ export const getResponseDownloadFile = async (
|
||||
}
|
||||
}
|
||||
|
||||
const { metaDataFields, questions, hiddenFields, variables, userAttributes } = extractSurveyDetails(
|
||||
const { metaDataFields, elements, hiddenFields, variables, userAttributes } = extractSurveyDetails(
|
||||
survey,
|
||||
responses
|
||||
);
|
||||
@@ -397,7 +399,7 @@ export const getResponseDownloadFile = async (
|
||||
"Notes",
|
||||
"Tags",
|
||||
...metaDataFields,
|
||||
...questions.flat(),
|
||||
...elements.flat(),
|
||||
...variables,
|
||||
...hiddenFields,
|
||||
...userAttributes,
|
||||
@@ -409,7 +411,7 @@ export const getResponseDownloadFile = async (
|
||||
const jsonData = getResponsesJson(
|
||||
survey,
|
||||
responses,
|
||||
questions,
|
||||
elements,
|
||||
userAttributes,
|
||||
hiddenFields,
|
||||
isQuotasAllowed
|
||||
@@ -548,15 +550,15 @@ export const updateResponse = async (
|
||||
};
|
||||
|
||||
const findAndDeleteUploadedFilesInResponse = async (response: TResponse, survey: TSurvey): Promise<void> => {
|
||||
const fileUploadQuestions = new Set(
|
||||
survey.questions
|
||||
.filter((question) => question.type === TSurveyQuestionTypeEnum.FileUpload)
|
||||
.map((q) => q.id)
|
||||
const elements = getElementsFromBlocks(survey.blocks);
|
||||
|
||||
const fileUploadElements = new Set(
|
||||
elements.filter((element) => element.type === TSurveyElementTypeEnum.FileUpload).map((q) => q.id)
|
||||
);
|
||||
|
||||
const fileUrls = Object.entries(response.data)
|
||||
.filter(([questionId]) => fileUploadQuestions.has(questionId))
|
||||
.flatMap(([, questionResponse]) => questionResponse as string[]);
|
||||
.filter(([elementId]) => fileUploadElements.has(elementId))
|
||||
.flatMap(([, elementResponse]) => elementResponse as string[]);
|
||||
|
||||
const deletionPromises = fileUrls.map(async (fileUrl) => {
|
||||
try {
|
||||
|
||||
@@ -375,8 +375,8 @@ export const mockSurveySummaryOutput = {
|
||||
dropOffCount: 0,
|
||||
dropOffPercentage: 0,
|
||||
headline: "Question Text",
|
||||
questionType: "openText",
|
||||
questionId: "ars2tjk8hsi8oqk1uac00mo8",
|
||||
elementType: "openText",
|
||||
elementId: "ars2tjk8hsi8oqk1uac00mo8",
|
||||
ttc: 0,
|
||||
impressions: 0,
|
||||
},
|
||||
@@ -396,7 +396,7 @@ export const mockSurveySummaryOutput = {
|
||||
quotas: [],
|
||||
summary: [
|
||||
{
|
||||
question: {
|
||||
element: {
|
||||
headline: { default: "Question Text", de: "Fragetext" },
|
||||
id: "ars2tjk8hsi8oqk1uac00mo8",
|
||||
inputType: "text",
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyOpenTextQuestion,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
buildWhereClause,
|
||||
calculateTtcTotal,
|
||||
@@ -44,20 +40,8 @@ describe("Response Utils", () => {
|
||||
const mockSurvey: Partial<TSurvey> = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
questions: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Question 1" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "1", label: { default: "Option 1" } },
|
||||
{ id: "other", label: { default: "Other" } },
|
||||
],
|
||||
shuffleOption: "none",
|
||||
isDraft: false,
|
||||
},
|
||||
],
|
||||
blocks: [],
|
||||
questions: [],
|
||||
type: "app",
|
||||
hiddenFields: { enabled: true, fieldIds: [] },
|
||||
createdAt: new Date(),
|
||||
@@ -115,6 +99,7 @@ describe("Response Utils", () => {
|
||||
const baseSurvey: Partial<TSurvey> = {
|
||||
id: "s1",
|
||||
name: "Survey",
|
||||
blocks: [],
|
||||
questions: [],
|
||||
type: "app",
|
||||
hiddenFields: { enabled: false, fieldIds: [] },
|
||||
@@ -203,26 +188,33 @@ describe("Response Utils", () => {
|
||||
const textSurvey: Partial<TSurvey> = {
|
||||
id: "s2",
|
||||
name: "TextSurvey",
|
||||
questions: [
|
||||
blocks: [
|
||||
{
|
||||
id: "qText",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Text Q" },
|
||||
required: false,
|
||||
isDraft: false,
|
||||
charLimit: {},
|
||||
inputType: "text",
|
||||
},
|
||||
{
|
||||
id: "qNum",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Num Q" },
|
||||
required: false,
|
||||
isDraft: false,
|
||||
charLimit: {},
|
||||
inputType: "number",
|
||||
id: "block1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
{
|
||||
id: "qText",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Text Q" },
|
||||
required: false,
|
||||
isDraft: false,
|
||||
charLimit: {},
|
||||
inputType: "text",
|
||||
},
|
||||
{
|
||||
id: "qNum",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Num Q" },
|
||||
required: false,
|
||||
isDraft: false,
|
||||
charLimit: {},
|
||||
inputType: "number",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
questions: [],
|
||||
type: "app",
|
||||
hiddenFields: { enabled: false, fieldIds: [] },
|
||||
createdAt: new Date(),
|
||||
@@ -232,7 +224,7 @@ describe("Response Utils", () => {
|
||||
status: "inProgress",
|
||||
};
|
||||
|
||||
const ops: Array<[keyof TSurveyQuestionTypeEnum | string, any, any]> = [
|
||||
const ops: Array<[keyof TSurveyElementTypeEnum | string, any, any]> = [
|
||||
["submitted", { op: "submitted" }, { path: ["qText"], not: Prisma.DbNull }],
|
||||
["filledOut", { op: "filledOut" }, { path: ["qText"], not: [] }],
|
||||
["skipped", { op: "skipped" }, "OR"],
|
||||
@@ -295,18 +287,25 @@ describe("Response Utils", () => {
|
||||
const matrixSurvey: Partial<TSurvey> = {
|
||||
id: "s3",
|
||||
name: "MatrixSurvey",
|
||||
questions: [
|
||||
blocks: [
|
||||
{
|
||||
id: "qM",
|
||||
type: TSurveyQuestionTypeEnum.Matrix,
|
||||
headline: { default: "Matrix" },
|
||||
required: false,
|
||||
rows: [{ default: "R1" }],
|
||||
columns: [{ default: "C1" }],
|
||||
shuffleOption: "none",
|
||||
isDraft: false,
|
||||
id: "block1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
{
|
||||
id: "qM",
|
||||
type: TSurveyElementTypeEnum.Matrix,
|
||||
headline: { default: "Matrix" },
|
||||
required: false,
|
||||
rows: [{ id: "r1", label: { default: "R1" } }],
|
||||
columns: [{ id: "c1", label: { default: "C1" } }],
|
||||
shuffleOption: "none",
|
||||
isDraft: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
questions: [],
|
||||
type: "app",
|
||||
hiddenFields: { enabled: false, fieldIds: [] },
|
||||
createdAt: new Date(),
|
||||
@@ -360,34 +359,48 @@ describe("Response Utils", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Fix this test after the survey editor poc is merged
|
||||
describe("extractSurveyDetails", () => {
|
||||
const mockSurvey: Partial<TSurvey> = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
questions: [
|
||||
blocks: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Question 1" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "1", label: { default: "Option 1" } },
|
||||
{ id: "2", label: { default: "Option 2" } },
|
||||
id: "block1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Question 1" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "1", label: { default: "Option 1" } },
|
||||
{ id: "2", label: { default: "Option 2" } },
|
||||
],
|
||||
shuffleOption: "none",
|
||||
isDraft: false,
|
||||
},
|
||||
{
|
||||
id: "q2",
|
||||
type: TSurveyElementTypeEnum.Matrix,
|
||||
headline: { default: "Matrix Question" },
|
||||
required: true,
|
||||
rows: [
|
||||
{ id: "r1", label: { default: "Row 1" } },
|
||||
{ id: "r2", label: { default: "Row 2" } },
|
||||
],
|
||||
columns: [
|
||||
{ id: "c1", label: { default: "Column 1" } },
|
||||
{ id: "c2", label: { default: "Column 2" } },
|
||||
],
|
||||
shuffleOption: "none",
|
||||
isDraft: false,
|
||||
},
|
||||
],
|
||||
shuffleOption: "none",
|
||||
isDraft: false,
|
||||
},
|
||||
{
|
||||
id: "q2",
|
||||
type: TSurveyQuestionTypeEnum.Matrix,
|
||||
headline: { default: "Matrix Question" },
|
||||
required: true,
|
||||
rows: [{ default: "Row 1" }, { default: "Row 2" }],
|
||||
columns: [{ default: "Column 1" }, { default: "Column 2" }],
|
||||
shuffleOption: "none",
|
||||
isDraft: false,
|
||||
},
|
||||
],
|
||||
questions: [],
|
||||
type: "app",
|
||||
hiddenFields: { enabled: true, fieldIds: ["hidden1"] },
|
||||
createdAt: new Date(),
|
||||
@@ -414,7 +427,7 @@ describe("Response Utils", () => {
|
||||
test("should extract survey details correctly", () => {
|
||||
const result = extractSurveyDetails(mockSurvey as TSurvey, mockResponses as TResponse[]);
|
||||
expect(result.metaDataFields).toContain("userAgent - browser");
|
||||
expect(result.questions).toHaveLength(2); // 1 regular question + 2 matrix rows
|
||||
expect(result.elements).toHaveLength(2); // 1 regular question + 2 matrix rows
|
||||
expect(result.hiddenFields).toContain("hidden1");
|
||||
expect(result.userAttributes).toContain("email");
|
||||
});
|
||||
@@ -424,20 +437,27 @@ describe("Response Utils", () => {
|
||||
const mockSurvey: Partial<TSurvey> = {
|
||||
id: "survey1",
|
||||
name: "Test Survey",
|
||||
questions: [
|
||||
blocks: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Question 1" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "1", label: { default: "Option 1" } },
|
||||
{ id: "2", label: { default: "Option 2" } },
|
||||
id: "block1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Question 1" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "1", label: { default: "Option 1" } },
|
||||
{ id: "2", label: { default: "Option 2" } },
|
||||
],
|
||||
shuffleOption: "none",
|
||||
isDraft: false,
|
||||
},
|
||||
],
|
||||
shuffleOption: "none",
|
||||
isDraft: false,
|
||||
},
|
||||
],
|
||||
questions: [],
|
||||
type: "app",
|
||||
hiddenFields: { enabled: true, fieldIds: [] },
|
||||
createdAt: new Date(),
|
||||
@@ -690,9 +710,9 @@ describe("Response Utils", () => {
|
||||
});
|
||||
|
||||
describe("extractChoiceIdsFromResponse", () => {
|
||||
const multipleChoiceMultiQuestion: TSurveyQuestion = {
|
||||
const multipleChoiceMultiQuestion = {
|
||||
id: "multi-choice-id",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceMulti,
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceMulti as typeof TSurveyElementTypeEnum.MultipleChoiceMulti,
|
||||
headline: { default: "Select multiple options" },
|
||||
required: false,
|
||||
choices: [
|
||||
@@ -709,11 +729,12 @@ describe("extractChoiceIdsFromResponse", () => {
|
||||
label: { default: "Option 3", es: "Opción 3" },
|
||||
},
|
||||
],
|
||||
shuffleOption: "none" as const,
|
||||
};
|
||||
|
||||
const multipleChoiceSingleQuestion: TSurveyQuestion = {
|
||||
const multipleChoiceSingleQuestion = {
|
||||
id: "single-choice-id",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceSingle as typeof TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Select one option" },
|
||||
required: false,
|
||||
choices: [
|
||||
@@ -726,14 +747,15 @@ describe("extractChoiceIdsFromResponse", () => {
|
||||
label: { default: "Choice B", fr: "Choix B" },
|
||||
},
|
||||
],
|
||||
shuffleOption: "none" as const,
|
||||
};
|
||||
|
||||
const textQuestion: TSurveyOpenTextQuestion = {
|
||||
const textQuestion = {
|
||||
id: "text-id",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
type: TSurveyElementTypeEnum.OpenText as typeof TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "What do you think?" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
inputType: "text" as const,
|
||||
charLimit: { enabled: false, min: 0, max: 0 },
|
||||
};
|
||||
|
||||
|
||||
@@ -10,48 +10,49 @@ import {
|
||||
TSurveyMetaFieldFilter,
|
||||
} from "@formbricks/types/responses";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyMultipleChoiceQuestion,
|
||||
TSurveyPictureSelectionQuestion,
|
||||
TSurveyQuestion,
|
||||
TSurveyRankingQuestion,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
TSurveyElement,
|
||||
TSurveyMultipleChoiceElement,
|
||||
TSurveyPictureSelectionElement,
|
||||
TSurveyRankingElement,
|
||||
} from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { processResponseData } from "../responses";
|
||||
import { getTodaysDateTimeFormatted } from "../time";
|
||||
import { getFormattedDateTimeString } from "../utils/datetime";
|
||||
import { sanitizeString } from "../utils/strings";
|
||||
|
||||
/**
|
||||
* Extracts choice IDs from response values for multiple choice questions
|
||||
* Extracts choice IDs from response values for multiple choice elements
|
||||
* @param responseValue - The response value (string for single choice, array for multi choice)
|
||||
* @param question - The survey question containing choices
|
||||
* @param element - The survey element containing choices
|
||||
* @param language - The language to match against (defaults to "default")
|
||||
* @returns Array of choice IDs
|
||||
*/
|
||||
export const extractChoiceIdsFromResponse = (
|
||||
responseValue: TResponseDataValue,
|
||||
question: TSurveyQuestion,
|
||||
element: TSurveyElement,
|
||||
language: string = "default"
|
||||
): string[] => {
|
||||
// Type guard to ensure the question has choices
|
||||
if (
|
||||
question.type !== "multipleChoiceMulti" &&
|
||||
question.type !== "multipleChoiceSingle" &&
|
||||
question.type !== "ranking" &&
|
||||
question.type !== "pictureSelection"
|
||||
element.type !== "multipleChoiceMulti" &&
|
||||
element.type !== "multipleChoiceSingle" &&
|
||||
element.type !== "ranking" &&
|
||||
element.type !== "pictureSelection"
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
const isPictureSelection = question.type === "pictureSelection";
|
||||
|
||||
const isPictureSelection = element.type === "pictureSelection";
|
||||
|
||||
if (!responseValue) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// For picture selection questions, the response value is already choice ID(s)
|
||||
// For picture selection elements, the response value is already choice ID(s)
|
||||
if (isPictureSelection) {
|
||||
if (Array.isArray(responseValue)) {
|
||||
// Multi-selection: array of choice IDs
|
||||
@@ -67,7 +68,7 @@ export const extractChoiceIdsFromResponse = (
|
||||
|
||||
// Helper function to find choice by label - eliminates duplication
|
||||
const findChoiceByLabel = (choiceLabel: string): string | null => {
|
||||
const targetChoice = question.choices.find((c) => {
|
||||
const targetChoice = element.choices.find((c) => {
|
||||
// Try exact language match first
|
||||
if (c.label[defaultLanguage] === choiceLabel) {
|
||||
return true;
|
||||
@@ -92,13 +93,13 @@ export const extractChoiceIdsFromResponse = (
|
||||
|
||||
export const getChoiceIdByValue = (
|
||||
value: string,
|
||||
question: TSurveyMultipleChoiceQuestion | TSurveyRankingQuestion | TSurveyPictureSelectionQuestion
|
||||
element: TSurveyMultipleChoiceElement | TSurveyRankingElement | TSurveyPictureSelectionElement
|
||||
) => {
|
||||
if (question.type === "pictureSelection") {
|
||||
return question.choices.find((choice) => choice.imageUrl === value)?.id ?? "other";
|
||||
if (element.type === "pictureSelection") {
|
||||
return element.choices.find((choice) => choice.imageUrl === value)?.id ?? "other";
|
||||
}
|
||||
|
||||
return question.choices.find((choice) => choice.label.default === value)?.id ?? "other";
|
||||
return element.choices.find((choice) => choice.label.default === value)?.id ?? "other";
|
||||
};
|
||||
|
||||
export const calculateTtcTotal = (ttc: TResponseTtc) => {
|
||||
@@ -324,12 +325,12 @@ export const buildWhereClause = (survey: TSurvey, filterCriteria?: TResponseFilt
|
||||
});
|
||||
}
|
||||
|
||||
// For Questions Data
|
||||
if (filterCriteria?.data) {
|
||||
const data: Prisma.ResponseWhereInput[] = [];
|
||||
|
||||
Object.entries(filterCriteria.data).forEach(([key, val]) => {
|
||||
const question = survey.questions.find((question) => question.id === key);
|
||||
const elements = getElementsFromBlocks(survey.blocks);
|
||||
const element = elements.find((element) => element.id === key);
|
||||
|
||||
switch (val.op) {
|
||||
case "submitted":
|
||||
@@ -363,7 +364,7 @@ export const buildWhereClause = (survey: TSurvey, filterCriteria?: TResponseFilt
|
||||
equals: "",
|
||||
},
|
||||
},
|
||||
// For address question
|
||||
// For address element
|
||||
{
|
||||
data: {
|
||||
path: [key],
|
||||
@@ -442,29 +443,29 @@ export const buildWhereClause = (survey: TSurvey, filterCriteria?: TResponseFilt
|
||||
});
|
||||
break;
|
||||
case "includesOne":
|
||||
// * If the question includes an 'other' choice and the user has selected it:
|
||||
// * - `predefinedLabels`: Collects labels from the question's choices that aren't selected by the user.
|
||||
// * If the element includes an 'other' choice and the user has selected it:
|
||||
// * - `predefinedLabels`: Collects labels from the element's choices that aren't selected by the user.
|
||||
// * - `subsets`: Generates all possible non-empty permutations of subsets of these predefined labels.
|
||||
// *
|
||||
// * Depending on the question type (multiple or single choice), the filter is constructed:
|
||||
// * Depending on the element type (multiple or single choice), the filter is constructed:
|
||||
// * - For "multipleChoiceMulti": Filters out any combinations of choices that match the subsets of predefined labels.
|
||||
// * - For "multipleChoiceSingle": Filters out any single predefined labels that match the user's selection.
|
||||
const values: string[] = val.value.map((v) => v.toString());
|
||||
const otherChoice =
|
||||
question && (question.type === "multipleChoiceMulti" || question.type === "multipleChoiceSingle")
|
||||
? question.choices.find((choice) => choice.id === "other")
|
||||
element && (element.type === "multipleChoiceMulti" || element.type === "multipleChoiceSingle")
|
||||
? element.choices.find((choice) => choice.id === "other")
|
||||
: null;
|
||||
|
||||
if (
|
||||
question &&
|
||||
(question.type === "multipleChoiceMulti" || question.type === "multipleChoiceSingle") &&
|
||||
question.choices.map((choice) => choice.id).includes("other") &&
|
||||
element &&
|
||||
(element.type === "multipleChoiceMulti" || element.type === "multipleChoiceSingle") &&
|
||||
element.choices.map((choice) => choice.id).includes("other") &&
|
||||
otherChoice &&
|
||||
values.includes(otherChoice.label.default)
|
||||
) {
|
||||
const predefinedLabels: string[] = [];
|
||||
|
||||
question.choices.forEach((choice) => {
|
||||
element.choices.forEach((choice) => {
|
||||
Object.values(choice.label).forEach((label) => {
|
||||
if (!values.includes(label)) {
|
||||
predefinedLabels.push(label);
|
||||
@@ -473,7 +474,7 @@ export const buildWhereClause = (survey: TSurvey, filterCriteria?: TResponseFilt
|
||||
});
|
||||
|
||||
const subsets = generateAllPermutationsOfSubsets(predefinedLabels);
|
||||
if (question.type === "multipleChoiceMulti") {
|
||||
if (element.type === "multipleChoiceMulti") {
|
||||
const subsetConditions = subsets.map((subset) => ({
|
||||
data: { path: [key], equals: subset },
|
||||
}));
|
||||
@@ -663,16 +664,18 @@ export const extractSurveyDetails = (survey: TSurvey, responses: TResponse[]) =>
|
||||
const metaDataFields = responses.length > 0 ? extracMetadataKeys(responses[0].meta) : [];
|
||||
const modifiedSurvey = replaceHeadlineRecall(survey, "default");
|
||||
|
||||
const questions = modifiedSurvey.questions.map((question, idx) => {
|
||||
const headline = getTextContent(getLocalizedValue(question.headline, "default")) ?? question.id;
|
||||
if (question.type === "matrix") {
|
||||
return question.rows.map((row) => {
|
||||
const modifiedElements = getElementsFromBlocks(modifiedSurvey.blocks);
|
||||
|
||||
const elements = modifiedElements.map((element, idx) => {
|
||||
const headline = getTextContent(getLocalizedValue(element.headline, "default")) ?? element.id;
|
||||
if (element.type === "matrix") {
|
||||
return element.rows.map((row) => {
|
||||
return `${idx + 1}. ${headline} - ${getTextContent(getLocalizedValue(row.label, "default"))}`;
|
||||
});
|
||||
} else if (
|
||||
question.type === "multipleChoiceMulti" ||
|
||||
question.type === "multipleChoiceSingle" ||
|
||||
question.type === "ranking"
|
||||
element.type === "multipleChoiceMulti" ||
|
||||
element.type === "multipleChoiceSingle" ||
|
||||
element.type === "ranking"
|
||||
) {
|
||||
return [`${idx + 1}. ${headline}`, `${idx + 1}. ${headline} - Option ID`];
|
||||
} else {
|
||||
@@ -687,13 +690,13 @@ export const extractSurveyDetails = (survey: TSurvey, responses: TResponse[]) =>
|
||||
: [];
|
||||
const variables = survey.variables?.map((variable) => variable.name) || [];
|
||||
|
||||
return { metaDataFields, questions, hiddenFields, variables, userAttributes };
|
||||
return { metaDataFields, elements, hiddenFields, variables, userAttributes };
|
||||
};
|
||||
|
||||
export const getResponsesJson = (
|
||||
survey: TSurvey,
|
||||
responses: TResponseWithQuotas[],
|
||||
questionsHeadlines: string[][],
|
||||
elementsHeadlines: string[][],
|
||||
userAttributes: string[],
|
||||
hiddenFields: string[],
|
||||
isQuotasAllowed: boolean = false
|
||||
@@ -729,16 +732,17 @@ export const getResponsesJson = (
|
||||
});
|
||||
|
||||
// survey response data
|
||||
questionsHeadlines.forEach((questionHeadline) => {
|
||||
const questionIndex = parseInt(questionHeadline[0]) - 1;
|
||||
const question = survey?.questions[questionIndex];
|
||||
const answer = response.data[question.id];
|
||||
elementsHeadlines.forEach((elementHeadline) => {
|
||||
const elementIndex = parseInt(elementHeadline[0]) - 1;
|
||||
const elements = getElementsFromBlocks(survey.blocks);
|
||||
const element = elements[elementIndex];
|
||||
const answer = response.data[element.id];
|
||||
|
||||
if (question.type === "matrix") {
|
||||
// For matrix questions, we need to handle each row separately
|
||||
questionHeadline.forEach((headline, index) => {
|
||||
if (element.type === "matrix") {
|
||||
// For matrix elements, we need to handle each row separately
|
||||
elementHeadline.forEach((headline, index) => {
|
||||
if (answer) {
|
||||
const row = question.rows[index];
|
||||
const row = element.rows[index];
|
||||
if (row && row.label.default && answer[row.label.default] !== undefined) {
|
||||
jsonData[idx][headline] = answer[row.label.default];
|
||||
} else {
|
||||
@@ -747,20 +751,20 @@ export const getResponsesJson = (
|
||||
}
|
||||
});
|
||||
} else if (
|
||||
question.type === "multipleChoiceMulti" ||
|
||||
question.type === "multipleChoiceSingle" ||
|
||||
question.type === "ranking"
|
||||
element.type === "multipleChoiceMulti" ||
|
||||
element.type === "multipleChoiceSingle" ||
|
||||
element.type === "ranking"
|
||||
) {
|
||||
// Set the main response value
|
||||
jsonData[idx][questionHeadline[0]] = processResponseData(answer);
|
||||
jsonData[idx][elementHeadline[0]] = processResponseData(answer);
|
||||
|
||||
// Set the option IDs using the reusable function
|
||||
if (questionHeadline[1]) {
|
||||
const choiceIds = extractChoiceIdsFromResponse(answer, question, response.language || "default");
|
||||
jsonData[idx][questionHeadline[1]] = choiceIds.join(", ");
|
||||
if (elementHeadline[1]) {
|
||||
const choiceIds = extractChoiceIdsFromResponse(answer, element, response.language || "default");
|
||||
jsonData[idx][elementHeadline[1]] = choiceIds.join(", ");
|
||||
}
|
||||
} else {
|
||||
jsonData[idx][questionHeadline[0]] = processResponseData(answer);
|
||||
jsonData[idx][elementHeadline[0]] = processResponseData(answer);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { TSurveyQuestionType, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { convertResponseValue, getQuestionResponseMapping, processResponseData } from "./responses";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { convertResponseValue, getElementResponseMapping, processResponseData } from "./responses";
|
||||
|
||||
// Mock the recall and i18n utils
|
||||
vi.mock("@/lib/utils/recall", () => ({
|
||||
@@ -63,7 +63,7 @@ describe("Response Processing", () => {
|
||||
describe("convertResponseValue", () => {
|
||||
const mockOpenTextQuestion = {
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText as const,
|
||||
type: TSurveyElementTypeEnum.OpenText as const,
|
||||
headline: { default: "Test Question" },
|
||||
required: true,
|
||||
inputType: "text" as const,
|
||||
@@ -73,7 +73,7 @@ describe("Response Processing", () => {
|
||||
|
||||
const mockRankingQuestion = {
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.Ranking as const,
|
||||
type: TSurveyElementTypeEnum.Ranking as const,
|
||||
headline: { default: "Test Question" },
|
||||
required: true,
|
||||
choices: [
|
||||
@@ -85,7 +85,7 @@ describe("Response Processing", () => {
|
||||
|
||||
const mockFileUploadQuestion = {
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.FileUpload as const,
|
||||
type: TSurveyElementTypeEnum.FileUpload as const,
|
||||
headline: { default: "Test Question" },
|
||||
required: true,
|
||||
allowMultipleFiles: true,
|
||||
@@ -93,7 +93,7 @@ describe("Response Processing", () => {
|
||||
|
||||
const mockPictureSelectionQuestion = {
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.PictureSelection as const,
|
||||
type: TSurveyElementTypeEnum.PictureSelection as const,
|
||||
headline: { default: "Test Question" },
|
||||
required: true,
|
||||
allowMulti: false,
|
||||
@@ -184,28 +184,36 @@ describe("Response Processing", () => {
|
||||
name: "Test Survey",
|
||||
environmentId: "env1",
|
||||
createdBy: null,
|
||||
questions: [
|
||||
blocks: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText as const,
|
||||
headline: { default: "Question 1" },
|
||||
required: true,
|
||||
inputType: "text" as const,
|
||||
longAnswer: false,
|
||||
charLimit: { enabled: false },
|
||||
},
|
||||
{
|
||||
id: "q2",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceMulti as const,
|
||||
headline: { default: "Question 2" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "1", label: { default: "Option 1" } },
|
||||
{ id: "2", label: { default: "Option 2" } },
|
||||
id: "block1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyElementTypeEnum.OpenText as const,
|
||||
headline: { default: "Question 1" },
|
||||
required: true,
|
||||
inputType: "text" as const,
|
||||
longAnswer: false,
|
||||
charLimit: { enabled: false },
|
||||
},
|
||||
{
|
||||
id: "q2",
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceMulti as const,
|
||||
headline: { default: "Question 2" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "1", label: { default: "Option 1" } },
|
||||
{ id: "2", label: { default: "Option 2" } },
|
||||
],
|
||||
shuffleOption: "none" as const,
|
||||
buttonLabel: { default: "Next" },
|
||||
},
|
||||
],
|
||||
shuffleOption: "none" as const,
|
||||
},
|
||||
],
|
||||
questions: [],
|
||||
hiddenFields: {
|
||||
enabled: false,
|
||||
fieldIds: [],
|
||||
@@ -255,6 +263,7 @@ describe("Response Processing", () => {
|
||||
enabled: false,
|
||||
isEncrypted: false,
|
||||
},
|
||||
metadata: {},
|
||||
};
|
||||
|
||||
const mockResponse = {
|
||||
@@ -286,17 +295,17 @@ describe("Response Processing", () => {
|
||||
};
|
||||
|
||||
test("should map questions to responses correctly", () => {
|
||||
const mapping = getQuestionResponseMapping(mockSurvey, mockResponse);
|
||||
const mapping = getElementResponseMapping(mockSurvey, mockResponse);
|
||||
expect(mapping).toHaveLength(2);
|
||||
expect(mapping[0]).toEqual({
|
||||
question: "Question 1",
|
||||
element: "Question 1",
|
||||
response: "Answer 1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
});
|
||||
expect(mapping[1]).toEqual({
|
||||
question: "Question 2",
|
||||
element: "Question 2",
|
||||
response: "Option 1; Option 2",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceMulti,
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceMulti,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -325,7 +334,7 @@ describe("Response Processing", () => {
|
||||
contactAttributes: {},
|
||||
singleUseId: null,
|
||||
};
|
||||
const mapping = getQuestionResponseMapping(mockSurvey, response);
|
||||
const mapping = getElementResponseMapping(mockSurvey, response);
|
||||
expect(mapping).toHaveLength(2);
|
||||
expect(mapping[0].response).toBe("");
|
||||
expect(mapping[1].response).toBe("");
|
||||
@@ -334,17 +343,24 @@ describe("Response Processing", () => {
|
||||
test("should handle different language", () => {
|
||||
const survey = {
|
||||
...mockSurvey,
|
||||
questions: [
|
||||
blocks: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText as const,
|
||||
headline: { default: "Question 1", en: "Question 1 EN" },
|
||||
required: true,
|
||||
inputType: "text" as const,
|
||||
longAnswer: false,
|
||||
charLimit: { enabled: false },
|
||||
id: "block1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyElementTypeEnum.OpenText as const,
|
||||
headline: { default: "Question 1", en: "Question 1 EN" },
|
||||
required: true,
|
||||
inputType: "text" as const,
|
||||
longAnswer: false,
|
||||
charLimit: { enabled: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
questions: [],
|
||||
languages: [
|
||||
{
|
||||
language: {
|
||||
@@ -396,8 +412,8 @@ describe("Response Processing", () => {
|
||||
contactAttributes: {},
|
||||
singleUseId: null,
|
||||
};
|
||||
const mapping = getQuestionResponseMapping(survey, response);
|
||||
expect(mapping[0].question).toBe("Question 1 EN");
|
||||
const mapping = getElementResponseMapping(survey, response);
|
||||
expect(mapping[0].element).toBe("Question 1 EN");
|
||||
});
|
||||
|
||||
test("should handle null response language", () => {
|
||||
@@ -425,9 +441,9 @@ describe("Response Processing", () => {
|
||||
contactAttributes: {},
|
||||
singleUseId: null,
|
||||
};
|
||||
const mapping = getQuestionResponseMapping(mockSurvey, response);
|
||||
const mapping = getElementResponseMapping(mockSurvey, response);
|
||||
expect(mapping).toHaveLength(2);
|
||||
expect(mapping[0].question).toBe("Question 1");
|
||||
expect(mapping[0].element).toBe("Question 1");
|
||||
});
|
||||
|
||||
test("should handle undefined response language", () => {
|
||||
@@ -455,9 +471,9 @@ describe("Response Processing", () => {
|
||||
contactAttributes: {},
|
||||
singleUseId: null,
|
||||
};
|
||||
const mapping = getQuestionResponseMapping(mockSurvey, response);
|
||||
const mapping = getElementResponseMapping(mockSurvey, response);
|
||||
expect(mapping).toHaveLength(2);
|
||||
expect(mapping[0].question).toBe("Question 1");
|
||||
expect(mapping[0].element).toBe("Question 1");
|
||||
});
|
||||
|
||||
test("should handle empty survey languages", () => {
|
||||
@@ -489,9 +505,9 @@ describe("Response Processing", () => {
|
||||
contactAttributes: {},
|
||||
singleUseId: null,
|
||||
};
|
||||
const mapping = getQuestionResponseMapping(survey, response);
|
||||
const mapping = getElementResponseMapping(survey, response);
|
||||
expect(mapping).toHaveLength(2);
|
||||
expect(mapping[0].question).toBe("Question 1"); // Should fallback to default
|
||||
expect(mapping[0].element).toBe("Question 1"); // Should fallback to default
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+22
-21
@@ -1,15 +1,17 @@
|
||||
import { TResponse, TResponseDataValue } from "@formbricks/types/responses";
|
||||
import { TSurvey, TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys/types";
|
||||
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { parseRecallInfo } from "@/lib/utils/recall";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { getLanguageCode, getLocalizedValue } from "./i18n/utils";
|
||||
|
||||
// function to convert response value of type string | number | string[] or Record<string, string> to string | string[]
|
||||
export const convertResponseValue = (
|
||||
answer: TResponseDataValue,
|
||||
question: TSurveyQuestion
|
||||
element: TSurveyElement
|
||||
): string | string[] => {
|
||||
switch (question.type) {
|
||||
switch (element.type) {
|
||||
case "ranking":
|
||||
case "fileUpload":
|
||||
if (typeof answer === "string") {
|
||||
@@ -18,11 +20,11 @@ export const convertResponseValue = (
|
||||
|
||||
case "pictureSelection":
|
||||
if (typeof answer === "string") {
|
||||
const imageUrl = question.choices.find((choice) => choice.id === answer)?.imageUrl;
|
||||
const imageUrl = element.choices.find((choice) => choice.id === answer)?.imageUrl;
|
||||
return imageUrl ? [imageUrl] : [];
|
||||
} else if (Array.isArray(answer)) {
|
||||
return answer
|
||||
.map((answerId) => question.choices.find((choice) => choice.id === answerId)?.imageUrl)
|
||||
.map((answerId) => element.choices.find((choice) => choice.id === answerId)?.imageUrl)
|
||||
.filter((url): url is string => url !== undefined);
|
||||
} else return [];
|
||||
|
||||
@@ -31,33 +33,32 @@ export const convertResponseValue = (
|
||||
}
|
||||
};
|
||||
|
||||
export const getQuestionResponseMapping = (
|
||||
export const getElementResponseMapping = (
|
||||
survey: TSurvey,
|
||||
response: TResponse
|
||||
): { question: string; response: string | string[]; type: TSurveyQuestionType }[] => {
|
||||
const questionResponseMapping: {
|
||||
question: string;
|
||||
): { element: string; response: string | string[]; type: TSurveyElementTypeEnum }[] => {
|
||||
const elementResponseMapping: {
|
||||
element: string;
|
||||
response: string | string[];
|
||||
type: TSurveyQuestionType;
|
||||
type: TSurveyElementTypeEnum;
|
||||
}[] = [];
|
||||
const responseLanguageCode = getLanguageCode(survey.languages, response.language);
|
||||
|
||||
for (const question of survey.questions) {
|
||||
const answer = response.data[question.id];
|
||||
const elements = getElementsFromBlocks(survey.blocks);
|
||||
|
||||
questionResponseMapping.push({
|
||||
question: getTextContent(
|
||||
parseRecallInfo(
|
||||
getLocalizedValue(question.headline, responseLanguageCode ?? "default"),
|
||||
response.data
|
||||
)
|
||||
for (const element of elements) {
|
||||
const answer = response.data[element.id];
|
||||
|
||||
elementResponseMapping.push({
|
||||
element: getTextContent(
|
||||
parseRecallInfo(getLocalizedValue(element.headline, responseLanguageCode ?? "default"), response.data)
|
||||
),
|
||||
response: convertResponseValue(answer, question),
|
||||
type: question.type,
|
||||
response: convertResponseValue(answer, element),
|
||||
type: element.type,
|
||||
});
|
||||
}
|
||||
|
||||
return questionResponseMapping;
|
||||
return elementResponseMapping;
|
||||
};
|
||||
|
||||
export const processResponseData = (responseData: TResponseDataValue): string => {
|
||||
|
||||
@@ -75,11 +75,11 @@ export const getSlackChannels = async (environmentId: string): Promise<TIntegrat
|
||||
export const writeDataToSlack = async (
|
||||
credentials: TIntegrationSlackCredential,
|
||||
channelId: string,
|
||||
values: string[][],
|
||||
responses: string[],
|
||||
elements: string[],
|
||||
surveyName: string | undefined
|
||||
) => {
|
||||
try {
|
||||
const [responses, questions] = values;
|
||||
let blockResponse = [
|
||||
{
|
||||
type: "section",
|
||||
@@ -92,12 +92,12 @@ export const writeDataToSlack = async (
|
||||
type: "divider",
|
||||
},
|
||||
];
|
||||
for (let i = 0; i < values[0].length; i++) {
|
||||
for (let i = 0; i < responses.length; i++) {
|
||||
let questionSection = {
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `*${questions[i]}*`,
|
||||
text: `*${elements[i]}*`,
|
||||
},
|
||||
};
|
||||
const responseText = responses[i];
|
||||
|
||||
@@ -4,12 +4,11 @@ import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyCreateInput,
|
||||
TSurveyLanguage,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
TSurveyWelcomeCard,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
@@ -172,12 +171,12 @@ export const mockContactAttributeKey: TContactAttributeKey = {
|
||||
...commonMockProperties,
|
||||
};
|
||||
|
||||
const mockQuestion: TSurveyQuestion = {
|
||||
const mockQuestion = {
|
||||
id: mockId,
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
type: TSurveyElementTypeEnum.OpenText as typeof TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question Text", de: "Fragetext" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
inputType: "text" as const,
|
||||
charLimit: {
|
||||
enabled: false,
|
||||
},
|
||||
@@ -200,7 +199,14 @@ const baseSurveyProperties = {
|
||||
recontactDays: 3,
|
||||
displayLimit: 3,
|
||||
welcomeCard: mockWelcomeCard,
|
||||
questions: [mockQuestion],
|
||||
questions: [],
|
||||
blocks: [
|
||||
{
|
||||
id: "block1",
|
||||
name: "Block 1",
|
||||
elements: [mockQuestion],
|
||||
},
|
||||
],
|
||||
isBackButtonHidden: false,
|
||||
endings: [
|
||||
{
|
||||
@@ -297,22 +303,22 @@ export const updateSurveyInput: TSurvey = {
|
||||
type: "link",
|
||||
status: "inProgress",
|
||||
displayOption: "respondMultiple",
|
||||
metadata: {},
|
||||
triggers: [{ actionClass: mockActionClass }],
|
||||
projectOverwrites: null,
|
||||
styling: null,
|
||||
recaptcha: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
displayPercentage: null,
|
||||
createdBy: null,
|
||||
pin: null,
|
||||
recaptcha: null,
|
||||
segment: null,
|
||||
languages: [],
|
||||
showLanguageSwitch: null,
|
||||
variables: [],
|
||||
followUps: [],
|
||||
metadata: {},
|
||||
...commonMockProperties,
|
||||
...baseSurveyProperties,
|
||||
...commonMockProperties,
|
||||
};
|
||||
|
||||
export const mockTransformedSurveyOutput = {
|
||||
@@ -331,16 +337,78 @@ export const mockSurveyWithLogic: TSurvey = {
|
||||
type: "link",
|
||||
endings: [],
|
||||
hiddenFields: { enabled: true, fieldIds: ["name"] },
|
||||
questions: [
|
||||
blocks: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
inputType: "text",
|
||||
headline: { default: "What is your favorite color?" },
|
||||
required: true,
|
||||
charLimit: {
|
||||
enabled: false,
|
||||
},
|
||||
id: "block1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
inputType: "text" as const,
|
||||
headline: { default: "What is your favorite color?" },
|
||||
required: true,
|
||||
charLimit: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "q2",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
inputType: "text" as const,
|
||||
headline: { default: "What is your favorite food?" },
|
||||
required: true,
|
||||
charLimit: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "q3",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
inputType: "text" as const,
|
||||
headline: { default: "What is your favorite movie?" },
|
||||
required: true,
|
||||
charLimit: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "q4",
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Select a number:" },
|
||||
choices: [
|
||||
{ id: "mvedaklp0gxxycprpyhhwen7", label: { default: "lol" } },
|
||||
{ id: "i7ws8uqyj66q5x086vbqtm8n", label: { default: "lmao" } },
|
||||
{ id: "cy8hbbr9e2q6ywbfjbzwdsqn", label: { default: "XD" } },
|
||||
{ id: "sojc5wwxc5gxrnuib30w7t6s", label: { default: "hehe" } },
|
||||
],
|
||||
required: true,
|
||||
shuffleOption: "none" as const,
|
||||
},
|
||||
{
|
||||
id: "q5",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
inputType: "number" as const,
|
||||
headline: { default: "Select your age group:" },
|
||||
required: true,
|
||||
charLimit: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "q6",
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceMulti,
|
||||
headline: { default: "Select your age group:" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "mvedaklp0gxxycprpyhhwen7", label: { default: "lol" } },
|
||||
{ id: "i7ws8uqyj66q5x086vbqtm8n", label: { default: "lmao" } },
|
||||
{ id: "cy8hbbr9e2q6ywbfjbzwdsqn", label: { default: "XD" } },
|
||||
{ id: "sojc5wwxc5gxrnuib30w7t6s", label: { default: "hehe" } },
|
||||
],
|
||||
shuffleOption: "none" as const,
|
||||
},
|
||||
],
|
||||
logic: [
|
||||
{
|
||||
id: "cdu9vgtmmd9b24l35pp9bodk",
|
||||
@@ -350,7 +418,7 @@ export const mockSurveyWithLogic: TSurvey = {
|
||||
conditions: [
|
||||
{
|
||||
id: "swlje0bsnh6lkyk8vqs13oyr",
|
||||
leftOperand: { type: "question", value: "q1" },
|
||||
leftOperand: { type: "element", value: "q1" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: "blue" },
|
||||
},
|
||||
@@ -358,18 +426,6 @@ export const mockSurveyWithLogic: TSurvey = {
|
||||
},
|
||||
actions: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "q2",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
inputType: "text",
|
||||
headline: { default: "What is your favorite food?" },
|
||||
required: true,
|
||||
charLimit: {
|
||||
enabled: false,
|
||||
},
|
||||
logic: [
|
||||
{
|
||||
id: "uwlm6kazj5pbt6licpa1hw5c",
|
||||
conditions: {
|
||||
@@ -378,13 +434,13 @@ export const mockSurveyWithLogic: TSurvey = {
|
||||
conditions: [
|
||||
{
|
||||
id: "n74oght3ozqgwm9rifp2fxrr",
|
||||
leftOperand: { type: "question", value: "q1" },
|
||||
leftOperand: { type: "element", value: "q1" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: "blue" },
|
||||
},
|
||||
{
|
||||
id: "fg4c9dwt9qjy8aba7zxbfdqd",
|
||||
leftOperand: { type: "question", value: "q2" },
|
||||
leftOperand: { type: "element", value: "q2" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: "pizza" },
|
||||
},
|
||||
@@ -392,18 +448,6 @@ export const mockSurveyWithLogic: TSurvey = {
|
||||
},
|
||||
actions: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "q3",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
inputType: "text",
|
||||
headline: { default: "What is your favorite movie?" },
|
||||
required: true,
|
||||
charLimit: {
|
||||
enabled: false,
|
||||
},
|
||||
logic: [
|
||||
{
|
||||
id: "dpi3zipezuo1idplztb1abes",
|
||||
conditions: {
|
||||
@@ -412,13 +456,13 @@ export const mockSurveyWithLogic: TSurvey = {
|
||||
conditions: [
|
||||
{
|
||||
id: "tmj7p9d3kpz1v4mcgpguqytw",
|
||||
leftOperand: { type: "question", value: "q2" },
|
||||
leftOperand: { type: "element", value: "q2" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: "pizza" },
|
||||
},
|
||||
{
|
||||
id: "rs7v5mmoetff7x8lo1gdsgpr",
|
||||
leftOperand: { type: "question", value: "q3" },
|
||||
leftOperand: { type: "element", value: "q3" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: "Inception" },
|
||||
},
|
||||
@@ -426,20 +470,6 @@ export const mockSurveyWithLogic: TSurvey = {
|
||||
},
|
||||
actions: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "q4",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "Select a number:" },
|
||||
choices: [
|
||||
{ id: "mvedaklp0gxxycprpyhhwen7", label: { default: "lol" } },
|
||||
{ id: "i7ws8uqyj66q5x086vbqtm8n", label: { default: "lmao" } },
|
||||
{ id: "cy8hbbr9e2q6ywbfjbzwdsqn", label: { default: "XD" } },
|
||||
{ id: "sojc5wwxc5gxrnuib30w7t6s", label: { default: "hehe" } },
|
||||
],
|
||||
required: true,
|
||||
logic: [
|
||||
{
|
||||
id: "fbim31ttxe1s7qkrjzkj1mtc",
|
||||
conditions: {
|
||||
@@ -450,24 +480,12 @@ export const mockSurveyWithLogic: TSurvey = {
|
||||
id: "ddhaccfqy7rr3d5jdswl8yl8",
|
||||
leftOperand: { type: "variable", value: "siog1dabtpo3l0a3xoxw2922" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "question", value: "q4" },
|
||||
rightOperand: { type: "element", value: "q4" },
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "q5",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
inputType: "number",
|
||||
headline: { default: "Select your age group:" },
|
||||
required: true,
|
||||
charLimit: {
|
||||
enabled: false,
|
||||
},
|
||||
logic: [
|
||||
{
|
||||
id: "o6n73uq9rysih9mpcbzlehfs",
|
||||
conditions: {
|
||||
@@ -484,35 +502,21 @@ export const mockSurveyWithLogic: TSurvey = {
|
||||
id: "ot894j7nwna24i6jo2zpk59o",
|
||||
leftOperand: { type: "variable", value: "km1srr55owtn2r7lkoh5ny1u" },
|
||||
operator: "isLessThan",
|
||||
rightOperand: { type: "question", value: "q5" },
|
||||
rightOperand: { type: "element", value: "q5" },
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "q6",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceMulti,
|
||||
headline: { default: "Select your age group:" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "mvedaklp0gxxycprpyhhwen7", label: { default: "lol" } },
|
||||
{ id: "i7ws8uqyj66q5x086vbqtm8n", label: { default: "lmao" } },
|
||||
{ id: "cy8hbbr9e2q6ywbfjbzwdsqn", label: { default: "XD" } },
|
||||
{ id: "sojc5wwxc5gxrnuib30w7t6s", label: { default: "hehe" } },
|
||||
],
|
||||
logic: [
|
||||
{
|
||||
id: "o6n73uq9rysih9mpcbzlehfs",
|
||||
id: "o6n73uq9rysih9mpcbzlehfs2",
|
||||
conditions: {
|
||||
id: "szdkmtz17j9008n4i2d1t040",
|
||||
id: "szdkmtz17j9008n4i2d1t041",
|
||||
connector: "and",
|
||||
conditions: [
|
||||
{
|
||||
id: "rb223vmzuuzo3ag1bp2m3i69",
|
||||
leftOperand: { type: "question", value: "q6" },
|
||||
leftOperand: { type: "element", value: "q6" },
|
||||
operator: "includesOneOf",
|
||||
rightOperand: {
|
||||
type: "static",
|
||||
@@ -521,7 +525,7 @@ export const mockSurveyWithLogic: TSurvey = {
|
||||
},
|
||||
{
|
||||
id: "ot894j7nwna24i6jo2zpk59o",
|
||||
leftOperand: { type: "question", value: "q1" },
|
||||
leftOperand: { type: "element", value: "q1" },
|
||||
operator: "doesNotEqual",
|
||||
rightOperand: { type: "static", value: "teal" },
|
||||
},
|
||||
@@ -531,7 +535,7 @@ export const mockSurveyWithLogic: TSurvey = {
|
||||
conditions: [
|
||||
{
|
||||
id: "gy6xowchkv8bp1qj7ur79jvc",
|
||||
leftOperand: { type: "question", value: "q2" },
|
||||
leftOperand: { type: "element", value: "q2" },
|
||||
operator: "doesNotEqual",
|
||||
rightOperand: { type: "static", value: "pizza" },
|
||||
},
|
||||
@@ -539,13 +543,13 @@ export const mockSurveyWithLogic: TSurvey = {
|
||||
id: "vxyccgwsbq34s3l0syom7y2w",
|
||||
leftOperand: { type: "hiddenField", value: "name" },
|
||||
operator: "contains",
|
||||
rightOperand: { type: "question", value: "q2" },
|
||||
rightOperand: { type: "element", value: "q2" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "yunz0k9w0xwparogz2n1twoy",
|
||||
leftOperand: { type: "question", value: "q3" },
|
||||
leftOperand: { type: "element", value: "q3" },
|
||||
operator: "doesNotEqual",
|
||||
rightOperand: { type: "static", value: "Inception" },
|
||||
},
|
||||
@@ -562,6 +566,7 @@ export const mockSurveyWithLogic: TSurvey = {
|
||||
],
|
||||
},
|
||||
],
|
||||
questions: [],
|
||||
variables: [
|
||||
{ id: "siog1dabtpo3l0a3xoxw2922", type: "text", name: "var1", value: "lmao" },
|
||||
{ id: "km1srr55owtn2r7lkoh5ny1u", type: "number", name: "var2", value: 32 },
|
||||
|
||||
@@ -61,7 +61,7 @@ describe("evaluateLogic with mockSurveyWithLogic", () => {
|
||||
mockSurveyWithLogic,
|
||||
data,
|
||||
variablesData,
|
||||
mockSurveyWithLogic.questions[0].logic![0].conditions,
|
||||
mockSurveyWithLogic.blocks[0].logic![0].conditions,
|
||||
"default"
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
@@ -75,7 +75,7 @@ describe("evaluateLogic with mockSurveyWithLogic", () => {
|
||||
mockSurveyWithLogic,
|
||||
data,
|
||||
variablesData,
|
||||
mockSurveyWithLogic.questions[0].logic![0].conditions,
|
||||
mockSurveyWithLogic.blocks[0].logic![0].conditions,
|
||||
"default"
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
@@ -89,7 +89,7 @@ describe("evaluateLogic with mockSurveyWithLogic", () => {
|
||||
mockSurveyWithLogic,
|
||||
data,
|
||||
variablesData,
|
||||
mockSurveyWithLogic.questions[1].logic![0].conditions,
|
||||
mockSurveyWithLogic.blocks[0].logic![1].conditions,
|
||||
"default"
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
@@ -103,7 +103,7 @@ describe("evaluateLogic with mockSurveyWithLogic", () => {
|
||||
mockSurveyWithLogic,
|
||||
data,
|
||||
variablesData,
|
||||
mockSurveyWithLogic.questions[1].logic![0].conditions,
|
||||
mockSurveyWithLogic.blocks[0].logic![1].conditions,
|
||||
"default"
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
@@ -117,7 +117,7 @@ describe("evaluateLogic with mockSurveyWithLogic", () => {
|
||||
mockSurveyWithLogic,
|
||||
data,
|
||||
variablesData,
|
||||
mockSurveyWithLogic.questions[2].logic![0].conditions,
|
||||
mockSurveyWithLogic.blocks[0].logic![2].conditions,
|
||||
"default"
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
@@ -131,7 +131,7 @@ describe("evaluateLogic with mockSurveyWithLogic", () => {
|
||||
mockSurveyWithLogic,
|
||||
data,
|
||||
variablesData,
|
||||
mockSurveyWithLogic.questions[3].logic![0].conditions,
|
||||
mockSurveyWithLogic.blocks[0].logic![3].conditions,
|
||||
"default"
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
@@ -145,7 +145,7 @@ describe("evaluateLogic with mockSurveyWithLogic", () => {
|
||||
mockSurveyWithLogic,
|
||||
data,
|
||||
variablesData,
|
||||
mockSurveyWithLogic.questions[3].logic![0].conditions,
|
||||
mockSurveyWithLogic.blocks[0].logic![3].conditions,
|
||||
"default"
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
@@ -159,7 +159,7 @@ describe("evaluateLogic with mockSurveyWithLogic", () => {
|
||||
mockSurveyWithLogic,
|
||||
data,
|
||||
variablesData,
|
||||
mockSurveyWithLogic.questions[4].logic![0].conditions,
|
||||
mockSurveyWithLogic.blocks[0].logic![4].conditions,
|
||||
"default"
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
@@ -173,7 +173,7 @@ describe("evaluateLogic with mockSurveyWithLogic", () => {
|
||||
mockSurveyWithLogic,
|
||||
data,
|
||||
variablesData,
|
||||
mockSurveyWithLogic.questions[4].logic![0].conditions,
|
||||
mockSurveyWithLogic.blocks[0].logic![4].conditions,
|
||||
"default"
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
@@ -187,7 +187,7 @@ describe("evaluateLogic with mockSurveyWithLogic", () => {
|
||||
mockSurveyWithLogic,
|
||||
data,
|
||||
variablesData,
|
||||
mockSurveyWithLogic.questions[5].logic![0].conditions,
|
||||
mockSurveyWithLogic.blocks[0].logic![5].conditions,
|
||||
"default"
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
|
||||
@@ -14,7 +14,13 @@ import {
|
||||
import { getActionClasses } from "../actionClass/service";
|
||||
import { ITEMS_PER_PAGE } from "../constants";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { checkForInvalidImagesInQuestions, transformPrismaSurvey } from "./utils";
|
||||
import {
|
||||
checkForInvalidImagesInQuestions,
|
||||
checkForInvalidMediaInBlocks,
|
||||
stripIsDraftFromBlocks,
|
||||
transformPrismaSurvey,
|
||||
validateMediaAndPrepareBlocks,
|
||||
} from "./utils";
|
||||
|
||||
interface TriggerUpdate {
|
||||
create?: Array<{ actionClassId: string }>;
|
||||
@@ -36,6 +42,7 @@ export const selectSurvey = {
|
||||
status: true,
|
||||
welcomeCard: true,
|
||||
questions: true,
|
||||
blocks: true,
|
||||
endings: true,
|
||||
hiddenFields: true,
|
||||
variables: true,
|
||||
@@ -296,6 +303,14 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> =>
|
||||
|
||||
checkForInvalidImagesInQuestions(questions);
|
||||
|
||||
// Add blocks media validation
|
||||
if (updatedSurvey.blocks && updatedSurvey.blocks.length > 0) {
|
||||
const blocksValidation = checkForInvalidMediaInBlocks(updatedSurvey.blocks);
|
||||
if (!blocksValidation.ok) {
|
||||
throw new InvalidInputError(blocksValidation.error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (languages) {
|
||||
// Process languages update logic here
|
||||
// Extract currentLanguageIds and updatedLanguageIds
|
||||
@@ -503,6 +518,11 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> =>
|
||||
return rest;
|
||||
});
|
||||
|
||||
// Strip isDraft from elements before saving
|
||||
if (updatedSurvey.blocks && updatedSurvey.blocks.length > 0) {
|
||||
data.blocks = stripIsDraftFromBlocks(updatedSurvey.blocks);
|
||||
}
|
||||
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
if (!organization) {
|
||||
throw new ResourceNotFoundError("Organization", null);
|
||||
@@ -607,6 +627,11 @@ export const createSurvey = async (
|
||||
checkForInvalidImagesInQuestions(data.questions);
|
||||
}
|
||||
|
||||
// Validate and prepare blocks for persistence
|
||||
if (data.blocks && data.blocks.length > 0) {
|
||||
data.blocks = validateMediaAndPrepareBlocks(data.blocks);
|
||||
}
|
||||
|
||||
const survey = await prisma.survey.create({
|
||||
data: {
|
||||
...data,
|
||||
@@ -621,14 +646,6 @@ export const createSurvey = async (
|
||||
|
||||
// if the survey created is an "app" survey, we also create a private segment for it.
|
||||
if (survey.type === "app") {
|
||||
// const newSegment = await createSegment({
|
||||
// environmentId: parsedEnvironmentId,
|
||||
// surveyId: survey.id,
|
||||
// filters: [],
|
||||
// title: survey.id,
|
||||
// isPrivate: true,
|
||||
// });
|
||||
|
||||
const newSegment = await prisma.segment.create({
|
||||
data: {
|
||||
title: survey.id,
|
||||
|
||||
@@ -2,9 +2,17 @@ import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
import { TSurveyBlock } from "@formbricks/types/surveys/blocks";
|
||||
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import * as videoValidation from "@/lib/utils/video-upload";
|
||||
import * as fileValidation from "@/modules/storage/utils";
|
||||
import { anySurveyHasFilters, checkForInvalidImagesInQuestions, transformPrismaSurvey } from "./utils";
|
||||
import {
|
||||
anySurveyHasFilters,
|
||||
checkForInvalidImagesInQuestions,
|
||||
checkForInvalidMediaInBlocks,
|
||||
transformPrismaSurvey,
|
||||
} from "./utils";
|
||||
|
||||
describe("transformPrismaSurvey", () => {
|
||||
test("transforms prisma survey without segment", () => {
|
||||
@@ -252,3 +260,418 @@ describe("checkForInvalidImagesInQuestions", () => {
|
||||
expect(fileValidation.isValidImageFile).toHaveBeenNthCalledWith(3, "image3.jpg");
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkForInvalidMediaInBlocks", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
test("returns ok when blocks array is empty", () => {
|
||||
const blocks: TSurveyBlock[] = [];
|
||||
|
||||
const result = checkForInvalidMediaInBlocks(blocks);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
test("returns ok when blocks have no images", () => {
|
||||
const blocks: TSurveyBlock[] = [
|
||||
{
|
||||
id: "block-1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
{
|
||||
id: "elem-1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
} as unknown as TSurveyElement,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkForInvalidMediaInBlocks(blocks);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
test("returns ok when all element images are valid", () => {
|
||||
vi.spyOn(fileValidation, "isValidImageFile").mockReturnValue(true);
|
||||
|
||||
const blocks: TSurveyBlock[] = [
|
||||
{
|
||||
id: "block-1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
{
|
||||
id: "elem-1",
|
||||
type: TSurveyElementTypeEnum.PictureSelection,
|
||||
headline: { default: "Question" },
|
||||
required: false,
|
||||
choices: [
|
||||
{ id: "c1", label: { default: "Option 1" }, imageUrl: "image1.jpg" },
|
||||
{ id: "c2", label: { default: "Option 2" }, imageUrl: "image2.jpg" },
|
||||
],
|
||||
} as unknown as TSurveyElement,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkForInvalidMediaInBlocks(blocks);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(fileValidation.isValidImageFile).toHaveBeenCalledWith("image1.jpg");
|
||||
expect(fileValidation.isValidImageFile).toHaveBeenCalledWith("image2.jpg");
|
||||
});
|
||||
|
||||
test("returns error when element image is invalid", () => {
|
||||
vi.spyOn(fileValidation, "isValidImageFile").mockReturnValue(false);
|
||||
|
||||
const blocks: TSurveyBlock[] = [
|
||||
{
|
||||
id: "block-1",
|
||||
name: "Welcome Block",
|
||||
elements: [
|
||||
{
|
||||
id: "welcome",
|
||||
type: TSurveyElementTypeEnum.PictureSelection,
|
||||
headline: { default: "Welcome" },
|
||||
required: false,
|
||||
choices: [
|
||||
{ id: "c1", label: { default: "Option 1" }, imageUrl: "image1.jpg" },
|
||||
{ id: "c2", label: { default: "Option 2" }, imageUrl: "image2.jpg" },
|
||||
],
|
||||
} as unknown as TSurveyElement,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkForInvalidMediaInBlocks(blocks);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
console.log(result.error);
|
||||
expect(result.error.message).toBe(
|
||||
'Invalid image URL in choice 1 of question 1 of block "Welcome Block"'
|
||||
);
|
||||
}
|
||||
expect(fileValidation.isValidImageFile).toHaveBeenCalledWith("image1.jpg");
|
||||
});
|
||||
|
||||
test("returns ok when all choice images are valid", () => {
|
||||
vi.spyOn(fileValidation, "isValidImageFile").mockReturnValue(true);
|
||||
|
||||
const blocks: TSurveyBlock[] = [
|
||||
{
|
||||
id: "block-1",
|
||||
name: "Choice Block",
|
||||
elements: [
|
||||
{
|
||||
id: "choice-q",
|
||||
type: TSurveyElementTypeEnum.PictureSelection,
|
||||
headline: { default: "Pick one" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "c1", imageUrl: "image1.jpg" },
|
||||
{ id: "c2", imageUrl: "image2.jpg" },
|
||||
],
|
||||
} as unknown as TSurveyElement,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkForInvalidMediaInBlocks(blocks);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(fileValidation.isValidImageFile).toHaveBeenCalledTimes(2);
|
||||
expect(fileValidation.isValidImageFile).toHaveBeenNthCalledWith(1, "image1.jpg");
|
||||
expect(fileValidation.isValidImageFile).toHaveBeenNthCalledWith(2, "image2.jpg");
|
||||
});
|
||||
|
||||
test("returns error when choice image is invalid", () => {
|
||||
vi.spyOn(fileValidation, "isValidImageFile").mockImplementation((url) => url === "valid.jpg");
|
||||
|
||||
const blocks: TSurveyBlock[] = [
|
||||
{
|
||||
id: "block-1",
|
||||
name: "Picture Selection",
|
||||
elements: [
|
||||
{
|
||||
id: "pic-select",
|
||||
type: TSurveyElementTypeEnum.PictureSelection,
|
||||
headline: { default: "Select a picture" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "c1", imageUrl: "valid.jpg" },
|
||||
{ id: "c2", imageUrl: "invalid.txt" },
|
||||
],
|
||||
} as unknown as TSurveyElement,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkForInvalidMediaInBlocks(blocks);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error.message).toBe(
|
||||
'Invalid image URL in choice 2 of question 1 of block "Picture Selection"'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("returns ok when video URL is valid (YouTube)", () => {
|
||||
vi.spyOn(videoValidation, "isValidVideoUrl").mockReturnValue(true);
|
||||
|
||||
const blocks: TSurveyBlock[] = [
|
||||
{
|
||||
id: "block-1",
|
||||
name: "Video Block",
|
||||
elements: [
|
||||
{
|
||||
id: "video-q",
|
||||
type: TSurveyElementTypeEnum.CTA,
|
||||
headline: { default: "Watch this" },
|
||||
required: false,
|
||||
videoUrl: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
} as unknown as TSurveyElement,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkForInvalidMediaInBlocks(blocks);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(videoValidation.isValidVideoUrl).toHaveBeenCalledWith(
|
||||
"https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
);
|
||||
});
|
||||
|
||||
test("returns error when video URL is invalid (not YouTube/Vimeo/Loom)", () => {
|
||||
vi.spyOn(videoValidation, "isValidVideoUrl").mockReturnValue(false);
|
||||
|
||||
const blocks: TSurveyBlock[] = [
|
||||
{
|
||||
id: "block-1",
|
||||
name: "Video Block",
|
||||
elements: [
|
||||
{
|
||||
id: "video-q",
|
||||
type: TSurveyElementTypeEnum.CTA,
|
||||
headline: { default: "Watch this" },
|
||||
required: false,
|
||||
videoUrl: "https://example.com/video.mp4",
|
||||
} as unknown as TSurveyElement,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkForInvalidMediaInBlocks(blocks);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error.message).toContain("Invalid video URL");
|
||||
expect(result.error.message).toContain("question 1");
|
||||
expect(result.error.message).toContain("YouTube, Vimeo, and Loom");
|
||||
}
|
||||
});
|
||||
|
||||
test("validates images across multiple blocks", () => {
|
||||
vi.spyOn(fileValidation, "isValidImageFile").mockReturnValue(true);
|
||||
|
||||
const blocks: TSurveyBlock[] = [
|
||||
{
|
||||
id: "block-1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
{
|
||||
id: "elem-1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Q1" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
imageUrl: "image1.jpg",
|
||||
} as unknown as TSurveyElement,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "block-2",
|
||||
name: "Block 2",
|
||||
elements: [
|
||||
{
|
||||
id: "elem-2",
|
||||
type: TSurveyElementTypeEnum.Rating,
|
||||
headline: { default: "Q2" },
|
||||
required: true,
|
||||
range: 5,
|
||||
scale: "star",
|
||||
imageUrl: "image2.jpg",
|
||||
} as unknown as TSurveyElement,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkForInvalidMediaInBlocks(blocks);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(fileValidation.isValidImageFile).toHaveBeenCalledTimes(2);
|
||||
expect(fileValidation.isValidImageFile).toHaveBeenNthCalledWith(1, "image1.jpg");
|
||||
expect(fileValidation.isValidImageFile).toHaveBeenNthCalledWith(2, "image2.jpg");
|
||||
});
|
||||
|
||||
test("stops at first invalid image and returns specific error", () => {
|
||||
vi.spyOn(fileValidation, "isValidImageFile").mockImplementation((url) => url !== "bad-image.gif");
|
||||
|
||||
const blocks: TSurveyBlock[] = [
|
||||
{
|
||||
id: "block-1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
{
|
||||
id: "elem-1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Q1" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
imageUrl: "good.jpg",
|
||||
} as unknown as TSurveyElement,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "block-2",
|
||||
name: "Block 2",
|
||||
elements: [
|
||||
{
|
||||
id: "elem-2",
|
||||
type: TSurveyElementTypeEnum.CTA,
|
||||
headline: { default: "Q2" },
|
||||
required: false,
|
||||
imageUrl: "bad-image.gif",
|
||||
} as unknown as TSurveyElement,
|
||||
{
|
||||
id: "elem-3",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Q3" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
imageUrl: "another.jpg",
|
||||
} as unknown as TSurveyElement,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkForInvalidMediaInBlocks(blocks);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error.message).toBe('Invalid image URL in question 1 of block "Block 2" (block 2)');
|
||||
}
|
||||
// Should stop after finding first invalid image
|
||||
expect(fileValidation.isValidImageFile).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test("validates choices without imageUrl (skips gracefully)", () => {
|
||||
vi.spyOn(fileValidation, "isValidImageFile").mockReturnValue(true);
|
||||
|
||||
const blocks: TSurveyBlock[] = [
|
||||
{
|
||||
id: "block-1",
|
||||
name: "Choice Block",
|
||||
elements: [
|
||||
{
|
||||
id: "mc-q",
|
||||
type: TSurveyElementTypeEnum.PictureSelection,
|
||||
headline: { default: "Pick one" },
|
||||
required: true,
|
||||
choices: [{ id: "c1", imageUrl: "image.jpg" }],
|
||||
} as unknown as TSurveyElement,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkForInvalidMediaInBlocks(blocks);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
// Only validates the one with imageUrl
|
||||
expect(fileValidation.isValidImageFile).toHaveBeenCalledTimes(1);
|
||||
expect(fileValidation.isValidImageFile).toHaveBeenCalledWith("image.jpg");
|
||||
});
|
||||
|
||||
test("handles multiple elements in single block", () => {
|
||||
vi.spyOn(fileValidation, "isValidImageFile").mockReturnValue(true);
|
||||
|
||||
const blocks: TSurveyBlock[] = [
|
||||
{
|
||||
id: "block-1",
|
||||
name: "Multi-Element Block",
|
||||
elements: [
|
||||
{
|
||||
id: "elem-1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Q1" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
imageUrl: "img1.jpg",
|
||||
} as unknown as TSurveyElement,
|
||||
{
|
||||
id: "elem-2",
|
||||
type: TSurveyElementTypeEnum.Rating,
|
||||
headline: { default: "Q2" },
|
||||
required: true,
|
||||
range: 5,
|
||||
scale: "number",
|
||||
imageUrl: "img2.jpg",
|
||||
} as unknown as TSurveyElement,
|
||||
{
|
||||
id: "elem-3",
|
||||
type: TSurveyElementTypeEnum.CTA,
|
||||
headline: { default: "Q3" },
|
||||
required: false,
|
||||
imageUrl: "img3.jpg",
|
||||
} as unknown as TSurveyElement,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkForInvalidMediaInBlocks(blocks);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(fileValidation.isValidImageFile).toHaveBeenCalledTimes(3);
|
||||
expect(fileValidation.isValidImageFile).toHaveBeenNthCalledWith(1, "img1.jpg");
|
||||
expect(fileValidation.isValidImageFile).toHaveBeenNthCalledWith(2, "img2.jpg");
|
||||
expect(fileValidation.isValidImageFile).toHaveBeenNthCalledWith(3, "img3.jpg");
|
||||
});
|
||||
|
||||
test("validates both element imageUrl and choice imageUrls", () => {
|
||||
vi.spyOn(fileValidation, "isValidImageFile").mockReturnValue(true);
|
||||
|
||||
const blocks: TSurveyBlock[] = [
|
||||
{
|
||||
id: "block-1",
|
||||
name: "Complex Block",
|
||||
elements: [
|
||||
{
|
||||
id: "elem-1",
|
||||
type: TSurveyElementTypeEnum.PictureSelection,
|
||||
headline: { default: "Choose" },
|
||||
required: true,
|
||||
imageUrl: "element-image.jpg",
|
||||
choices: [
|
||||
{ id: "c1", imageUrl: "choice1.jpg" },
|
||||
{ id: "c2", imageUrl: "choice2.jpg" },
|
||||
],
|
||||
} as unknown as TSurveyElement,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkForInvalidMediaInBlocks(blocks);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(fileValidation.isValidImageFile).toHaveBeenCalledTimes(3);
|
||||
expect(fileValidation.isValidImageFile).toHaveBeenNthCalledWith(1, "element-image.jpg");
|
||||
expect(fileValidation.isValidImageFile).toHaveBeenNthCalledWith(2, "choice1.jpg");
|
||||
expect(fileValidation.isValidImageFile).toHaveBeenNthCalledWith(3, "choice2.jpg");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import "server-only";
|
||||
import { Result, err, ok } from "@formbricks/types/error-handlers";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
import { TSurveyBlock } from "@formbricks/types/surveys/blocks";
|
||||
import {
|
||||
TSurveyElement,
|
||||
TSurveyElementTypeEnum,
|
||||
TSurveyPictureChoice,
|
||||
} from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { isValidVideoUrl } from "@/lib/utils/video-upload";
|
||||
import { isValidImageFile } from "@/modules/storage/utils";
|
||||
|
||||
export const transformPrismaSurvey = <T extends TSurvey | TJsEnvironmentStateSurvey>(
|
||||
@@ -56,3 +64,188 @@ export const checkForInvalidImagesInQuestions = (questions: TSurveyQuestion[]) =
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates a single choice's image URL
|
||||
* @param choice - Choice to validate
|
||||
* @param choiceIdx - Index of the choice for error reporting
|
||||
* @param elementIdx - Index of the element for error reporting
|
||||
* @param blockName - Block name for error reporting
|
||||
* @returns Result with void data on success or Error on failure
|
||||
*/
|
||||
const validateChoiceImage = (
|
||||
choice: TSurveyPictureChoice,
|
||||
choiceIdx: number,
|
||||
elementIdx: number,
|
||||
blockName: string
|
||||
): Result<void, Error> => {
|
||||
if (choice.imageUrl && !isValidImageFile(choice.imageUrl)) {
|
||||
return err(
|
||||
new Error(
|
||||
`Invalid image URL in choice ${choiceIdx + 1} of question ${elementIdx + 1} of block "${blockName}"`
|
||||
)
|
||||
);
|
||||
}
|
||||
return ok(undefined);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates choice images for picture selection elements
|
||||
* Only picture selection elements have imageUrl in choices
|
||||
* @param element - Element with choices to validate
|
||||
* @param elementIdx - Index of the element for error reporting
|
||||
* @param blockName - Block name for error reporting
|
||||
* @returns Result with void data on success or Error on failure
|
||||
*/
|
||||
const validatePictureSelectionChoiceImages = (
|
||||
element: TSurveyElement,
|
||||
elementIdx: number,
|
||||
blockName: string
|
||||
): Result<void, Error> => {
|
||||
// Only validate choices for picture selection elements
|
||||
if (element.type !== TSurveyElementTypeEnum.PictureSelection) {
|
||||
return ok(undefined);
|
||||
}
|
||||
|
||||
if (!("choices" in element) || !Array.isArray(element.choices)) {
|
||||
return ok(undefined);
|
||||
}
|
||||
|
||||
for (let choiceIdx = 0; choiceIdx < element.choices.length; choiceIdx++) {
|
||||
const result = validateChoiceImage(element.choices[choiceIdx], choiceIdx, elementIdx, blockName);
|
||||
if (!result.ok) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return ok(undefined);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates a single element's image URL, video URL, and picture selection choice images
|
||||
* @param element - Element to validate
|
||||
* @param elementIdx - Index of the element for error reporting
|
||||
* @param blockIdx - Index of the block for error reporting
|
||||
* @param blockName - Block name for error reporting
|
||||
* @returns Result with void data on success or Error on failure
|
||||
*/
|
||||
const validateElement = (
|
||||
element: TSurveyElement,
|
||||
elementIdx: number,
|
||||
blockIdx: number,
|
||||
blockName: string
|
||||
): Result<void, Error> => {
|
||||
// Check element imageUrl
|
||||
if (element.imageUrl && !isValidImageFile(element.imageUrl)) {
|
||||
return err(
|
||||
new Error(
|
||||
`Invalid image URL in question ${elementIdx + 1} of block "${blockName}" (block ${blockIdx + 1})`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check element videoUrl
|
||||
if (element.videoUrl && !isValidVideoUrl(element.videoUrl)) {
|
||||
return err(
|
||||
new Error(
|
||||
`Invalid video URL in question ${elementIdx + 1} of block "${blockName}" (block ${blockIdx + 1}). Only YouTube, Vimeo, and Loom URLs are supported.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check choices for picture selection
|
||||
return validatePictureSelectionChoiceImages(element, elementIdx, blockName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates that all media URLs (images and videos) in blocks are valid
|
||||
* - Validates element imageUrl
|
||||
* - Validates element videoUrl
|
||||
* - Validates choice imageUrl for picture selection elements
|
||||
* @param blocks - Array of survey blocks to validate
|
||||
* @returns Result with void data on success or Error on failure
|
||||
*/
|
||||
export const checkForInvalidMediaInBlocks = (blocks: TSurveyBlock[]): Result<void, Error> => {
|
||||
for (let blockIdx = 0; blockIdx < blocks.length; blockIdx++) {
|
||||
const block = blocks[blockIdx];
|
||||
|
||||
for (let elementIdx = 0; elementIdx < block.elements.length; elementIdx++) {
|
||||
const result = validateElement(block.elements[elementIdx], elementIdx, blockIdx, block.name);
|
||||
if (!result.ok) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ok(undefined);
|
||||
};
|
||||
|
||||
/**
|
||||
* Strips isDraft field from elements before saving to database
|
||||
* Note: Blocks don't have isDraft since block IDs are CUIDs (not user-editable)
|
||||
* Only element IDs need protection as they're user-editable and used in responses
|
||||
* @param blocks - Array of survey blocks
|
||||
* @returns New array with isDraft stripped from all elements
|
||||
*/
|
||||
export const stripIsDraftFromBlocks = (blocks: TSurveyBlock[]): TSurveyBlock[] => {
|
||||
return blocks.map((block) => ({
|
||||
...block,
|
||||
elements: block.elements.map((element) => {
|
||||
const { isDraft, ...elementRest } = element;
|
||||
return elementRest;
|
||||
}),
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates and prepares blocks for persistence
|
||||
* - Validates all media URLs (images and videos) in blocks
|
||||
* - Strips isDraft flags from elements
|
||||
* @param blocks - Array of survey blocks to validate and prepare
|
||||
* @returns Prepared blocks ready for database persistence
|
||||
* @throws Error if any media validation fails
|
||||
*/
|
||||
export const validateMediaAndPrepareBlocks = (blocks: TSurveyBlock[]): TSurveyBlock[] => {
|
||||
// Validate media (images and videos)
|
||||
const validation = checkForInvalidMediaInBlocks(blocks);
|
||||
if (!validation.ok) {
|
||||
throw validation.error;
|
||||
}
|
||||
|
||||
// Strip isDraft
|
||||
return stripIsDraftFromBlocks(blocks);
|
||||
};
|
||||
|
||||
/**
|
||||
* Derives a flat array of elements from the survey's blocks structure
|
||||
* Useful for server-side processing where we need to iterate over all questions
|
||||
* Note: This is duplicated from the client-side survey utils since this file is server-only
|
||||
* @param blocks - Array of survey blocks
|
||||
* @returns Flat array of all elements across all blocks
|
||||
*/
|
||||
export const getElementsFromBlocks = (blocks: TSurveyBlock[]): TSurveyElement[] => {
|
||||
return blocks.flatMap((block) => block.elements);
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the location of an element within the survey blocks
|
||||
* @param survey - The survey object
|
||||
* @param elementId - The ID of the element to find
|
||||
* @returns Object containing blockId, blockIndex, elementIndex and the block
|
||||
*/
|
||||
export const findElementLocation = (
|
||||
survey: TSurvey,
|
||||
elementId: string
|
||||
): { blockId: string | null; blockIndex: number; elementIndex: number; block: TSurveyBlock | null } => {
|
||||
const blocks = survey.blocks;
|
||||
|
||||
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
|
||||
const block = blocks[blockIndex];
|
||||
const elementIndex = block.elements.findIndex((e) => e.id === elementId);
|
||||
if (elementIndex !== -1) {
|
||||
return { blockId: block.id, blockIndex, elementIndex, block };
|
||||
}
|
||||
}
|
||||
|
||||
return { blockId: null, blockIndex: -1, elementIndex: -1, block: null };
|
||||
};
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { TResponseData, TResponseVariables } from "@formbricks/types/responses";
|
||||
import {
|
||||
TConditionGroup,
|
||||
TSingleCondition,
|
||||
TSurveyLogic,
|
||||
TSurveyLogicAction,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TSurveyBlockLogic, TSurveyBlockLogicAction } from "@formbricks/types/surveys/blocks";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TConditionGroup, TSingleCondition } from "@formbricks/types/surveys/logic";
|
||||
import { TSurveyLogicAction } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
addConditionBelow,
|
||||
createGroupFromResource,
|
||||
@@ -36,9 +33,6 @@ describe("surveyLogic", () => {
|
||||
type: "link",
|
||||
status: "inProgress",
|
||||
welcomeCard: {
|
||||
html: {
|
||||
default: "Thanks for providing your feedback - let's go!",
|
||||
},
|
||||
enabled: false,
|
||||
headline: {
|
||||
default: "Welcome!",
|
||||
@@ -49,25 +43,28 @@ describe("surveyLogic", () => {
|
||||
timeToFinish: false,
|
||||
showResponseCount: false,
|
||||
},
|
||||
questions: [
|
||||
blocks: [
|
||||
{
|
||||
id: "vjniuob08ggl8dewl0hwed41",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: {
|
||||
default: "What would you like to know?",
|
||||
},
|
||||
required: true,
|
||||
charLimit: {},
|
||||
inputType: "email",
|
||||
longAnswer: false,
|
||||
buttonLabel: {
|
||||
default: "Next",
|
||||
},
|
||||
placeholder: {
|
||||
default: "example@email.com",
|
||||
},
|
||||
id: "block1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
{
|
||||
id: "vjniuob08ggl8dewl0hwed41",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: {
|
||||
default: "What would you like to know?",
|
||||
},
|
||||
required: true,
|
||||
charLimit: { enabled: false },
|
||||
inputType: "email",
|
||||
placeholder: {
|
||||
default: "example@email.com",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
questions: [],
|
||||
endings: [
|
||||
{
|
||||
id: "gt1yoaeb5a3istszxqbl08mk",
|
||||
@@ -132,7 +129,7 @@ describe("surveyLogic", () => {
|
||||
});
|
||||
|
||||
test("duplicateLogicItem duplicates IDs recursively", () => {
|
||||
const logic: TSurveyLogic = {
|
||||
const logic: TSurveyBlockLogic = {
|
||||
id: "L1",
|
||||
conditions: simpleGroup(),
|
||||
actions: [{ id: "A1", objective: "requireAnswer", target: "q1" }],
|
||||
@@ -211,13 +208,13 @@ describe("surveyLogic", () => {
|
||||
});
|
||||
|
||||
test("getUpdatedActionBody returns new action bodies correctly", () => {
|
||||
const base: TSurveyLogicAction = { id: "A", objective: "requireAnswer", target: "q" };
|
||||
const base: TSurveyBlockLogicAction = { id: "A", objective: "requireAnswer", target: "q" };
|
||||
const calc = getUpdatedActionBody(base, "calculate");
|
||||
expect(calc.objective).toBe("calculate");
|
||||
const req = getUpdatedActionBody(calc, "requireAnswer");
|
||||
expect(req.objective).toBe("requireAnswer");
|
||||
const jump = getUpdatedActionBody(req, "jumpToQuestion");
|
||||
expect(jump.objective).toBe("jumpToQuestion");
|
||||
const jump = getUpdatedActionBody(req, "jumpToBlock");
|
||||
expect(jump.objective).toBe("jumpToBlock");
|
||||
});
|
||||
|
||||
test("evaluateLogic handles AND/OR groups and single conditions", () => {
|
||||
@@ -249,7 +246,7 @@ describe("surveyLogic", () => {
|
||||
test("performActions calculates, requires, and jumps correctly", () => {
|
||||
const data: TResponseData = { q: "5" };
|
||||
const initialVars: TResponseVariables = {};
|
||||
const actions: TSurveyLogicAction[] = [
|
||||
const actions: TSurveyBlockLogicAction[] = [
|
||||
{
|
||||
id: "a1",
|
||||
objective: "calculate",
|
||||
@@ -258,11 +255,11 @@ describe("surveyLogic", () => {
|
||||
value: { type: "static", value: 3 },
|
||||
},
|
||||
{ id: "a2", objective: "requireAnswer", target: "q2" },
|
||||
{ id: "a3", objective: "jumpToQuestion", target: "q3" },
|
||||
{ id: "a3", objective: "jumpToBlock", target: "q3" },
|
||||
];
|
||||
const result = performActions(mockSurvey, actions, data, initialVars);
|
||||
expect(result.calculations.v).toBe(3);
|
||||
expect(result.requiredQuestionIds).toContain("q2");
|
||||
expect(result.requiredElementIds).toContain("q2");
|
||||
expect(result.jumpTarget).toBe("q3");
|
||||
});
|
||||
|
||||
@@ -451,7 +448,7 @@ describe("surveyLogic", () => {
|
||||
mockSurvey,
|
||||
{},
|
||||
vars,
|
||||
group({ ...baseCond("equals", "foo"), leftOperand: { type: "question", value: "notfound" } }),
|
||||
group({ ...baseCond("equals", "foo"), leftOperand: { type: "element", value: "notfound" } }),
|
||||
"en"
|
||||
)
|
||||
).toBe(false);
|
||||
@@ -463,7 +460,7 @@ describe("surveyLogic", () => {
|
||||
variables: [{ id: "v", name: "num", type: "number", value: 0 }],
|
||||
};
|
||||
const data: TResponseData = { q: 2 };
|
||||
const actions: TSurveyLogicAction[] = [
|
||||
const actions: TSurveyBlockLogicAction[] = [
|
||||
{
|
||||
id: "a1",
|
||||
objective: "calculate",
|
||||
@@ -750,84 +747,87 @@ describe("surveyLogic", () => {
|
||||
test("getLeftOperandValue handles different question types", () => {
|
||||
const surveyWithQuestions: TJsEnvironmentStateSurvey = {
|
||||
...mockSurvey,
|
||||
questions: [
|
||||
...mockSurvey.questions,
|
||||
blocks: [
|
||||
{
|
||||
id: "numQuestion",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Number question" },
|
||||
required: true,
|
||||
inputType: "number",
|
||||
charLimit: { enabled: false },
|
||||
},
|
||||
{
|
||||
id: "mcSingle",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "MC Single" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "choice1", label: { default: "Choice 1" } },
|
||||
{ id: "choice2", label: { default: "Choice 2" } },
|
||||
{ id: "other", label: { default: "Other" } },
|
||||
id: "block1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
...mockSurvey.blocks[0].elements,
|
||||
{
|
||||
id: "numQuestion",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Number question" },
|
||||
required: true,
|
||||
inputType: "number",
|
||||
charLimit: { enabled: false },
|
||||
},
|
||||
{
|
||||
id: "mcSingle",
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||
headline: { default: "MC Single" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "choice1", label: { default: "Choice 1" } },
|
||||
{ id: "choice2", label: { default: "Choice 2" } },
|
||||
{ id: "other", label: { default: "Other" } },
|
||||
],
|
||||
shuffleOption: "none",
|
||||
},
|
||||
{
|
||||
id: "mcMulti",
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceMulti,
|
||||
headline: { default: "MC Multi" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "choice1", label: { default: "Choice 1" } },
|
||||
{ id: "choice2", label: { default: "Choice 2" } },
|
||||
],
|
||||
shuffleOption: "none",
|
||||
},
|
||||
{
|
||||
id: "matrixQ",
|
||||
type: TSurveyElementTypeEnum.Matrix,
|
||||
headline: { default: "Matrix Question" },
|
||||
required: true,
|
||||
rows: [
|
||||
{ id: "row-1", label: { default: "Row 1" } },
|
||||
{ id: "row-2", label: { default: "Row 2" } },
|
||||
],
|
||||
columns: [
|
||||
{ id: "col-1", label: { default: "Column 1" } },
|
||||
{ id: "col-2", label: { default: "Column 2" } },
|
||||
],
|
||||
shuffleOption: "none",
|
||||
},
|
||||
{
|
||||
id: "pictureQ",
|
||||
type: TSurveyElementTypeEnum.PictureSelection,
|
||||
allowMulti: false,
|
||||
headline: { default: "Picture Selection" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "pic1", imageUrl: "url1" },
|
||||
{ id: "pic2", imageUrl: "url2" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "dateQ",
|
||||
type: TSurveyElementTypeEnum.Date,
|
||||
format: "M-d-y",
|
||||
headline: { default: "Date Question" },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: "fileQ",
|
||||
type: TSurveyElementTypeEnum.FileUpload,
|
||||
allowMultipleFiles: false,
|
||||
headline: { default: "File Upload" },
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
buttonLabel: { default: "Next" },
|
||||
},
|
||||
{
|
||||
id: "mcMulti",
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceMulti,
|
||||
headline: { default: "MC Multi" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "choice1", label: { default: "Choice 1" } },
|
||||
{ id: "choice2", label: { default: "Choice 2" } },
|
||||
],
|
||||
buttonLabel: { default: "Next" },
|
||||
},
|
||||
{
|
||||
id: "matrixQ",
|
||||
type: TSurveyQuestionTypeEnum.Matrix,
|
||||
headline: { default: "Matrix Question" },
|
||||
required: true,
|
||||
rows: [
|
||||
{ id: "row-1", label: { default: "Row 1" } },
|
||||
{ id: "row-2", label: { default: "Row 2" } },
|
||||
],
|
||||
columns: [
|
||||
{ id: "col-1", label: { default: "Column 1" } },
|
||||
{ id: "col-2", label: { default: "Column 2" } },
|
||||
],
|
||||
buttonLabel: { default: "Next" },
|
||||
shuffleOption: "none",
|
||||
},
|
||||
{
|
||||
id: "pictureQ",
|
||||
type: TSurveyQuestionTypeEnum.PictureSelection,
|
||||
allowMulti: false,
|
||||
headline: { default: "Picture Selection" },
|
||||
required: true,
|
||||
choices: [
|
||||
{ id: "pic1", imageUrl: "url1" },
|
||||
{ id: "pic2", imageUrl: "url2" },
|
||||
],
|
||||
buttonLabel: { default: "Next" },
|
||||
},
|
||||
{
|
||||
id: "dateQ",
|
||||
type: TSurveyQuestionTypeEnum.Date,
|
||||
format: "M-d-y",
|
||||
headline: { default: "Date Question" },
|
||||
required: true,
|
||||
buttonLabel: { default: "Next" },
|
||||
},
|
||||
{
|
||||
id: "fileQ",
|
||||
type: TSurveyQuestionTypeEnum.FileUpload,
|
||||
allowMultipleFiles: false,
|
||||
headline: { default: "File Upload" },
|
||||
required: true,
|
||||
buttonLabel: { default: "Next" },
|
||||
},
|
||||
],
|
||||
questions: [],
|
||||
variables: [
|
||||
{ id: "numVar", name: "numberVar", type: "number", value: 5 },
|
||||
{ id: "textVar", name: "textVar", type: "text", value: "hello" },
|
||||
@@ -854,7 +854,7 @@ describe("surveyLogic", () => {
|
||||
// Test number question
|
||||
const numberCondition: TSingleCondition = {
|
||||
id: "numCond",
|
||||
leftOperand: { type: "question", value: "numQuestion" },
|
||||
leftOperand: { type: "element", value: "numQuestion" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: 42 },
|
||||
};
|
||||
@@ -871,7 +871,7 @@ describe("surveyLogic", () => {
|
||||
// Test MC single with recognized choice
|
||||
const mcSingleCondition: TSingleCondition = {
|
||||
id: "mcCond",
|
||||
leftOperand: { type: "question", value: "mcSingle" },
|
||||
leftOperand: { type: "element", value: "mcSingle" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: "choice1" },
|
||||
};
|
||||
@@ -888,7 +888,7 @@ describe("surveyLogic", () => {
|
||||
// Test MC multi
|
||||
const mcMultiCondition: TSingleCondition = {
|
||||
id: "mcMultiCond",
|
||||
leftOperand: { type: "question", value: "mcMulti" },
|
||||
leftOperand: { type: "element", value: "mcMulti" },
|
||||
operator: "includesOneOf",
|
||||
rightOperand: { type: "static", value: ["choice1"] },
|
||||
};
|
||||
@@ -905,7 +905,7 @@ describe("surveyLogic", () => {
|
||||
// Test matrix question
|
||||
const matrixCondition: TSingleCondition = {
|
||||
id: "matrixCond",
|
||||
leftOperand: { type: "question", value: "matrixQ", meta: { row: "0" } },
|
||||
leftOperand: { type: "element", value: "matrixQ", meta: { row: "0" } },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: "0" },
|
||||
};
|
||||
@@ -939,7 +939,7 @@ describe("surveyLogic", () => {
|
||||
// Test with missing question
|
||||
const missingQuestionCondition: TSingleCondition = {
|
||||
id: "missingCond",
|
||||
leftOperand: { type: "question", value: "nonExistent" },
|
||||
leftOperand: { type: "element", value: "nonExistent" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: "foo" },
|
||||
};
|
||||
@@ -973,7 +973,7 @@ describe("surveyLogic", () => {
|
||||
// Test MC single with "other" option
|
||||
const otherCondition: TSingleCondition = {
|
||||
id: "otherCond",
|
||||
leftOperand: { type: "question", value: "mcSingle" },
|
||||
leftOperand: { type: "element", value: "mcSingle" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: "Unknown option" },
|
||||
};
|
||||
@@ -990,7 +990,7 @@ describe("surveyLogic", () => {
|
||||
// Test matrix with invalid row index
|
||||
const invalidMatrixCondition: TSingleCondition = {
|
||||
id: "invalidMatrixCond",
|
||||
leftOperand: { type: "question", value: "matrixQ", meta: { row: "999" } },
|
||||
leftOperand: { type: "element", value: "matrixQ", meta: { row: "999" } },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: "0" },
|
||||
};
|
||||
@@ -1008,17 +1008,24 @@ describe("surveyLogic", () => {
|
||||
test("getRightOperandValue handles different data types and sources", () => {
|
||||
const surveyWithVars: TJsEnvironmentStateSurvey = {
|
||||
...mockSurvey,
|
||||
questions: [
|
||||
...mockSurvey.questions,
|
||||
blocks: [
|
||||
{
|
||||
id: "question1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Question 1" },
|
||||
required: true,
|
||||
inputType: "text",
|
||||
charLimit: { enabled: false },
|
||||
id: "block1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
...mockSurvey.blocks[0].elements,
|
||||
{
|
||||
id: "question1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Question 1" },
|
||||
required: true,
|
||||
inputType: "text",
|
||||
charLimit: { enabled: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
questions: [],
|
||||
variables: [
|
||||
{ id: "numVar", name: "numberVar", type: "number", value: 5 },
|
||||
{ id: "textVar", name: "textVar", type: "text", value: "hello" },
|
||||
@@ -1042,7 +1049,7 @@ describe("surveyLogic", () => {
|
||||
id: "questionCond",
|
||||
leftOperand: { type: "hiddenField", value: "f" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "question", value: "question1" },
|
||||
rightOperand: { type: "element", value: "question1" },
|
||||
};
|
||||
|
||||
const variableCondition: TSingleCondition = {
|
||||
@@ -1143,7 +1150,7 @@ describe("surveyLogic", () => {
|
||||
objective: "calculate",
|
||||
variableId: "numVar",
|
||||
operator: "add",
|
||||
value: { type: "question", value: "questionNum" },
|
||||
value: { type: "element", value: "questionNum" },
|
||||
};
|
||||
|
||||
// Test with hidden field value
|
||||
@@ -1161,7 +1168,7 @@ describe("surveyLogic", () => {
|
||||
objective: "calculate",
|
||||
variableId: "textVar",
|
||||
operator: "concat",
|
||||
value: { type: "question", value: "questionText" },
|
||||
value: { type: "element", value: "questionText" },
|
||||
};
|
||||
|
||||
// Test with missing variable
|
||||
@@ -1179,7 +1186,7 @@ describe("surveyLogic", () => {
|
||||
objective: "calculate",
|
||||
variableId: "numVar",
|
||||
operator: "add",
|
||||
value: { type: "question", value: "nonExistentQuestion" },
|
||||
value: { type: "element", value: "nonExistentQuestion" },
|
||||
};
|
||||
|
||||
// Test with other math operations
|
||||
@@ -1319,24 +1326,29 @@ describe("surveyLogic", () => {
|
||||
test("getLeftOperandValue handles number input type with non-number value", () => {
|
||||
const surveyWithNumberInput: TJsEnvironmentStateSurvey = {
|
||||
...mockSurvey,
|
||||
questions: [
|
||||
blocks: [
|
||||
{
|
||||
id: "numQuestion",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Number question" },
|
||||
required: true,
|
||||
inputType: "number",
|
||||
placeholder: { default: "Enter a number" },
|
||||
buttonLabel: { default: "Next" },
|
||||
longAnswer: false,
|
||||
charLimit: {},
|
||||
id: "block1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
{
|
||||
id: "numQuestion",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
headline: { default: "Number question" },
|
||||
required: true,
|
||||
inputType: "number",
|
||||
placeholder: { default: "Enter a number" },
|
||||
charLimit: { enabled: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
questions: [],
|
||||
};
|
||||
|
||||
const condition: TSingleCondition = {
|
||||
id: "numCond",
|
||||
leftOperand: { type: "question", value: "numQuestion" },
|
||||
leftOperand: { type: "element", value: "numQuestion" },
|
||||
operator: "equals",
|
||||
rightOperand: { type: "static", value: 0 },
|
||||
};
|
||||
|
||||
@@ -2,17 +2,15 @@ import { createId } from "@paralleldrive/cuid2";
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { TResponseData, TResponseVariables } from "@formbricks/types/responses";
|
||||
import {
|
||||
TActionCalculate,
|
||||
TActionObjective,
|
||||
TConditionGroup,
|
||||
TSingleCondition,
|
||||
TSurveyLogic,
|
||||
TSurveyLogicAction,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
TSurveyVariable,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
TSurveyBlockLogic,
|
||||
TSurveyBlockLogicAction,
|
||||
TSurveyBlockLogicActionObjective,
|
||||
} from "@formbricks/types/surveys/blocks";
|
||||
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TConditionGroup, TSingleCondition } from "@formbricks/types/surveys/logic";
|
||||
import { TActionCalculate, TSurveyLogicAction, TSurveyVariable } from "@formbricks/types/surveys/types";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
|
||||
type TCondition = TSingleCondition | TConditionGroup;
|
||||
|
||||
@@ -20,7 +18,7 @@ export const isConditionGroup = (condition: TCondition): condition is TCondition
|
||||
return (condition as TConditionGroup).connector !== undefined;
|
||||
};
|
||||
|
||||
export const duplicateLogicItem = (logicItem: TSurveyLogic): TSurveyLogic => {
|
||||
export const duplicateLogicItem = (logicItem: TSurveyBlockLogic): TSurveyBlockLogic => {
|
||||
const duplicateConditionGroup = (group: TConditionGroup): TConditionGroup => {
|
||||
return {
|
||||
...group,
|
||||
@@ -42,7 +40,7 @@ export const duplicateLogicItem = (logicItem: TSurveyLogic): TSurveyLogic => {
|
||||
};
|
||||
};
|
||||
|
||||
const duplicateAction = (action: TSurveyLogicAction): TSurveyLogicAction => {
|
||||
const duplicateAction = (action: TSurveyBlockLogicAction): TSurveyBlockLogicAction => {
|
||||
return {
|
||||
...action,
|
||||
id: createId(),
|
||||
@@ -198,9 +196,9 @@ export const updateCondition = (
|
||||
};
|
||||
|
||||
export const getUpdatedActionBody = (
|
||||
action: TSurveyLogicAction,
|
||||
objective: TActionObjective
|
||||
): TSurveyLogicAction => {
|
||||
action: TSurveyBlockLogicAction,
|
||||
objective: TSurveyBlockLogicActionObjective
|
||||
): TSurveyBlockLogicAction => {
|
||||
if (objective === action.objective) return action;
|
||||
switch (objective) {
|
||||
case "calculate":
|
||||
@@ -217,12 +215,14 @@ export const getUpdatedActionBody = (
|
||||
objective: "requireAnswer",
|
||||
target: "",
|
||||
};
|
||||
case "jumpToQuestion":
|
||||
case "jumpToBlock":
|
||||
return {
|
||||
id: action.id,
|
||||
objective: "jumpToQuestion",
|
||||
objective: "jumpToBlock",
|
||||
target: "",
|
||||
};
|
||||
default:
|
||||
return action;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -263,14 +263,17 @@ const evaluateSingleCondition = (
|
||||
condition.leftOperand,
|
||||
selectedLanguage
|
||||
);
|
||||
|
||||
let rightValue = condition.rightOperand
|
||||
? getRightOperandValue(localSurvey, data, variablesData, condition.rightOperand)
|
||||
: undefined;
|
||||
|
||||
let leftField: TSurveyQuestion | TSurveyVariable | string;
|
||||
const elements = getElementsFromBlocks(localSurvey.blocks);
|
||||
|
||||
if (condition.leftOperand?.type === "question") {
|
||||
leftField = localSurvey.questions.find((q) => q.id === condition.leftOperand?.value) as TSurveyQuestion;
|
||||
let leftField: TSurveyElement | TSurveyVariable | string;
|
||||
|
||||
if (condition.leftOperand?.type === "element") {
|
||||
leftField = elements.find((q) => q.id === condition.leftOperand?.value) ?? "";
|
||||
} else if (condition.leftOperand?.type === "variable") {
|
||||
leftField = localSurvey.variables.find((v) => v.id === condition.leftOperand?.value) as TSurveyVariable;
|
||||
} else if (condition.leftOperand?.type === "hiddenField") {
|
||||
@@ -279,12 +282,10 @@ const evaluateSingleCondition = (
|
||||
leftField = "";
|
||||
}
|
||||
|
||||
let rightField: TSurveyQuestion | TSurveyVariable | string;
|
||||
let rightField: TSurveyElement | TSurveyVariable | string;
|
||||
|
||||
if (condition.rightOperand?.type === "question") {
|
||||
rightField = localSurvey.questions.find(
|
||||
(q) => q.id === condition.rightOperand?.value
|
||||
) as TSurveyQuestion;
|
||||
if (condition.rightOperand?.type === "element") {
|
||||
rightField = elements.find((q) => q.id === condition.rightOperand?.value) ?? "";
|
||||
} else if (condition.rightOperand?.type === "variable") {
|
||||
rightField = localSurvey.variables.find(
|
||||
(v) => v.id === condition.rightOperand?.value
|
||||
@@ -305,25 +306,25 @@ const evaluateSingleCondition = (
|
||||
|
||||
switch (condition.operator) {
|
||||
case "equals":
|
||||
if (condition.leftOperand.type === "question") {
|
||||
if (condition.leftOperand.type === "element") {
|
||||
if (
|
||||
(leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date &&
|
||||
(leftField as TSurveyElement).type === TSurveyElementTypeEnum.Date &&
|
||||
typeof leftValue === "string" &&
|
||||
typeof rightValue === "string"
|
||||
) {
|
||||
// when left value is of date question and right value is string
|
||||
// when left value is of date element and right value is string
|
||||
return new Date(leftValue).getTime() === new Date(rightValue).getTime();
|
||||
}
|
||||
}
|
||||
|
||||
// when left value is of openText, hiddenField, variable and right value is of multichoice
|
||||
if (condition.rightOperand?.type === "question") {
|
||||
if ((rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.MultipleChoiceMulti) {
|
||||
if (condition.rightOperand?.type === "element") {
|
||||
if ((rightField as TSurveyElement).type === TSurveyElementTypeEnum.MultipleChoiceMulti) {
|
||||
if (Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.length === 1) {
|
||||
return rightValue.includes(leftValue as string);
|
||||
} else return false;
|
||||
} else if (
|
||||
(rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date &&
|
||||
(rightField as TSurveyElement).type === TSurveyElementTypeEnum.Date &&
|
||||
typeof leftValue === "string" &&
|
||||
typeof rightValue === "string"
|
||||
) {
|
||||
@@ -339,10 +340,10 @@ const evaluateSingleCondition = (
|
||||
leftValue === rightValue
|
||||
);
|
||||
case "doesNotEqual":
|
||||
// when left value is of picture selection question and right value is its option
|
||||
// when left value is of picture selection element and right value is its option
|
||||
if (
|
||||
condition.leftOperand.type === "question" &&
|
||||
(leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.PictureSelection &&
|
||||
condition.leftOperand.type === "element" &&
|
||||
(leftField as TSurveyElement).type === TSurveyElementTypeEnum.PictureSelection &&
|
||||
Array.isArray(leftValue) &&
|
||||
leftValue.length > 0 &&
|
||||
typeof rightValue === "string"
|
||||
@@ -350,10 +351,10 @@ const evaluateSingleCondition = (
|
||||
return !leftValue.includes(rightValue);
|
||||
}
|
||||
|
||||
// when left value is of date question and right value is string
|
||||
// when left value is of date element and right value is string
|
||||
if (
|
||||
condition.leftOperand.type === "question" &&
|
||||
(leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date &&
|
||||
condition.leftOperand.type === "element" &&
|
||||
(leftField as TSurveyElement).type === TSurveyElementTypeEnum.Date &&
|
||||
typeof leftValue === "string" &&
|
||||
typeof rightValue === "string"
|
||||
) {
|
||||
@@ -361,13 +362,13 @@ const evaluateSingleCondition = (
|
||||
}
|
||||
|
||||
// when left value is of openText, hiddenField, variable and right value is of multichoice
|
||||
if (condition.rightOperand?.type === "question") {
|
||||
if ((rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.MultipleChoiceMulti) {
|
||||
if (condition.rightOperand?.type === "element") {
|
||||
if ((rightField as TSurveyElement).type === TSurveyElementTypeEnum.MultipleChoiceMulti) {
|
||||
if (Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.length === 1) {
|
||||
return !rightValue.includes(leftValue as string);
|
||||
} else return false;
|
||||
} else if (
|
||||
(rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date &&
|
||||
(rightField as TSurveyElement).type === TSurveyElementTypeEnum.Date &&
|
||||
typeof leftValue === "string" &&
|
||||
typeof rightValue === "string"
|
||||
) {
|
||||
@@ -397,8 +398,8 @@ const evaluateSingleCondition = (
|
||||
case "isSubmitted":
|
||||
if (typeof leftValue === "string") {
|
||||
if (
|
||||
condition.leftOperand.type === "question" &&
|
||||
(leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.FileUpload &&
|
||||
condition.leftOperand.type === "element" &&
|
||||
(leftField as TSurveyElement).type === TSurveyElementTypeEnum.FileUpload &&
|
||||
leftValue
|
||||
) {
|
||||
return leftValue !== "skipped";
|
||||
@@ -510,13 +511,14 @@ const getLeftOperandValue = (
|
||||
selectedLanguage: string
|
||||
) => {
|
||||
switch (leftOperand.type) {
|
||||
case "question":
|
||||
const currentQuestion = localSurvey.questions.find((q) => q.id === leftOperand.value);
|
||||
if (!currentQuestion) return undefined;
|
||||
case "element":
|
||||
const elements = getElementsFromBlocks(localSurvey.blocks);
|
||||
const currentElement = elements.find((q) => q.id === leftOperand.value);
|
||||
if (!currentElement) return undefined;
|
||||
|
||||
const responseValue = data[leftOperand.value];
|
||||
|
||||
if (currentQuestion.type === "openText" && currentQuestion.inputType === "number") {
|
||||
if (currentElement.type === "openText" && currentElement.inputType === "number") {
|
||||
if (responseValue === undefined) return undefined;
|
||||
if (typeof responseValue === "string" && responseValue.trim() === "") return undefined;
|
||||
|
||||
@@ -524,11 +526,11 @@ const getLeftOperandValue = (
|
||||
return isNaN(numberValue) ? undefined : numberValue;
|
||||
}
|
||||
|
||||
if (currentQuestion.type === "multipleChoiceSingle" || currentQuestion.type === "multipleChoiceMulti") {
|
||||
const isOthersEnabled = currentQuestion.choices.at(-1)?.id === "other";
|
||||
if (currentElement.type === "multipleChoiceSingle" || currentElement.type === "multipleChoiceMulti") {
|
||||
const isOthersEnabled = currentElement.choices.at(-1)?.id === "other";
|
||||
|
||||
if (typeof responseValue === "string") {
|
||||
const choice = currentQuestion.choices.find((choice) => {
|
||||
const choice = currentElement.choices.find((choice) => {
|
||||
return getLocalizedValue(choice.label, selectedLanguage) === responseValue;
|
||||
});
|
||||
|
||||
@@ -544,7 +546,7 @@ const getLeftOperandValue = (
|
||||
} else if (Array.isArray(responseValue)) {
|
||||
let choices: string[] = [];
|
||||
responseValue.forEach((value) => {
|
||||
const foundChoice = currentQuestion.choices.find((choice) => {
|
||||
const foundChoice = currentElement.choices.find((choice) => {
|
||||
return getLocalizedValue(choice.label, selectedLanguage) === value;
|
||||
});
|
||||
|
||||
@@ -561,23 +563,23 @@ const getLeftOperandValue = (
|
||||
}
|
||||
|
||||
if (
|
||||
currentQuestion.type === "matrix" &&
|
||||
currentElement.type === "matrix" &&
|
||||
typeof responseValue === "object" &&
|
||||
!Array.isArray(responseValue)
|
||||
) {
|
||||
if (leftOperand.meta && leftOperand.meta.row !== undefined) {
|
||||
const rowIndex = Number(leftOperand.meta.row);
|
||||
|
||||
if (isNaN(rowIndex) || rowIndex < 0 || rowIndex >= currentQuestion.rows.length) {
|
||||
if (isNaN(rowIndex) || rowIndex < 0 || rowIndex >= currentElement.rows.length) {
|
||||
return undefined;
|
||||
}
|
||||
const row = getLocalizedValue(currentQuestion.rows[rowIndex].label, selectedLanguage);
|
||||
const row = getLocalizedValue(currentElement.rows[rowIndex].label, selectedLanguage);
|
||||
|
||||
const rowValue = responseValue[row];
|
||||
if (rowValue === "") return "";
|
||||
|
||||
if (rowValue) {
|
||||
const columnIndex = currentQuestion.columns.findIndex((column) => {
|
||||
const columnIndex = currentElement.columns.findIndex((column) => {
|
||||
return getLocalizedValue(column.label, selectedLanguage) === rowValue;
|
||||
});
|
||||
if (columnIndex === -1) return undefined;
|
||||
@@ -607,7 +609,7 @@ const getRightOperandValue = (
|
||||
if (!rightOperand) return undefined;
|
||||
|
||||
switch (rightOperand.type) {
|
||||
case "question":
|
||||
case "element":
|
||||
return data[rightOperand.value];
|
||||
case "variable":
|
||||
const variables = localSurvey.variables || [];
|
||||
@@ -623,16 +625,16 @@ const getRightOperandValue = (
|
||||
|
||||
export const performActions = (
|
||||
survey: TJsEnvironmentStateSurvey,
|
||||
actions: TSurveyLogicAction[],
|
||||
actions: TSurveyBlockLogicAction[] | TSurveyLogicAction[],
|
||||
data: TResponseData,
|
||||
calculationResults: TResponseVariables
|
||||
): {
|
||||
jumpTarget: string | undefined;
|
||||
requiredQuestionIds: string[];
|
||||
requiredElementIds: string[];
|
||||
calculations: TResponseVariables;
|
||||
} => {
|
||||
let jumpTarget: string | undefined;
|
||||
const requiredQuestionIds: string[] = [];
|
||||
const requiredElementIds: string[] = [];
|
||||
const calculations: TResponseVariables = { ...calculationResults };
|
||||
|
||||
actions.forEach((action) => {
|
||||
@@ -642,9 +644,9 @@ export const performActions = (
|
||||
if (result !== undefined) calculations[action.variableId] = result;
|
||||
break;
|
||||
case "requireAnswer":
|
||||
requiredQuestionIds.push(action.target);
|
||||
requiredElementIds.push(action.target);
|
||||
break;
|
||||
case "jumpToQuestion":
|
||||
case "jumpToBlock":
|
||||
if (!jumpTarget) {
|
||||
jumpTarget = action.target;
|
||||
}
|
||||
@@ -652,7 +654,7 @@ export const performActions = (
|
||||
}
|
||||
});
|
||||
|
||||
return { jumpTarget, requiredQuestionIds, calculations };
|
||||
return { jumpTarget, requiredElementIds, calculations };
|
||||
};
|
||||
|
||||
const performCalculation = (
|
||||
@@ -683,7 +685,7 @@ const performCalculation = (
|
||||
operandValue = value;
|
||||
}
|
||||
break;
|
||||
case "question":
|
||||
case "element":
|
||||
case "hiddenField":
|
||||
const val = data[action.value.value];
|
||||
if (typeof val === "number" || typeof val === "string") {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { TResponseData, TResponseVariables } from "@formbricks/types/responses";
|
||||
import { TSurvey, TSurveyQuestion, TSurveyRecallItem } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyRecallItem } from "@formbricks/types/surveys/types";
|
||||
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
||||
import {
|
||||
checkForEmptyFallBackValue,
|
||||
@@ -40,7 +40,7 @@ vi.mock("@/lib/utils/datetime", () => ({
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
formatDateWithOrdinal: vi.fn((date) => {
|
||||
formatDateWithOrdinal: vi.fn(() => {
|
||||
return "January 1st, 2023";
|
||||
}),
|
||||
}));
|
||||
@@ -145,7 +145,7 @@ describe("recall utility functions", () => {
|
||||
const headline = { en: "How do you like #recall:product/fallback:ournbspproduct#?" };
|
||||
const survey = {
|
||||
id: "test-survey",
|
||||
questions: [{ id: "product", headline: { en: "Product Question" } }],
|
||||
blocks: [{ id: "b1", elements: [{ id: "product", headline: { en: "Product Question" } }] }],
|
||||
hiddenFields: { fieldIds: [] },
|
||||
variables: [],
|
||||
} as any;
|
||||
@@ -158,7 +158,7 @@ describe("recall utility functions", () => {
|
||||
const headline = { en: "Rate #recall:product/fallback:ournbspproduct#" };
|
||||
const survey = {
|
||||
id: "test-survey",
|
||||
questions: [{ id: "product", headline: { en: "Product Question" } }],
|
||||
blocks: [{ id: "b1", elements: [{ id: "product", headline: { en: "Product Question" } }] }],
|
||||
hiddenFields: { fieldIds: [] },
|
||||
variables: [],
|
||||
} as any;
|
||||
@@ -171,7 +171,7 @@ describe("recall utility functions", () => {
|
||||
const headline = { en: "Your email is #recall:email/fallback:notnbspprovided#" };
|
||||
const survey: TSurvey = {
|
||||
id: "test-survey",
|
||||
questions: [],
|
||||
blocks: [],
|
||||
hiddenFields: { fieldIds: ["email"] },
|
||||
variables: [],
|
||||
} as unknown as TSurvey;
|
||||
@@ -184,7 +184,7 @@ describe("recall utility functions", () => {
|
||||
const headline = { en: "Your plan is #recall:plan/fallback:unknown#" };
|
||||
const survey: TSurvey = {
|
||||
id: "test-survey",
|
||||
questions: [],
|
||||
blocks: [],
|
||||
hiddenFields: { fieldIds: [] },
|
||||
variables: [{ id: "plan", name: "Subscription Plan" }],
|
||||
} as unknown as TSurvey;
|
||||
@@ -207,7 +207,7 @@ describe("recall utility functions", () => {
|
||||
};
|
||||
const survey = {
|
||||
id: "test-survey",
|
||||
questions: [{ id: "inner", headline: { en: "Inner with @outer" } }],
|
||||
blocks: [{ id: "b1", elements: [{ id: "inner", headline: { en: "Inner with @outer" } }] }],
|
||||
hiddenFields: { fieldIds: [] },
|
||||
variables: [],
|
||||
} as any;
|
||||
@@ -241,41 +241,56 @@ describe("recall utility functions", () => {
|
||||
test("identifies question with empty fallback value", () => {
|
||||
const questionHeadline = { en: "Question with #recall:id1/fallback:# empty fallback" };
|
||||
const survey = {
|
||||
questions: [
|
||||
blocks: [
|
||||
{
|
||||
id: "q1",
|
||||
headline: questionHeadline,
|
||||
id: "b1",
|
||||
elements: [
|
||||
{
|
||||
id: "q1",
|
||||
headline: questionHeadline,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
|
||||
const result = checkForEmptyFallBackValue(survey, "en");
|
||||
expect(result).toBe(survey.questions[0]);
|
||||
expect(result).toBe(survey.blocks[0].elements[0]);
|
||||
});
|
||||
|
||||
test("identifies question with empty fallback in subheader", () => {
|
||||
const questionSubheader = { en: "Subheader with #recall:id1/fallback:# empty fallback" };
|
||||
const survey = {
|
||||
questions: [
|
||||
blocks: [
|
||||
{
|
||||
id: "q1",
|
||||
headline: { en: "Normal question" },
|
||||
subheader: questionSubheader,
|
||||
id: "b1",
|
||||
elements: [
|
||||
{
|
||||
id: "q1",
|
||||
headline: { en: "Normal question" },
|
||||
subheader: questionSubheader,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
|
||||
const result = checkForEmptyFallBackValue(survey, "en");
|
||||
expect(result).toBe(survey.questions[0]);
|
||||
expect(result).toBe(survey.blocks[0].elements[0]);
|
||||
});
|
||||
|
||||
test("returns null when no empty fallback values are found", () => {
|
||||
const questionHeadline = { en: "Question with #recall:id1/fallback:default# valid fallback" };
|
||||
const survey = {
|
||||
questions: [
|
||||
blocks: [
|
||||
{
|
||||
id: "q1",
|
||||
headline: questionHeadline,
|
||||
id: "b1",
|
||||
elements: [
|
||||
{
|
||||
id: "q1",
|
||||
headline: questionHeadline,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
@@ -288,16 +303,21 @@ describe("recall utility functions", () => {
|
||||
describe("replaceHeadlineRecall", () => {
|
||||
test("processes all questions in a survey", () => {
|
||||
const survey: TSurvey = {
|
||||
questions: [
|
||||
blocks: [
|
||||
{
|
||||
id: "q1",
|
||||
headline: { en: "Question with #recall:id1/fallback:default#" },
|
||||
id: "b1",
|
||||
elements: [
|
||||
{
|
||||
id: "q1",
|
||||
headline: { en: "Question with #recall:id1/fallback:default#" },
|
||||
},
|
||||
{
|
||||
id: "q2",
|
||||
headline: { en: "Another with #recall:id2/fallback:other#" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "q2",
|
||||
headline: { en: "Another with #recall:id2/fallback:other#" },
|
||||
},
|
||||
] as unknown as TSurveyQuestion[],
|
||||
],
|
||||
hiddenFields: { fieldIds: [] },
|
||||
variables: [],
|
||||
} as unknown as TSurvey;
|
||||
@@ -308,8 +328,8 @@ describe("recall utility functions", () => {
|
||||
|
||||
// Verify recallToHeadline was called for each question
|
||||
expect(result).not.toBe(survey); // Should be a clone
|
||||
expect(result.questions[0].headline).not.toEqual(survey.questions[0].headline);
|
||||
expect(result.questions[1].headline).not.toEqual(survey.questions[1].headline);
|
||||
expect(result.blocks[0].elements[0].headline).not.toEqual(survey.blocks[0].elements[0].headline);
|
||||
expect(result.blocks[0].elements[1].headline).not.toEqual(survey.blocks[0].elements[1].headline);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -317,10 +337,15 @@ describe("recall utility functions", () => {
|
||||
test("extracts recall items from text", () => {
|
||||
const text = "Text with #recall:id1/fallback:val1# and #recall:id2/fallback:val2#";
|
||||
const survey: TSurvey = {
|
||||
questions: [
|
||||
{ id: "id1", headline: { en: "Question One" } },
|
||||
{ id: "id2", headline: { en: "Question Two" } },
|
||||
] as unknown as TSurveyQuestion[],
|
||||
blocks: [
|
||||
{
|
||||
id: "b1",
|
||||
elements: [
|
||||
{ id: "id1", headline: { en: "Question One" } },
|
||||
{ id: "id2", headline: { en: "Question Two" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
hiddenFields: { fieldIds: [] },
|
||||
variables: [],
|
||||
} as unknown as TSurvey;
|
||||
@@ -330,16 +355,16 @@ describe("recall utility functions", () => {
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].id).toBe("id1");
|
||||
expect(result[0].label).toBe("Question One");
|
||||
expect(result[0].type).toBe("question");
|
||||
expect(result[0].type).toBe("element");
|
||||
expect(result[1].id).toBe("id2");
|
||||
expect(result[1].label).toBe("Question Two");
|
||||
expect(result[1].type).toBe("question");
|
||||
expect(result[1].type).toBe("element");
|
||||
});
|
||||
|
||||
test("handles hidden fields in recall items", () => {
|
||||
const text = "Text with #recall:hidden1/fallback:val1#";
|
||||
const survey: TSurvey = {
|
||||
questions: [],
|
||||
blocks: [],
|
||||
hiddenFields: { fieldIds: ["hidden1"] },
|
||||
variables: [],
|
||||
} as unknown as TSurvey;
|
||||
@@ -354,7 +379,7 @@ describe("recall utility functions", () => {
|
||||
test("handles variables in recall items", () => {
|
||||
const text = "Text with #recall:var1/fallback:val1#";
|
||||
const survey: TSurvey = {
|
||||
questions: [],
|
||||
blocks: [],
|
||||
hiddenFields: { fieldIds: [] },
|
||||
variables: [{ id: "var1", name: "Variable One" }],
|
||||
} as unknown as TSurvey;
|
||||
@@ -397,7 +422,7 @@ describe("recall utility functions", () => {
|
||||
describe("headlineToRecall", () => {
|
||||
test("transforms headlines to recall info", () => {
|
||||
const text = "What do you think of @Product?";
|
||||
const recallItems: TSurveyRecallItem[] = [{ id: "product", label: "Product", type: "question" }];
|
||||
const recallItems: TSurveyRecallItem[] = [{ id: "product", label: "Product", type: "element" }];
|
||||
const fallbacks: fallbacks = {
|
||||
product: "our product",
|
||||
};
|
||||
@@ -409,8 +434,8 @@ describe("recall utility functions", () => {
|
||||
test("transforms multiple headlines", () => {
|
||||
const text = "Rate @Product made by @Company";
|
||||
const recallItems: TSurveyRecallItem[] = [
|
||||
{ id: "product", label: "Product", type: "question" },
|
||||
{ id: "company", label: "Company", type: "question" },
|
||||
{ id: "product", label: "Product", type: "element" },
|
||||
{ id: "company", label: "Company", type: "element" },
|
||||
];
|
||||
const fallbacks: fallbacks = {
|
||||
product: "our product",
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { type TI18nString } from "@formbricks/types/i18n";
|
||||
import { TResponseData, TResponseDataValue, TResponseVariables } from "@formbricks/types/responses";
|
||||
import { TI18nString, TSurvey, TSurveyQuestion, TSurveyRecallItem } from "@formbricks/types/surveys/types";
|
||||
import { TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey, TSurveyRecallItem } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { formatDateWithOrdinal, isValidDateString } from "./datetime";
|
||||
|
||||
export interface fallbacks {
|
||||
@@ -59,7 +62,8 @@ const getRecallItemLabel = <T extends TSurvey>(
|
||||
const isHiddenField = survey.hiddenFields.fieldIds?.includes(recallItemId);
|
||||
if (isHiddenField) return recallItemId;
|
||||
|
||||
const surveyQuestion = survey.questions.find((question) => question.id === recallItemId);
|
||||
const questions = getElementsFromBlocks(survey.blocks);
|
||||
const surveyQuestion = questions.find((question) => question.id === recallItemId);
|
||||
if (surveyQuestion) {
|
||||
const headline = getLocalizedValue(surveyQuestion.headline, languageCode);
|
||||
// Strip HTML tags to prevent raw HTML from showing in nested recalls
|
||||
@@ -122,13 +126,14 @@ export const replaceRecallInfoWithUnderline = (label: string): string => {
|
||||
};
|
||||
|
||||
// Checks for survey questions with a "recall" pattern but no fallback value.
|
||||
export const checkForEmptyFallBackValue = (survey: TSurvey, language: string): TSurveyQuestion | null => {
|
||||
export const checkForEmptyFallBackValue = (survey: TSurvey, language: string): TSurveyElement | null => {
|
||||
const doesTextHaveRecall = (text: string) => {
|
||||
const recalls = text.match(/#recall:[^ ]+/g);
|
||||
return recalls?.some((recall) => !extractFallbackValue(recall));
|
||||
};
|
||||
|
||||
for (const question of survey.questions) {
|
||||
const questions = getElementsFromBlocks(survey.blocks);
|
||||
for (const question of questions) {
|
||||
if (
|
||||
doesTextHaveRecall(getLocalizedValue(question.headline, language)) ||
|
||||
(question.subheader && doesTextHaveRecall(getLocalizedValue(question.subheader, language)))
|
||||
@@ -142,7 +147,8 @@ export const checkForEmptyFallBackValue = (survey: TSurvey, language: string): T
|
||||
// Processes each question in a survey to ensure headlines are formatted correctly for recall and return the modified survey.
|
||||
export const replaceHeadlineRecall = <T extends TSurvey>(survey: T, language: string): T => {
|
||||
const modifiedSurvey = structuredClone(survey);
|
||||
modifiedSurvey.questions.forEach((question) => {
|
||||
const questions = getElementsFromBlocks(modifiedSurvey.blocks);
|
||||
questions.forEach((question) => {
|
||||
question.headline = recallToHeadline(question.headline, modifiedSurvey, false, language);
|
||||
});
|
||||
return modifiedSurvey;
|
||||
@@ -156,14 +162,15 @@ export const getRecallItems = (text: string, survey: TSurvey, languageCode: stri
|
||||
let recallItems: TSurveyRecallItem[] = [];
|
||||
ids.forEach((recallItemId) => {
|
||||
const isHiddenField = survey.hiddenFields.fieldIds?.includes(recallItemId);
|
||||
const isSurveyQuestion = survey.questions.find((question) => question.id === recallItemId);
|
||||
const questions = getElementsFromBlocks(survey.blocks);
|
||||
const isSurveyQuestion = questions.find((question) => question.id === recallItemId);
|
||||
const isVariable = survey.variables.find((variable) => variable.id === recallItemId);
|
||||
|
||||
const recallItemLabel = getRecallItemLabel(recallItemId, survey, languageCode);
|
||||
|
||||
const getRecallItemType = () => {
|
||||
if (isHiddenField) return "hiddenField";
|
||||
if (isSurveyQuestion) return "question";
|
||||
if (isSurveyQuestion) return "element";
|
||||
if (isVariable) return "variable";
|
||||
};
|
||||
|
||||
|
||||
@@ -1,164 +1,119 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TTemplate } from "@formbricks/types/templates";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { type TProject } from "@formbricks/types/project";
|
||||
import type { TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||
import type { TTemplate } from "@formbricks/types/templates";
|
||||
import * as i18nUtils from "@/lib/i18n/utils";
|
||||
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
||||
import { replacePresetPlaceholders, replaceQuestionPresetPlaceholders } from "./templates";
|
||||
import { replaceElementPresetPlaceholders, replacePresetPlaceholders } from "./templates";
|
||||
|
||||
// Mock the imported functions
|
||||
vi.mock("@/lib/i18n/utils", () => ({
|
||||
getLocalizedValue: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/i18n/utils");
|
||||
vi.mock("@/lib/pollyfills/structuredClone");
|
||||
|
||||
vi.mock("@/lib/pollyfills/structuredClone", () => ({
|
||||
structuredClone: vi.fn((obj) => JSON.parse(JSON.stringify(obj))),
|
||||
}));
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(structuredClone).mockImplementation((obj) => JSON.parse(JSON.stringify(obj)));
|
||||
// Mock getLocalizedValue to return the value from the object
|
||||
vi.mocked(i18nUtils.getLocalizedValue).mockImplementation((obj: any, lang: string) => obj?.[lang] || "");
|
||||
});
|
||||
|
||||
describe("Template Utilities", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
describe("replaceElementPresetPlaceholders", () => {
|
||||
test("returns original element when project is not provided", () => {
|
||||
const element = {
|
||||
type: "openText",
|
||||
headline: { default: "Question about $[projectName]?" },
|
||||
} as unknown as TSurveyElement;
|
||||
|
||||
describe("replaceQuestionPresetPlaceholders", () => {
|
||||
test("returns original question when project is not provided", () => {
|
||||
const question: TSurveyQuestion = {
|
||||
id: "test-id",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: {
|
||||
default: "Test Question $[projectName]",
|
||||
},
|
||||
} as unknown as TSurveyQuestion;
|
||||
const result = replaceElementPresetPlaceholders(element, undefined as any);
|
||||
|
||||
const result = replaceQuestionPresetPlaceholders(question, undefined as unknown as TProject);
|
||||
expect(result).toEqual(element);
|
||||
});
|
||||
|
||||
expect(result).toEqual(question);
|
||||
expect(structuredClone).not.toHaveBeenCalled();
|
||||
test("replaces projectName placeholder in headline", () => {
|
||||
const element = {
|
||||
type: "openText",
|
||||
headline: { default: "How do you like $[projectName]?" },
|
||||
} as unknown as TSurveyElement;
|
||||
|
||||
const project = {
|
||||
name: "TestProject",
|
||||
} as unknown as TProject;
|
||||
|
||||
const result = replaceElementPresetPlaceholders(element, project);
|
||||
|
||||
// The function directly replaces without calling getLocalizedValue in the test scenario
|
||||
expect(result.headline?.default).toBe("How do you like TestProject?");
|
||||
});
|
||||
|
||||
test("replaces projectName placeholder in subheader", () => {
|
||||
const question: TSurveyQuestion = {
|
||||
id: "test-id",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: {
|
||||
default: "Test Question",
|
||||
},
|
||||
subheader: {
|
||||
default: "Subheader for $[projectName]",
|
||||
},
|
||||
} as unknown as TSurveyQuestion;
|
||||
const element = {
|
||||
type: "openText",
|
||||
headline: { default: "Question" },
|
||||
subheader: { default: "Subheader for $[projectName]" },
|
||||
} as unknown as TSurveyElement;
|
||||
|
||||
const project: TProject = {
|
||||
id: "project-id",
|
||||
name: "Test Project",
|
||||
organizationId: "org-id",
|
||||
const project = {
|
||||
name: "TestProject",
|
||||
} as unknown as TProject;
|
||||
|
||||
// Mock for headline and subheader with correct return values
|
||||
vi.mocked(getLocalizedValue).mockReturnValueOnce("Test Question");
|
||||
vi.mocked(getLocalizedValue).mockReturnValueOnce("Subheader for $[projectName]");
|
||||
const result = replaceElementPresetPlaceholders(element, project);
|
||||
|
||||
const result = replaceQuestionPresetPlaceholders(question, project);
|
||||
|
||||
expect(vi.mocked(getLocalizedValue)).toHaveBeenCalledTimes(2);
|
||||
expect(result.subheader?.default).toBe("Subheader for Test Project");
|
||||
expect(result.headline?.default).toBe("Question");
|
||||
expect(result.subheader?.default).toBe("Subheader for TestProject");
|
||||
});
|
||||
|
||||
test("handles missing headline and subheader", () => {
|
||||
const question: TSurveyQuestion = {
|
||||
id: "test-id",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
} as unknown as TSurveyQuestion;
|
||||
const element = {
|
||||
type: "openText",
|
||||
} as unknown as TSurveyElement;
|
||||
|
||||
const project: TProject = {
|
||||
id: "project-id",
|
||||
name: "Test Project",
|
||||
organizationId: "org-id",
|
||||
const project = {
|
||||
name: "TestProject",
|
||||
} as unknown as TProject;
|
||||
|
||||
const result = replaceQuestionPresetPlaceholders(question, project);
|
||||
const result = replaceElementPresetPlaceholders(element, project);
|
||||
|
||||
expect(structuredClone).toHaveBeenCalledWith(question);
|
||||
expect(result).toEqual(question);
|
||||
expect(getLocalizedValue).not.toHaveBeenCalled();
|
||||
expect(structuredClone).toHaveBeenCalledWith(element);
|
||||
expect(result).toEqual(element);
|
||||
});
|
||||
});
|
||||
|
||||
describe("replacePresetPlaceholders", () => {
|
||||
test("replaces projectName placeholder in template name and questions", () => {
|
||||
const template: TTemplate = {
|
||||
id: "template-1",
|
||||
name: "Test Template",
|
||||
description: "Template Description",
|
||||
test("replaces projectName placeholder in template name and blocks", () => {
|
||||
const mockTemplate = {
|
||||
name: "Template 1",
|
||||
preset: {
|
||||
name: "$[projectName] Feedback",
|
||||
questions: [
|
||||
welcomeCard: { enabled: false, timeToFinish: false, showResponseCount: false },
|
||||
blocks: [
|
||||
{
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: {
|
||||
default: "How do you like $[projectName]?",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "q2",
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: {
|
||||
default: "Another question",
|
||||
},
|
||||
subheader: {
|
||||
default: "About $[projectName]",
|
||||
},
|
||||
id: "block1",
|
||||
name: "Block 1",
|
||||
elements: [
|
||||
{
|
||||
id: "elem1",
|
||||
type: "openText",
|
||||
headline: { default: "How would you rate $[projectName]?" },
|
||||
required: true,
|
||||
inputType: "text",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
endings: [],
|
||||
hiddenFields: { enabled: true, fieldIds: [] },
|
||||
},
|
||||
category: "product",
|
||||
} as unknown as TTemplate;
|
||||
|
||||
const project = {
|
||||
name: "Awesome App",
|
||||
};
|
||||
name: "TestProject",
|
||||
} as TProject;
|
||||
|
||||
// Mock getLocalizedValue to return the original strings with placeholders
|
||||
vi.mocked(getLocalizedValue)
|
||||
.mockReturnValueOnce("How do you like $[projectName]?")
|
||||
.mockReturnValueOnce("Another question")
|
||||
.mockReturnValueOnce("About $[projectName]");
|
||||
const result = replacePresetPlaceholders(mockTemplate, project);
|
||||
|
||||
const result = replacePresetPlaceholders(template, project);
|
||||
|
||||
expect(result.preset.name).toBe("Awesome App Feedback");
|
||||
expect(structuredClone).toHaveBeenCalledWith(template.preset);
|
||||
|
||||
// Verify that replaceQuestionPresetPlaceholders was applied to both questions
|
||||
expect(vi.mocked(getLocalizedValue)).toHaveBeenCalledTimes(3);
|
||||
expect(result.preset.questions[0].headline?.default).toBe("How do you like Awesome App?");
|
||||
expect(result.preset.questions[1].subheader?.default).toBe("About Awesome App");
|
||||
});
|
||||
|
||||
test("maintains other template properties", () => {
|
||||
const template: TTemplate = {
|
||||
id: "template-1",
|
||||
name: "Test Template",
|
||||
description: "Template Description",
|
||||
preset: {
|
||||
name: "$[projectName] Feedback",
|
||||
questions: [],
|
||||
},
|
||||
category: "product",
|
||||
} as unknown as TTemplate;
|
||||
|
||||
const project = {
|
||||
name: "Awesome App",
|
||||
};
|
||||
|
||||
const result = replacePresetPlaceholders(template, project) as unknown as {
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
expect(result.name).toBe(template.name);
|
||||
expect(result.description).toBe(template.description);
|
||||
expect(structuredClone).toHaveBeenCalledWith(mockTemplate.preset);
|
||||
expect(result.preset.name).toBe("TestProject Feedback");
|
||||
expect(result.preset.blocks[0].elements[0].headline?.default).toBe("How would you rate TestProject?");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,37 +1,46 @@
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TTemplate } from "@formbricks/types/templates";
|
||||
import type { TProject } from "@formbricks/types/project";
|
||||
import type { TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||
import type { TTemplate } from "@formbricks/types/templates";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
||||
|
||||
export const replaceQuestionPresetPlaceholders = (
|
||||
question: TSurveyQuestion,
|
||||
export const replaceElementPresetPlaceholders = (
|
||||
element: TSurveyElement,
|
||||
project: TProject
|
||||
): TSurveyQuestion => {
|
||||
if (!project) return question;
|
||||
const newQuestion = structuredClone(question);
|
||||
): TSurveyElement => {
|
||||
if (!project) return element;
|
||||
const newElement = structuredClone(element);
|
||||
const defaultLanguageCode = "default";
|
||||
if (newQuestion.headline) {
|
||||
newQuestion.headline[defaultLanguageCode] = getLocalizedValue(
|
||||
newQuestion.headline,
|
||||
|
||||
if (newElement.headline) {
|
||||
newElement.headline[defaultLanguageCode] = getLocalizedValue(
|
||||
newElement.headline,
|
||||
defaultLanguageCode
|
||||
).replace("$[projectName]", project.name);
|
||||
}
|
||||
if (newQuestion.subheader) {
|
||||
newQuestion.subheader[defaultLanguageCode] = getLocalizedValue(
|
||||
newQuestion.subheader,
|
||||
|
||||
if (newElement.subheader) {
|
||||
newElement.subheader[defaultLanguageCode] = getLocalizedValue(
|
||||
newElement.subheader,
|
||||
defaultLanguageCode
|
||||
)?.replace("$[projectName]", project.name);
|
||||
}
|
||||
return newQuestion;
|
||||
|
||||
return newElement;
|
||||
};
|
||||
|
||||
// replace all occurences of projectName with the actual project name in the current template
|
||||
export const replacePresetPlaceholders = (template: TTemplate, project: any) => {
|
||||
const preset = structuredClone(template.preset);
|
||||
preset.name = preset.name.replace("$[projectName]", project.name);
|
||||
preset.questions = preset.questions.map((question) => {
|
||||
return replaceQuestionPresetPlaceholders(question, project);
|
||||
});
|
||||
|
||||
// Handle blocks if present
|
||||
if (preset.blocks && preset.blocks.length > 0) {
|
||||
preset.blocks = preset.blocks.map((block) => ({
|
||||
...block,
|
||||
elements: block.elements.map((element) => replaceElementPresetPlaceholders(element, project)),
|
||||
}));
|
||||
}
|
||||
|
||||
return { ...template, preset };
|
||||
};
|
||||
|
||||
@@ -124,3 +124,12 @@ export const convertToEmbedUrl = (url: string): string | undefined => {
|
||||
// If no supported platform found, return undefined
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates if a URL is from a supported video platform (YouTube, Vimeo, or Loom)
|
||||
* @param url - URL to validate
|
||||
* @returns true if URL is from a supported platform, false otherwise
|
||||
*/
|
||||
export const isValidVideoUrl = (url: string): boolean => {
|
||||
return checkForYoutubeUrl(url) || checkForVimeoUrl(url) || checkForLoomUrl(url);
|
||||
};
|
||||
|
||||
+25
-24
@@ -1206,9 +1206,9 @@
|
||||
"add": "+ hinzufügen",
|
||||
"add_a_delay_or_auto_close_the_survey": "Füge eine Verzögerung hinzu oder schließe die Umfrage automatisch.",
|
||||
"add_a_four_digit_pin": "Füge eine vierstellige PIN hinzu",
|
||||
"add_a_new_question_to_your_survey": "Neue Frage hinzufügen",
|
||||
"add_a_variable_to_calculate": "Variable hinzufügen",
|
||||
"add_action_below": "Aktion unten hinzufügen",
|
||||
"add_block": "Block hinzufügen",
|
||||
"add_choice_below": "Auswahl unten hinzufügen",
|
||||
"add_color_coding": "Farbkodierung hinzufügen",
|
||||
"add_color_coding_description": "Füge rote, orange und grüne Farbcodes zu den Optionen hinzu.",
|
||||
@@ -1229,8 +1229,8 @@
|
||||
"add_other": "Anderes hinzufügen",
|
||||
"add_photo_or_video": "Foto oder Video hinzufügen",
|
||||
"add_pin": "PIN hinzufügen",
|
||||
"add_question": "Frage hinzufügen",
|
||||
"add_question_below": "Frage unten hinzufügen",
|
||||
"add_question_to_block": "Frage zum Block hinzufügen",
|
||||
"add_row": "Zeile hinzufügen",
|
||||
"add_variable": "Variable hinzufügen",
|
||||
"address_fields": "Adressfelder",
|
||||
@@ -1256,12 +1256,14 @@
|
||||
"automatically_mark_the_survey_as_complete_after": "Umfrage automatisch als abgeschlossen markieren nach",
|
||||
"back_button_label": "Zurück\"- Button ",
|
||||
"background_styling": "Hintergründe",
|
||||
"block_deleted": "Block gelöscht.",
|
||||
"block_duplicated": "Block dupliziert.",
|
||||
"bold": "Fett",
|
||||
"brand_color": "Markenfarbe",
|
||||
"brightness": "Helligkeit",
|
||||
"button_external": "Externen Link aktivieren",
|
||||
"button_external_description": "Fügen Sie eine Schaltfläche hinzu, die eine externe URL in einem neuen Tab öffnet",
|
||||
"button_label": "Beschriftung",
|
||||
"button_to_continue_in_survey": "Fahre in der Umfrage fort",
|
||||
"button_to_link_to_external_url": "Verlinke auf externe URL",
|
||||
"button_url": "URL",
|
||||
"cal_username": "Cal.com Benutzername oder Benutzername/Ereignis",
|
||||
"calculate": "Berechnen",
|
||||
@@ -1300,6 +1302,7 @@
|
||||
"character_limit_toggle_title": "Fügen Sie Zeichenbeschränkungen hinzu",
|
||||
"checkbox_label": "Checkbox-Beschriftung",
|
||||
"choose_the_actions_which_trigger_the_survey": "Aktionen auswählen, die die Umfrage auslösen.",
|
||||
"choose_the_first_question_on_your_block": "Wählen sie die erste frage in ihrem block",
|
||||
"choose_where_to_run_the_survey": "Wähle, wo die Umfrage durchgeführt werden soll.",
|
||||
"city": "Stadt",
|
||||
"close_survey_on_response_limit": "Umfrage bei Erreichen des Antwortlimits schließen",
|
||||
@@ -1323,10 +1326,12 @@
|
||||
"create_group": "Gruppe erstellen",
|
||||
"create_your_own_survey": "Erstelle deine eigene Umfrage",
|
||||
"css_selector": "CSS-Selektor",
|
||||
"cta_button_label": "\"CTA\"-Schaltflächen-Beschriftung",
|
||||
"custom_hostname": "Benutzerdefinierter Hostname",
|
||||
"darken_or_lighten_background_of_your_choice": "Hintergrund deiner Wahl abdunkeln oder aufhellen.",
|
||||
"date_format": "Datumsformat",
|
||||
"days_before_showing_this_survey_again": "Tage nachdem eine beliebige Umfrage angezeigt wurde, bevor diese Umfrage erscheinen kann.",
|
||||
"delete_block": "Block löschen",
|
||||
"delete_choice": "Auswahl löschen",
|
||||
"disable_the_visibility_of_survey_progress": "Deaktiviere die Sichtbarkeit des Umfragefortschritts.",
|
||||
"display_an_estimate_of_completion_time_for_survey": "Zeige eine Schätzung der Fertigstellungszeit für die Umfrage an",
|
||||
@@ -1338,9 +1343,12 @@
|
||||
"does_not_include_all_of": "Enthält nicht alle von",
|
||||
"does_not_include_one_of": "Enthält nicht eines von",
|
||||
"does_not_start_with": "Fängt nicht an mit",
|
||||
"duplicate_block": "Block duplizieren",
|
||||
"duplicate_question": "Frage duplizieren",
|
||||
"edit_link": "Bearbeitungslink",
|
||||
"edit_recall": "Erinnerung bearbeiten",
|
||||
"edit_translations": "{lang} -Übersetzungen bearbeiten",
|
||||
"element_not_found": "Frage nicht gefunden",
|
||||
"enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Befragten erlauben, die Sprache jederzeit zu wechseln. Benötigt mind. 2 aktive Sprachen.",
|
||||
"enable_recaptcha_to_protect_your_survey_from_spam": "Spamschutz verwendet reCAPTCHA v3, um Spam-Antworten herauszufiltern.",
|
||||
"enable_spam_protection": "Spamschutz",
|
||||
@@ -1416,11 +1424,12 @@
|
||||
"hidden_field_used_in_recall": "Verstecktes Feld \"{hiddenField}\" wird in Frage {questionIndex} abgerufen.",
|
||||
"hidden_field_used_in_recall_ending_card": "Verstecktes Feld \"{hiddenField}\" wird in der Abschlusskarte abgerufen.",
|
||||
"hidden_field_used_in_recall_welcome": "Verstecktes Feld \"{hiddenField}\" wird in der Willkommenskarte abgerufen.",
|
||||
"hide_advanced_settings": "Erweiterte Einstellungen ausblenden",
|
||||
"hide_back_button": "'Zurück'-Button ausblenden",
|
||||
"hide_back_button_description": "Den Zurück-Button in der Umfrage nicht anzeigen",
|
||||
"hide_block_settings": "Block-Einstellungen ausblenden",
|
||||
"hide_logo": "Logo verstecken",
|
||||
"hide_progress_bar": "Fortschrittsbalken ausblenden",
|
||||
"hide_question_settings": "Frageeinstellungen ausblenden",
|
||||
"hide_the_logo_in_this_specific_survey": "Logo in dieser speziellen Umfrage verstecken",
|
||||
"hostname": "Hostname",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Wie funky sollen deine Karten in {surveyTypeDerived} Umfragen sein",
|
||||
@@ -1447,6 +1456,7 @@
|
||||
"is_clicked": "Wird geklickt",
|
||||
"is_completely_submitted": "Vollständig eingereicht",
|
||||
"is_empty": "Ist leer",
|
||||
"is_not_clicked": "Wird nicht geklickt",
|
||||
"is_not_empty": "Ist nicht leer",
|
||||
"is_not_set": "Ist nicht festgelegt",
|
||||
"is_partially_submitted": "Teilweise eingereicht",
|
||||
@@ -1454,7 +1464,7 @@
|
||||
"is_skipped": "Wird übersprungen",
|
||||
"is_submitted": "Wird eingereicht",
|
||||
"italic": "Kursiv",
|
||||
"jump_to_question": "Zur Frage springen",
|
||||
"jump_to_block": "Zum Block springen",
|
||||
"keep_current_order": "Bestehende Anordnung beibehalten",
|
||||
"keep_showing_while_conditions_match": "Zeige weiter, solange die Bedingungen übereinstimmen",
|
||||
"key": "Schlüssel",
|
||||
@@ -1468,16 +1478,18 @@
|
||||
"logic_error_warning": "Änderungen werden zu Logikfehlern führen",
|
||||
"logic_error_warning_text": "Das Ändern des Fragetypen entfernt die Logikbedingungen von dieser Frage",
|
||||
"long_answer": "Lange Antwort",
|
||||
"long_answer_toggle_description": "Ermöglichen Sie den Befragten, längere Antworten über mehrere Zeilen zu schreiben.",
|
||||
"lower_label": "Unteres Label",
|
||||
"manage_languages": "Sprachen verwalten",
|
||||
"matrix_all_fields": "Alle Felder",
|
||||
"matrix_rows": "Zeilen",
|
||||
"max_file_size": "Max. Dateigröße",
|
||||
"max_file_size_limit_is": "Max. Dateigröße ist",
|
||||
"move_question_to_block": "Frage in Block verschieben",
|
||||
"multiply": "Multiplizieren *",
|
||||
"needed_for_self_hosted_cal_com_instance": "Benötigt für eine selbstgehostete Cal.com-Instanz",
|
||||
"next_button_label": "Weiter",
|
||||
"next_question": "Nächste Frage",
|
||||
"next_block": "Nächster Block",
|
||||
"next_button_label": "Beschriftung der Schaltfläche \"Weiter\"",
|
||||
"no_hidden_fields_yet_add_first_one_below": "Noch keine versteckten Felder. Füge das erste unten hinzu.",
|
||||
"no_images_found_for": "Keine Bilder gefunden für ''{query}\"",
|
||||
"no_languages_found_add_first_one_to_get_started": "Keine Sprachen gefunden. Füge die erste hinzu, um loszulegen.",
|
||||
@@ -1589,17 +1601,17 @@
|
||||
"set_the_global_placement_in_the_look_feel_settings": "Stelle die globale Platzierung in den Look & Feel-Einstellungen ein.",
|
||||
"settings_saved_successfully": "Einstellungen erfolgreich gespeichert",
|
||||
"seven_points": "7 Punkte",
|
||||
"show_advanced_settings": "Erweiterte Einstellungen anzeigen",
|
||||
"show_block_settings": "Block-Einstellungen anzeigen",
|
||||
"show_button": "Button anzeigen",
|
||||
"show_language_switch": "Sprachwechsel anzeigen",
|
||||
"show_multiple_times": "Begrenzte Anzahl von Malen anzeigen",
|
||||
"show_only_once": "Nur einmal anzeigen",
|
||||
"show_question_settings": "Frageeinstellungen anzeigen",
|
||||
"show_survey_maximum_of": "Umfrage maximal anzeigen von",
|
||||
"show_survey_to_users": "Umfrage % der Nutzer anzeigen",
|
||||
"show_to_x_percentage_of_targeted_users": "Zeige {percentage}% der Zielbenutzer",
|
||||
"simple": "Einfach",
|
||||
"six_points": "6 Punkte",
|
||||
"skip_button_label": "Überspringen-Button-Beschriftung",
|
||||
"smiley": "Smiley",
|
||||
"spam_protection_note": "Spamschutz funktioniert nicht für Umfragen, die mit den iOS-, React Native- und Android-SDKs angezeigt werden. Es wird die Umfrage unterbrechen.",
|
||||
"spam_protection_threshold_description": "Wert zwischen 0 und 1 festlegen, Antworten unter diesem Wert werden abgelehnt.",
|
||||
@@ -1620,6 +1632,7 @@
|
||||
"survey_placement": "Platzierung der Umfrage",
|
||||
"survey_trigger": "Auslöser der Umfrage",
|
||||
"switch_multi_lanugage_on_to_get_started": "Schalte Mehrsprachigkeit ein, um loszulegen 👉",
|
||||
"target_block_not_found": "Zielblock nicht gefunden",
|
||||
"targeted": "Gezielt",
|
||||
"ten_points": "10 Punkte",
|
||||
"the_survey_will_be_shown_multiple_times_until_they_respond": "Höchstens die angegebene Anzahl von Malen anzeigen oder bis sie antworten (je nachdem, was zuerst eintritt).",
|
||||
@@ -1639,6 +1652,7 @@
|
||||
"unlock_targeting_title": "Targeting mit einem höheren Plan freischalten",
|
||||
"unsaved_changes_warning": "Du hast ungespeicherte Änderungen in deiner Umfrage. Möchtest Du sie speichern, bevor Du gehst?",
|
||||
"until_they_submit_a_response": "Fragen, bis sie eine Antwort abgeben",
|
||||
"untitled_block": "Unbenannter Block",
|
||||
"upgrade_notice_description": "Erstelle mehrsprachige Umfragen und entdecke viele weitere Funktionen",
|
||||
"upgrade_notice_title": "Schalte mehrsprachige Umfragen mit einem höheren Plan frei",
|
||||
"upload": "Hochladen",
|
||||
@@ -2070,7 +2084,6 @@
|
||||
"card_abandonment_survey": "Umfrage zum Warenkorbabbruch",
|
||||
"card_abandonment_survey_description": "Verstehe die Gründe für Warenkorbabbrüche in deinem Webshop.",
|
||||
"card_abandonment_survey_question_1_button_label": "Klar!",
|
||||
"card_abandonment_survey_question_1_dismiss_button_label": "Nein, danke.",
|
||||
"card_abandonment_survey_question_1_headline": "Haben Sie 2 Minuten Zeit, um uns bei der Verbesserung zu helfen?",
|
||||
"card_abandonment_survey_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Wir haben bemerkt, dass Du einige Artikel in deinem Warenkorb gelassen hast. Wir würden gerne verstehen, warum.</span></p>",
|
||||
"card_abandonment_survey_question_2_choice_1": "Hohe Versandkosten",
|
||||
@@ -2157,12 +2170,10 @@
|
||||
"churn_survey_question_2_button_label": "Senden",
|
||||
"churn_survey_question_2_headline": "Was hätte $[projectName] benutzerfreundlicher gemacht?",
|
||||
"churn_survey_question_3_button_label": "Erhalte 30% Rabatt",
|
||||
"churn_survey_question_3_dismiss_button_label": "Überspringen",
|
||||
"churn_survey_question_3_headline": "Erhalte 30% Rabatt für das nächste Jahr!",
|
||||
"churn_survey_question_3_html": "Wir würden Dich gerne als Kunden behalten! Gerne bieten wir dir einen 30% Rabatt für das nächste Jahr an.",
|
||||
"churn_survey_question_4_headline": "Welche Funktionen vermisst du?",
|
||||
"churn_survey_question_5_button_label": "E-Mail an den CEO senden",
|
||||
"churn_survey_question_5_dismiss_button_label": "Überspringen",
|
||||
"churn_survey_question_5_headline": "Es tut mir leid zu hören 😔 Sprich direkt mit unserem CEO!",
|
||||
"churn_survey_question_5_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Wir möchten den bestmöglichen Kundenservice bieten. Bitte sende eine E-Mail an unsere Geschäftsführerin, und sie wird sich persönlich um dein Anliegen kümmern.</span></p>",
|
||||
"collect_feedback_description": "Sammle umfassendes Feedback zu deinem Produkt oder deiner Dienstleistung.",
|
||||
@@ -2269,6 +2280,7 @@
|
||||
"csat_survey_question_3_headline": "Ugh, sorry! Können wir irgendwas tun, um deine Erfahrung zu verbessern?",
|
||||
"csat_survey_question_3_placeholder": "Tippe deine Antwort hier...",
|
||||
"cta_description": "Information anzeigen und Benutzer auffordern, eine bestimmte Aktion auszuführen",
|
||||
"custom_survey_block_1_name": "Block 1",
|
||||
"custom_survey_description": "Erstelle eine Umfrage ohne Vorlage.",
|
||||
"custom_survey_name": "Eigene Umfrage erstellen",
|
||||
"custom_survey_question_1_headline": "Was möchtest Du wissen?",
|
||||
@@ -2355,7 +2367,6 @@
|
||||
"evaluate_a_product_idea_description": "Befrage Nutzer zu Produkt- oder Feature-Ideen. Erhalte schnell Feedback.",
|
||||
"evaluate_a_product_idea_name": "Ein Produktidee bewerten",
|
||||
"evaluate_a_product_idea_question_1_button_label": "Los geht's!",
|
||||
"evaluate_a_product_idea_question_1_dismiss_button_label": "Überspringen",
|
||||
"evaluate_a_product_idea_question_1_headline": "Uns gefällt, wie Du $[projectName] benutzt! Wir würden gerne deine Meinung zu einer Feature-Idee hören. Hast Du eine Minute?",
|
||||
"evaluate_a_product_idea_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Wir respektieren deine Zeit und haben es kurz gehalten 🤸</span></p>",
|
||||
"evaluate_a_product_idea_question_2_headline": "Danke! Wie schwierig oder einfach ist es für Dich heute, [PROBLEM AREA] zu [erledigen]?",
|
||||
@@ -2364,7 +2375,6 @@
|
||||
"evaluate_a_product_idea_question_3_headline": "Was fällt dir am schwersten, wenn es um [PROBLEM BEREICH] geht?",
|
||||
"evaluate_a_product_idea_question_3_placeholder": "Tippe deine Antwort hier...",
|
||||
"evaluate_a_product_idea_question_4_button_label": "Weiter",
|
||||
"evaluate_a_product_idea_question_4_dismiss_button_label": "Überspringen",
|
||||
"evaluate_a_product_idea_question_4_headline": "Wir arbeiten an einer Idee, um bei [PROBLEM BEREICH] zu helfen.",
|
||||
"evaluate_a_product_idea_question_4_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Füge hier die Konzeptbeschreibung ein. Füge notwendige Details hinzu, aber halte es kurz und leicht verständlich.</span></p>",
|
||||
"evaluate_a_product_idea_question_5_headline": "Wie wertvoll wäre diese Funktion für dich?",
|
||||
@@ -2414,7 +2424,6 @@
|
||||
"feedback_box_question_2_headline": "Was ist kaputt?",
|
||||
"feedback_box_question_2_subheader": "Je mehr Details, desto besser :)",
|
||||
"feedback_box_question_3_button_label": "Ja, benachrichtige mich",
|
||||
"feedback_box_question_3_dismiss_button_label": "Nein, danke",
|
||||
"feedback_box_question_3_headline": "Möchtest Du auf dem Laufenden bleiben?",
|
||||
"feedback_box_question_3_html": "Wir werden das so schnell wie möglich beheben. Möchtest Du benachrichtigt werden, wenn wir es getan haben?",
|
||||
"feedback_box_question_4_button_label": "Feature anfordern",
|
||||
@@ -2439,7 +2448,6 @@
|
||||
"identify_sign_up_barriers_description": "Biete einen Rabatt an, um Einblicke in Anmeldebarrieren zu gewinnen.",
|
||||
"identify_sign_up_barriers_name": "Identifiziere Anmeldebarrieren",
|
||||
"identify_sign_up_barriers_question_1_button_label": "Erhalte 10% Rabatt",
|
||||
"identify_sign_up_barriers_question_1_dismiss_button_label": "Nein, danke",
|
||||
"identify_sign_up_barriers_question_1_headline": "Beantworte diese kurze Umfrage, erhalte 10% Rabatt!",
|
||||
"identify_sign_up_barriers_question_1_html": "Du scheinst darüber nachzudenken, Dich anzumelden. Beantworte vier Fragen und erhalte 10% Rabatt auf jeden Plan.",
|
||||
"identify_sign_up_barriers_question_2_headline": "Wie wahrscheinlich ist es, dass Du Dich für $[projectName] anmeldest?",
|
||||
@@ -2462,7 +2470,6 @@
|
||||
"identify_sign_up_barriers_question_8_headline": "Bitte erzähle uns mehr:",
|
||||
"identify_sign_up_barriers_question_8_placeholder": "Tippe deine Antwort hier...",
|
||||
"identify_sign_up_barriers_question_9_button_label": "Registrieren",
|
||||
"identify_sign_up_barriers_question_9_dismiss_button_label": "Erstmal überspringen",
|
||||
"identify_sign_up_barriers_question_9_headline": "Danke! Hier ist dein Code: SIGNUPNOW10",
|
||||
"identify_sign_up_barriers_question_9_html": "Vielen Dank, dass Du dir die Zeit genommen hast, Feedback zu geben 🙏",
|
||||
"identify_upsell_opportunities_description": "Finde heraus, wie viel Zeit dein Produkt deinem Nutzer spart. Nutze dies, um mehr zu verkaufen.",
|
||||
@@ -2499,7 +2506,6 @@
|
||||
"improve_newsletter_content_question_2_headline": "Was hätte den Newsletter dieser Woche hilfreicher gemacht?",
|
||||
"improve_newsletter_content_question_2_placeholder": "Tippe deine Antwort hier...",
|
||||
"improve_newsletter_content_question_3_button_label": "Freut mich, dir zu helfen!",
|
||||
"improve_newsletter_content_question_3_dismiss_button_label": "Finde deine eigenen Freunde",
|
||||
"improve_newsletter_content_question_3_headline": "Danke! ❤️ Teile den Newsletter mit einer Person, die Dir wichtig ist.",
|
||||
"improve_newsletter_content_question_3_html": "Wer denkt wie du? Du würdest uns einen riesigen Gefallen tun, wenn Du diese Episode teilen würdest!",
|
||||
"improve_trial_conversion_description": "Finde heraus, warum Leute ihre Testphase abgebrochen haben. Diese Erkenntnisse helfen dir, deinen Funnel zu verbessern.",
|
||||
@@ -2514,7 +2520,6 @@
|
||||
"improve_trial_conversion_question_2_button_label": "Weiter",
|
||||
"improve_trial_conversion_question_2_headline": "Das tut mir leid zu hören. Was war das größte Problem bei der Nutzung von $[projectName]?",
|
||||
"improve_trial_conversion_question_4_button_label": "Erhalte 20% Rabatt",
|
||||
"improve_trial_conversion_question_4_dismiss_button_label": "Überspringen",
|
||||
"improve_trial_conversion_question_4_headline": "Das tut mir leid zu hören! Erhalte 20% Rabatt im ersten Jahr.",
|
||||
"improve_trial_conversion_question_4_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Wir freuen uns, dir einen 20% Rabatt auf einen Jahresplan anzubieten.</span></p>",
|
||||
"improve_trial_conversion_question_5_button_label": "Weiter",
|
||||
@@ -2678,7 +2683,6 @@
|
||||
"preview_survey_question_2_choice_2_label": "Nein, danke!",
|
||||
"preview_survey_question_2_headline": "Möchtest Du auf dem Laufenden bleiben?",
|
||||
"preview_survey_welcome_card_headline": "Willkommen!",
|
||||
"preview_survey_welcome_card_html": "Danke für dein Feedback - los geht's!",
|
||||
"prioritize_features_description": "Identifiziere die Funktionen, die deine Nutzer am meisten und am wenigsten brauchen.",
|
||||
"prioritize_features_name": "Funktionen priorisieren",
|
||||
"prioritize_features_question_1_choice_1": "Funktion 1",
|
||||
@@ -2704,7 +2708,6 @@
|
||||
"product_market_fit_superhuman": "Product-Market-Fit (Lang)",
|
||||
"product_market_fit_superhuman_description": "Miss den Product-Market-Fit, indem Du bewertest, wie enttäuscht die Nutzer wären, wenn es dein Produkt nicht mehr gäbe.",
|
||||
"product_market_fit_superhuman_question_1_button_label": "Freut mich, dir zu helfen!",
|
||||
"product_market_fit_superhuman_question_1_dismiss_button_label": "Nein, danke.",
|
||||
"product_market_fit_superhuman_question_1_headline": "Du bist einer unserer Power-User! Hast Du 5 Minuten?",
|
||||
"product_market_fit_superhuman_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Wir würden gerne besser verstehen, wie Dir $[projectName] gefällt! Deine Meinung hilft uns sehr!</span></p>",
|
||||
"product_market_fit_superhuman_question_2_choice_1": "Überhaupt nicht enttäuscht",
|
||||
@@ -2810,7 +2813,6 @@
|
||||
"site_abandonment_survey_description": "Verstehe die Gründe für den Kaufabbruch in deinem Webshop.",
|
||||
"site_abandonment_survey_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Wir haben bemerkt, dass Du unsere Seite verlässt, ohne einen Kauf zu tätigen. Wir würden gerne verstehen, warum.</span></p>",
|
||||
"site_abandonment_survey_question_2_button_label": "Klar!",
|
||||
"site_abandonment_survey_question_2_dismiss_button_label": "Nein, danke.",
|
||||
"site_abandonment_survey_question_2_headline": "Hast Du eine Minute?",
|
||||
"site_abandonment_survey_question_3_choice_1": "Konnte nicht finden, wonach ich suche",
|
||||
"site_abandonment_survey_question_3_choice_2": "Eine bessere Seite gefunden",
|
||||
@@ -2835,7 +2837,6 @@
|
||||
"site_abandonment_survey_question_7_label": "Ja, bitte melde dich.",
|
||||
"site_abandonment_survey_question_8_headline": "Bitte teile deine E-Mail-Adresse:",
|
||||
"site_abandonment_survey_question_9_headline": "Weitere Kommentare oder Vorschläge?",
|
||||
"skip": "Überspringen",
|
||||
"smileys_survey_name": "Smileys-Umfrage",
|
||||
"smileys_survey_question_1_headline": "Wie gefällt dir $[projectName]?",
|
||||
"smileys_survey_question_1_lower_label": "Nicht gut",
|
||||
|
||||
+24
-23
@@ -1206,9 +1206,9 @@
|
||||
"add": "Add +",
|
||||
"add_a_delay_or_auto_close_the_survey": "Add a delay or auto-close the survey",
|
||||
"add_a_four_digit_pin": "Add a four digit PIN",
|
||||
"add_a_new_question_to_your_survey": "Add a new question to your survey",
|
||||
"add_a_variable_to_calculate": "Add a variable to calculate",
|
||||
"add_action_below": "Add action below",
|
||||
"add_block": "Add Block",
|
||||
"add_choice_below": "Add choice below",
|
||||
"add_color_coding": "Add color coding",
|
||||
"add_color_coding_description": "Add red, orange and green color codes to the options.",
|
||||
@@ -1229,8 +1229,8 @@
|
||||
"add_other": "Add \"Other\"",
|
||||
"add_photo_or_video": "Add photo or video",
|
||||
"add_pin": "Add PIN",
|
||||
"add_question": "Add question",
|
||||
"add_question_below": "Add question below",
|
||||
"add_question_to_block": "Add question to block",
|
||||
"add_row": "Add row",
|
||||
"add_variable": "Add variable",
|
||||
"address_fields": "Address Fields",
|
||||
@@ -1256,12 +1256,14 @@
|
||||
"automatically_mark_the_survey_as_complete_after": "Automatically mark the survey as complete after",
|
||||
"back_button_label": "\"Back\" Button Label",
|
||||
"background_styling": "Background Styling",
|
||||
"block_deleted": "Block deleted.",
|
||||
"block_duplicated": "Block duplicated.",
|
||||
"bold": "Bold",
|
||||
"brand_color": "Brand color",
|
||||
"brightness": "Brightness",
|
||||
"button_external": "Enable External Link",
|
||||
"button_external_description": "Add a button that opens an external URL in a new tab",
|
||||
"button_label": "Button Label",
|
||||
"button_to_continue_in_survey": "Button to continue in survey",
|
||||
"button_to_link_to_external_url": "Button to link to external URL",
|
||||
"button_url": "Button URL",
|
||||
"cal_username": "Cal.com username or username/event",
|
||||
"calculate": "Calculate",
|
||||
@@ -1300,6 +1302,7 @@
|
||||
"character_limit_toggle_title": "Add character limits",
|
||||
"checkbox_label": "Checkbox Label",
|
||||
"choose_the_actions_which_trigger_the_survey": "Choose the actions which trigger the survey.",
|
||||
"choose_the_first_question_on_your_block": "Choose the first question on your Block",
|
||||
"choose_where_to_run_the_survey": "Choose where to run the survey.",
|
||||
"city": "City",
|
||||
"close_survey_on_response_limit": "Close survey on response limit",
|
||||
@@ -1323,10 +1326,12 @@
|
||||
"create_group": "Create group",
|
||||
"create_your_own_survey": "Create your own survey",
|
||||
"css_selector": "CSS Selector",
|
||||
"cta_button_label": "\"CTA\" button label",
|
||||
"custom_hostname": "Custom hostname",
|
||||
"darken_or_lighten_background_of_your_choice": "Darken or lighten background of your choice.",
|
||||
"date_format": "Date format",
|
||||
"days_before_showing_this_survey_again": "days after any survey is shown before this survey can appear.",
|
||||
"delete_block": "Delete block",
|
||||
"delete_choice": "Delete choice",
|
||||
"disable_the_visibility_of_survey_progress": "Disable the visibility of survey progress.",
|
||||
"display_an_estimate_of_completion_time_for_survey": "Display an estimate of completion time for survey",
|
||||
@@ -1338,9 +1343,12 @@
|
||||
"does_not_include_all_of": "Does not include all of",
|
||||
"does_not_include_one_of": "Does not include one of",
|
||||
"does_not_start_with": "Does not start with",
|
||||
"duplicate_block": "Duplicate block",
|
||||
"duplicate_question": "Duplicate question",
|
||||
"edit_link": "Edit link",
|
||||
"edit_recall": "Edit Recall",
|
||||
"edit_translations": "Edit {lang} translations",
|
||||
"element_not_found": "Question not found",
|
||||
"enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Allow respondents to switch language at any time. Needs min. 2 active languages.",
|
||||
"enable_recaptcha_to_protect_your_survey_from_spam": "Spam protection uses reCAPTCHA v3 to filter out the spam responses.",
|
||||
"enable_spam_protection": "Spam protection",
|
||||
@@ -1416,11 +1424,12 @@
|
||||
"hidden_field_used_in_recall": "Hidden field \"{hiddenField}\" is being recalled in question {questionIndex}.",
|
||||
"hidden_field_used_in_recall_ending_card": "Hidden field \"{hiddenField}\" is being recalled in Ending Card",
|
||||
"hidden_field_used_in_recall_welcome": "Hidden field \"{hiddenField}\" is being recalled in Welcome card.",
|
||||
"hide_advanced_settings": "Hide advanced settings",
|
||||
"hide_back_button": "Hide 'Back' button",
|
||||
"hide_back_button_description": "Do not display the back button in the survey",
|
||||
"hide_block_settings": "Hide Block settings",
|
||||
"hide_logo": "Hide logo",
|
||||
"hide_progress_bar": "Hide progress bar",
|
||||
"hide_question_settings": "Hide Question settings",
|
||||
"hide_the_logo_in_this_specific_survey": "Hide the logo in this specific survey",
|
||||
"hostname": "Hostname",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "How funky do you want your cards in {surveyTypeDerived} Surveys",
|
||||
@@ -1447,6 +1456,7 @@
|
||||
"is_clicked": "Is clicked",
|
||||
"is_completely_submitted": "Is completely submitted",
|
||||
"is_empty": "Is empty",
|
||||
"is_not_clicked": "Is not clicked",
|
||||
"is_not_empty": "Is not empty",
|
||||
"is_not_set": "Is not set",
|
||||
"is_partially_submitted": "Is partially submitted",
|
||||
@@ -1454,7 +1464,7 @@
|
||||
"is_skipped": "Is skipped",
|
||||
"is_submitted": "Is submitted",
|
||||
"italic": "Italic",
|
||||
"jump_to_question": "Jump to question",
|
||||
"jump_to_block": "Jump to block",
|
||||
"keep_current_order": "Keep current order",
|
||||
"keep_showing_while_conditions_match": "Keep showing while conditions match",
|
||||
"key": "Key",
|
||||
@@ -1468,16 +1478,18 @@
|
||||
"logic_error_warning": "Changing will cause logic errors",
|
||||
"logic_error_warning_text": "Changing the question type will remove the logic conditions from this question",
|
||||
"long_answer": "Long answer",
|
||||
"long_answer_toggle_description": "Allow respondents to write longer, multi-line answers.",
|
||||
"lower_label": "Lower Label",
|
||||
"manage_languages": "Manage Languages",
|
||||
"matrix_all_fields": "All fields",
|
||||
"matrix_rows": "Rows",
|
||||
"max_file_size": "Max file size",
|
||||
"max_file_size_limit_is": "Max file size limit is",
|
||||
"move_question_to_block": "Move question to block",
|
||||
"multiply": "Multiply *",
|
||||
"needed_for_self_hosted_cal_com_instance": "Needed for a self-hosted Cal.com instance",
|
||||
"next_block": "Next block",
|
||||
"next_button_label": "\"Next\" button label",
|
||||
"next_question": "Next question",
|
||||
"no_hidden_fields_yet_add_first_one_below": "No hidden fields yet. Add the first one below.",
|
||||
"no_images_found_for": "No images found for ''{query}\"",
|
||||
"no_languages_found_add_first_one_to_get_started": "No languages found. Add the first one to get started.",
|
||||
@@ -1589,17 +1601,17 @@
|
||||
"set_the_global_placement_in_the_look_feel_settings": "Set the global placement in the Look & Feel settings.",
|
||||
"settings_saved_successfully": "Settings saved successfully.",
|
||||
"seven_points": "7 points",
|
||||
"show_advanced_settings": "Show Advanced settings",
|
||||
"show_block_settings": "Show Block settings",
|
||||
"show_button": "Show Button",
|
||||
"show_language_switch": "Show language switch",
|
||||
"show_multiple_times": "Show a limited number of times",
|
||||
"show_only_once": "Show only once",
|
||||
"show_question_settings": "Show Question settings",
|
||||
"show_survey_maximum_of": "Show survey maximum of",
|
||||
"show_survey_to_users": "Show survey to % of users",
|
||||
"show_to_x_percentage_of_targeted_users": "Show to {percentage}% of targeted users",
|
||||
"simple": "Simple",
|
||||
"six_points": "6 points",
|
||||
"skip_button_label": "Skip Button Label",
|
||||
"smiley": "Smiley",
|
||||
"spam_protection_note": "Spam protection does not work for surveys displayed with the iOS, React Native, and Android SDKs. It will break the survey.",
|
||||
"spam_protection_threshold_description": "Set value between 0 and 1, responses below this value will be rejected.",
|
||||
@@ -1620,6 +1632,7 @@
|
||||
"survey_placement": "Survey Placement",
|
||||
"survey_trigger": "Survey Trigger",
|
||||
"switch_multi_lanugage_on_to_get_started": "Switch multi-lanugage on to get started \uD83D\uDC49",
|
||||
"target_block_not_found": "Target block not found",
|
||||
"targeted": "Targeted",
|
||||
"ten_points": "10 points",
|
||||
"the_survey_will_be_shown_multiple_times_until_they_respond": "Show at most the specified number of times, or until they respond (whichever comes first).",
|
||||
@@ -1639,6 +1652,7 @@
|
||||
"unlock_targeting_title": "Unlock targeting with a higher plan",
|
||||
"unsaved_changes_warning": "You have unsaved changes in your survey. Would you like to save them before leaving?",
|
||||
"until_they_submit_a_response": "Ask until they submit a response",
|
||||
"untitled_block": "Untitled Block",
|
||||
"upgrade_notice_description": "Create multilingual surveys and unlock many more features",
|
||||
"upgrade_notice_title": "Unlock multi-language surveys with a higher plan",
|
||||
"upload": "Upload",
|
||||
@@ -2070,7 +2084,6 @@
|
||||
"card_abandonment_survey": "Cart Abandonment Survey",
|
||||
"card_abandonment_survey_description": "Understand the reasons behind cart abandonment in your web shop.",
|
||||
"card_abandonment_survey_question_1_button_label": "Sure!",
|
||||
"card_abandonment_survey_question_1_dismiss_button_label": "No, thanks.",
|
||||
"card_abandonment_survey_question_1_headline": "Do you have 2 minutes to help us improve?",
|
||||
"card_abandonment_survey_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We noticed you left some items in your cart. We would love to understand why.</span></p>",
|
||||
"card_abandonment_survey_question_2_choice_1": "High shipping costs",
|
||||
@@ -2157,12 +2170,10 @@
|
||||
"churn_survey_question_2_button_label": "Send",
|
||||
"churn_survey_question_2_headline": "What would have made $[projectName] easier to use?",
|
||||
"churn_survey_question_3_button_label": "Get 30% off",
|
||||
"churn_survey_question_3_dismiss_button_label": "Skip",
|
||||
"churn_survey_question_3_headline": "Get 30% off for the next year!",
|
||||
"churn_survey_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We'd love to keep you as a customer. Happy to offer a 30% discount for the next year.</span></p>",
|
||||
"churn_survey_question_4_headline": "What features are you missing?",
|
||||
"churn_survey_question_5_button_label": "Send email to CEO",
|
||||
"churn_survey_question_5_dismiss_button_label": "Skip",
|
||||
"churn_survey_question_5_headline": "So sorry to hear \uD83D\uDE14 Talk to our CEO directly!",
|
||||
"churn_survey_question_5_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We aim to provide the best possible customer service. Please email our CEO and she will personally handle your issue.</span></p>",
|
||||
"collect_feedback_description": "Gather comprehensive feedback on your product or service.",
|
||||
@@ -2269,6 +2280,7 @@
|
||||
"csat_survey_question_3_headline": "Ugh, sorry! Is there anything we can do to improve your experience?",
|
||||
"csat_survey_question_3_placeholder": "Type your answer here...",
|
||||
"cta_description": "Display information and prompt users to take a specific action",
|
||||
"custom_survey_block_1_name": "Block 1",
|
||||
"custom_survey_description": "Create a survey without template.",
|
||||
"custom_survey_name": "Start from scratch",
|
||||
"custom_survey_question_1_headline": "What would you like to know?",
|
||||
@@ -2355,7 +2367,6 @@
|
||||
"evaluate_a_product_idea_description": "Survey users about product or feature ideas. Get feedback rapidly.",
|
||||
"evaluate_a_product_idea_name": "Evaluate a Product Idea",
|
||||
"evaluate_a_product_idea_question_1_button_label": "Let's do it!",
|
||||
"evaluate_a_product_idea_question_1_dismiss_button_label": "Skip",
|
||||
"evaluate_a_product_idea_question_1_headline": "We love how you use $[projectName]! We'd love to pick your brain on a feature idea. Got a minute?",
|
||||
"evaluate_a_product_idea_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We respect your time and kept it short \uD83E\uDD38</span></p>",
|
||||
"evaluate_a_product_idea_question_2_headline": "Thanks! How difficult or easy is it for you to [PROBLEM AREA] today?",
|
||||
@@ -2364,7 +2375,6 @@
|
||||
"evaluate_a_product_idea_question_3_headline": "What's most difficult for you when it comes to [PROBLEM AREA]?",
|
||||
"evaluate_a_product_idea_question_3_placeholder": "Type your answer here...",
|
||||
"evaluate_a_product_idea_question_4_button_label": "Next",
|
||||
"evaluate_a_product_idea_question_4_dismiss_button_label": "Skip",
|
||||
"evaluate_a_product_idea_question_4_headline": "We're working on an idea to help with [PROBLEM AREA].",
|
||||
"evaluate_a_product_idea_question_4_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Insert concept brief here. Add necessary details but keep it concise and easy to understand.</span></p>",
|
||||
"evaluate_a_product_idea_question_5_headline": "How valuable would this feature be to you?",
|
||||
@@ -2414,7 +2424,6 @@
|
||||
"feedback_box_question_2_headline": "What's broken?",
|
||||
"feedback_box_question_2_subheader": "The more detail, the better :)",
|
||||
"feedback_box_question_3_button_label": "Yes, notify me",
|
||||
"feedback_box_question_3_dismiss_button_label": "No, thanks",
|
||||
"feedback_box_question_3_headline": "Want to stay in the loop?",
|
||||
"feedback_box_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We will fix this as soon as possible. Do you want to be notified when we did?</span></p>",
|
||||
"feedback_box_question_4_button_label": "Request feature",
|
||||
@@ -2439,7 +2448,6 @@
|
||||
"identify_sign_up_barriers_description": "Offer a discount to gather insights about sign up barriers.",
|
||||
"identify_sign_up_barriers_name": "Identify Sign Up Barriers",
|
||||
"identify_sign_up_barriers_question_1_button_label": "Get 10% discount",
|
||||
"identify_sign_up_barriers_question_1_dismiss_button_label": "No, thanks",
|
||||
"identify_sign_up_barriers_question_1_headline": "Answer this short survey, get 10% off!",
|
||||
"identify_sign_up_barriers_question_1_html": "You seem to be considering signing up. Answer four questions and get 10% on any plan.",
|
||||
"identify_sign_up_barriers_question_2_headline": "How likely are you to sign up for $[projectName]?",
|
||||
@@ -2462,7 +2470,6 @@
|
||||
"identify_sign_up_barriers_question_8_headline": "Please explain:",
|
||||
"identify_sign_up_barriers_question_8_placeholder": "Type your answer here...",
|
||||
"identify_sign_up_barriers_question_9_button_label": "Sign Up",
|
||||
"identify_sign_up_barriers_question_9_dismiss_button_label": "Skip for now",
|
||||
"identify_sign_up_barriers_question_9_headline": "Thanks! Here is your code: SIGNUPNOW10",
|
||||
"identify_sign_up_barriers_question_9_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Thanks a lot for taking the time to share feedback \uD83D\uDE4F</span></p>",
|
||||
"identify_upsell_opportunities_description": "Find out how much time your product saves your user. Use it to upsell.",
|
||||
@@ -2499,7 +2506,6 @@
|
||||
"improve_newsletter_content_question_2_headline": "What would have made this weeks newsletter more helpful?",
|
||||
"improve_newsletter_content_question_2_placeholder": "Type your answer here...",
|
||||
"improve_newsletter_content_question_3_button_label": "Happy to help!",
|
||||
"improve_newsletter_content_question_3_dismiss_button_label": "Find your own friends",
|
||||
"improve_newsletter_content_question_3_headline": "Thanks! ❤️ Spread the love with ONE friend.",
|
||||
"improve_newsletter_content_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Who thinks like you? You'd do us a huge favor if you'd share this weeks episode with your brain friend!</span></p>",
|
||||
"improve_trial_conversion_description": "Find out why people stopped their trial. These insights help you improve your funnel.",
|
||||
@@ -2514,7 +2520,6 @@
|
||||
"improve_trial_conversion_question_2_button_label": "Next",
|
||||
"improve_trial_conversion_question_2_headline": "Sorry to hear. What was the biggest problem using $[projectName]?",
|
||||
"improve_trial_conversion_question_4_button_label": "Get 20% off",
|
||||
"improve_trial_conversion_question_4_dismiss_button_label": "Skip",
|
||||
"improve_trial_conversion_question_4_headline": "Sorry to hear! Get 20% off the first year.",
|
||||
"improve_trial_conversion_question_4_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We're happy to offer you a 20% discount on a yearly plan.</span></p>",
|
||||
"improve_trial_conversion_question_5_button_label": "Next",
|
||||
@@ -2678,7 +2683,6 @@
|
||||
"preview_survey_question_2_choice_2_label": "No, thank you!",
|
||||
"preview_survey_question_2_headline": "Want to stay in the loop?",
|
||||
"preview_survey_welcome_card_headline": "Welcome!",
|
||||
"preview_survey_welcome_card_html": "Thanks for providing your feedback - let's go!",
|
||||
"prioritize_features_description": "Identify features your users need most and least.",
|
||||
"prioritize_features_name": "Prioritize Features",
|
||||
"prioritize_features_question_1_choice_1": "Feature 1",
|
||||
@@ -2704,7 +2708,6 @@
|
||||
"product_market_fit_superhuman": "Product Market Fit (Superhuman)",
|
||||
"product_market_fit_superhuman_description": "Measure PMF by assessing how disappointed users would be if your product disappeared.",
|
||||
"product_market_fit_superhuman_question_1_button_label": "Happy to help!",
|
||||
"product_market_fit_superhuman_question_1_dismiss_button_label": "No, thanks.",
|
||||
"product_market_fit_superhuman_question_1_headline": "You are one of our power users! Do you have 5 minutes?",
|
||||
"product_market_fit_superhuman_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We would love to understand your user experience better. Sharing your insight helps a lot.</span></p>",
|
||||
"product_market_fit_superhuman_question_2_choice_1": "Not at all disappointed",
|
||||
@@ -2810,7 +2813,6 @@
|
||||
"site_abandonment_survey_description": "Understand the reasons behind site abandonment in your web shop.",
|
||||
"site_abandonment_survey_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We noticed you're leaving our site without making a purchase. We would love to understand why.</span></p>",
|
||||
"site_abandonment_survey_question_2_button_label": "Sure!",
|
||||
"site_abandonment_survey_question_2_dismiss_button_label": "No, thanks.",
|
||||
"site_abandonment_survey_question_2_headline": "Do you have a minute?",
|
||||
"site_abandonment_survey_question_3_choice_1": "Can't find what I am looking for",
|
||||
"site_abandonment_survey_question_3_choice_2": "Found a better site",
|
||||
@@ -2835,7 +2837,6 @@
|
||||
"site_abandonment_survey_question_7_label": "Yes, please reach out.",
|
||||
"site_abandonment_survey_question_8_headline": "Please share your email address:",
|
||||
"site_abandonment_survey_question_9_headline": "Any additional comments or suggestions?",
|
||||
"skip": "Skip",
|
||||
"smileys_survey_name": "Smileys Survey",
|
||||
"smileys_survey_question_1_headline": "How do you like $[projectName]?",
|
||||
"smileys_survey_question_1_lower_label": "Not good",
|
||||
|
||||
+24
-23
@@ -1206,9 +1206,9 @@
|
||||
"add": "Añadir +",
|
||||
"add_a_delay_or_auto_close_the_survey": "Añadir un retraso o cerrar automáticamente la encuesta",
|
||||
"add_a_four_digit_pin": "Añadir un PIN de cuatro dígitos",
|
||||
"add_a_new_question_to_your_survey": "Añadir una nueva pregunta a tu encuesta",
|
||||
"add_a_variable_to_calculate": "Añadir una variable para calcular",
|
||||
"add_action_below": "Añadir acción debajo",
|
||||
"add_block": "Añadir bloque",
|
||||
"add_choice_below": "Añadir opción debajo",
|
||||
"add_color_coding": "Añadir codificación por colores",
|
||||
"add_color_coding_description": "Añadir códigos de color rojo, naranja y verde a las opciones.",
|
||||
@@ -1229,8 +1229,8 @@
|
||||
"add_other": "Añadir \"Otro\"",
|
||||
"add_photo_or_video": "Añadir foto o vídeo",
|
||||
"add_pin": "Añadir PIN",
|
||||
"add_question": "Añadir pregunta",
|
||||
"add_question_below": "Añadir pregunta debajo",
|
||||
"add_question_to_block": "Añadir pregunta al bloque",
|
||||
"add_row": "Añadir fila",
|
||||
"add_variable": "Añadir variable",
|
||||
"address_fields": "Campos de dirección",
|
||||
@@ -1256,12 +1256,14 @@
|
||||
"automatically_mark_the_survey_as_complete_after": "Marcar automáticamente la encuesta como completa después de",
|
||||
"back_button_label": "Etiqueta del botón \"Atrás\"",
|
||||
"background_styling": "Estilo de fondo",
|
||||
"block_deleted": "Bloque eliminado.",
|
||||
"block_duplicated": "Bloque duplicado.",
|
||||
"bold": "Negrita",
|
||||
"brand_color": "Color de marca",
|
||||
"brightness": "Brillo",
|
||||
"button_external": "Habilitar enlace externo",
|
||||
"button_external_description": "Añadir un botón que abre una URL externa en una nueva pestaña",
|
||||
"button_label": "Etiqueta del botón",
|
||||
"button_to_continue_in_survey": "Botón para continuar en la encuesta",
|
||||
"button_to_link_to_external_url": "Botón para enlazar a URL externa",
|
||||
"button_url": "URL del botón",
|
||||
"cal_username": "Nombre de usuario de Cal.com o nombre de usuario/evento",
|
||||
"calculate": "Calcular",
|
||||
@@ -1300,6 +1302,7 @@
|
||||
"character_limit_toggle_title": "Añadir límites de caracteres",
|
||||
"checkbox_label": "Etiqueta de casilla de verificación",
|
||||
"choose_the_actions_which_trigger_the_survey": "Elige las acciones que activan la encuesta.",
|
||||
"choose_the_first_question_on_your_block": "Elige la primera pregunta en tu bloque",
|
||||
"choose_where_to_run_the_survey": "Elige dónde ejecutar la encuesta.",
|
||||
"city": "Ciudad",
|
||||
"close_survey_on_response_limit": "Cerrar encuesta al alcanzar el límite de respuestas",
|
||||
@@ -1323,10 +1326,12 @@
|
||||
"create_group": "Crear grupo",
|
||||
"create_your_own_survey": "Crea tu propia encuesta",
|
||||
"css_selector": "Selector CSS",
|
||||
"cta_button_label": "Etiqueta del botón \"CTA\"",
|
||||
"custom_hostname": "Nombre de host personalizado",
|
||||
"darken_or_lighten_background_of_your_choice": "Oscurece o aclara el fondo de tu elección.",
|
||||
"date_format": "Formato de fecha",
|
||||
"days_before_showing_this_survey_again": "días después de que se muestre cualquier encuesta antes de que esta encuesta pueda aparecer.",
|
||||
"delete_block": "Eliminar bloque",
|
||||
"delete_choice": "Eliminar opción",
|
||||
"disable_the_visibility_of_survey_progress": "Desactivar la visibilidad del progreso de la encuesta.",
|
||||
"display_an_estimate_of_completion_time_for_survey": "Mostrar una estimación del tiempo de finalización de la encuesta",
|
||||
@@ -1338,9 +1343,12 @@
|
||||
"does_not_include_all_of": "No incluye todos los",
|
||||
"does_not_include_one_of": "No incluye uno de",
|
||||
"does_not_start_with": "No comienza con",
|
||||
"duplicate_block": "Duplicar bloque",
|
||||
"duplicate_question": "Duplicar pregunta",
|
||||
"edit_link": "Editar enlace",
|
||||
"edit_recall": "Editar recuperación",
|
||||
"edit_translations": "Editar traducciones de {lang}",
|
||||
"element_not_found": "Pregunta no encontrada",
|
||||
"enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permitir a los participantes cambiar el idioma de la encuesta en cualquier momento durante la encuesta.",
|
||||
"enable_recaptcha_to_protect_your_survey_from_spam": "La protección contra spam utiliza reCAPTCHA v3 para filtrar las respuestas spam.",
|
||||
"enable_spam_protection": "Protección contra spam",
|
||||
@@ -1416,11 +1424,12 @@
|
||||
"hidden_field_used_in_recall": "El campo oculto \"{hiddenField}\" se está recordando en la pregunta {questionIndex}.",
|
||||
"hidden_field_used_in_recall_ending_card": "El campo oculto \"{hiddenField}\" se está recordando en la tarjeta final.",
|
||||
"hidden_field_used_in_recall_welcome": "El campo oculto \"{hiddenField}\" se está recordando en la tarjeta de bienvenida.",
|
||||
"hide_advanced_settings": "Ocultar ajustes avanzados",
|
||||
"hide_back_button": "Ocultar botón 'Atrás'",
|
||||
"hide_back_button_description": "No mostrar el botón de retroceso en la encuesta",
|
||||
"hide_block_settings": "Ocultar ajustes del bloque",
|
||||
"hide_logo": "Ocultar logotipo",
|
||||
"hide_progress_bar": "Ocultar barra de progreso",
|
||||
"hide_question_settings": "Ocultar ajustes de la pregunta",
|
||||
"hide_the_logo_in_this_specific_survey": "Ocultar el logotipo en esta encuesta específica",
|
||||
"hostname": "Nombre de host",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "¿Cuánto estilo quieres darle a tus tarjetas en las encuestas de tipo {surveyTypeDerived}?",
|
||||
@@ -1447,6 +1456,7 @@
|
||||
"is_clicked": "Está clicado",
|
||||
"is_completely_submitted": "Está completamente enviado",
|
||||
"is_empty": "Está vacío",
|
||||
"is_not_clicked": "No está clicado",
|
||||
"is_not_empty": "No está vacío",
|
||||
"is_not_set": "No está establecido",
|
||||
"is_partially_submitted": "Está parcialmente enviado",
|
||||
@@ -1454,7 +1464,7 @@
|
||||
"is_skipped": "Está omitido",
|
||||
"is_submitted": "Está enviado",
|
||||
"italic": "Cursiva",
|
||||
"jump_to_question": "Saltar a pregunta",
|
||||
"jump_to_block": "Saltar al bloque",
|
||||
"keep_current_order": "Mantener orden actual",
|
||||
"keep_showing_while_conditions_match": "Seguir mostrando mientras las condiciones coincidan",
|
||||
"key": "Clave",
|
||||
@@ -1468,16 +1478,18 @@
|
||||
"logic_error_warning": "El cambio causará errores lógicos",
|
||||
"logic_error_warning_text": "Cambiar el tipo de pregunta eliminará las condiciones lógicas de esta pregunta",
|
||||
"long_answer": "Respuesta larga",
|
||||
"long_answer_toggle_description": "Permitir a los encuestados escribir respuestas más largas y de varias líneas.",
|
||||
"lower_label": "Etiqueta inferior",
|
||||
"manage_languages": "Gestionar idiomas",
|
||||
"matrix_all_fields": "Todos los campos",
|
||||
"matrix_rows": "Filas",
|
||||
"max_file_size": "Tamaño máximo de archivo",
|
||||
"max_file_size_limit_is": "El límite de tamaño máximo de archivo es",
|
||||
"move_question_to_block": "Mover pregunta al bloque",
|
||||
"multiply": "Multiplicar *",
|
||||
"needed_for_self_hosted_cal_com_instance": "Necesario para una instancia Cal.com autohospedada",
|
||||
"next_block": "Bloque siguiente",
|
||||
"next_button_label": "Etiqueta del botón \"Siguiente\"",
|
||||
"next_question": "Pregunta siguiente",
|
||||
"no_hidden_fields_yet_add_first_one_below": "Aún no hay campos ocultos. Añade el primero a continuación.",
|
||||
"no_images_found_for": "No se encontraron imágenes para ''{query}\"",
|
||||
"no_languages_found_add_first_one_to_get_started": "No se encontraron idiomas. Añade el primero para comenzar.",
|
||||
@@ -1589,17 +1601,17 @@
|
||||
"set_the_global_placement_in_the_look_feel_settings": "Establece la ubicación global en los ajustes de apariencia.",
|
||||
"settings_saved_successfully": "Ajustes guardados correctamente.",
|
||||
"seven_points": "7 puntos",
|
||||
"show_advanced_settings": "Mostrar ajustes avanzados",
|
||||
"show_block_settings": "Mostrar ajustes del bloque",
|
||||
"show_button": "Mostrar botón",
|
||||
"show_language_switch": "Mostrar cambio de idioma",
|
||||
"show_multiple_times": "Mostrar un número limitado de veces",
|
||||
"show_only_once": "Mostrar solo una vez",
|
||||
"show_question_settings": "Mostrar ajustes de la pregunta",
|
||||
"show_survey_maximum_of": "Mostrar encuesta un máximo de",
|
||||
"show_survey_to_users": "Mostrar encuesta al % de usuarios",
|
||||
"show_to_x_percentage_of_targeted_users": "Mostrar al {percentage} % de usuarios objetivo",
|
||||
"simple": "Simple",
|
||||
"six_points": "6 puntos",
|
||||
"skip_button_label": "Etiqueta del botón omitir",
|
||||
"smiley": "Emoticono",
|
||||
"spam_protection_note": "La protección contra spam no funciona para encuestas mostradas con los SDK de iOS, React Native y Android. Romperá la encuesta.",
|
||||
"spam_protection_threshold_description": "Establece un valor entre 0 y 1, las respuestas por debajo de este valor serán rechazadas.",
|
||||
@@ -1620,6 +1632,7 @@
|
||||
"survey_placement": "Ubicación de la encuesta",
|
||||
"survey_trigger": "Activador de la encuesta",
|
||||
"switch_multi_lanugage_on_to_get_started": "Activa el multiidioma para empezar 👉",
|
||||
"target_block_not_found": "Bloque objetivo no encontrado",
|
||||
"targeted": "Dirigido",
|
||||
"ten_points": "10 puntos",
|
||||
"the_survey_will_be_shown_multiple_times_until_they_respond": "Mostrar como máximo el número de veces especificado, o hasta que respondan (lo que ocurra primero).",
|
||||
@@ -1639,6 +1652,7 @@
|
||||
"unlock_targeting_title": "Desbloquea la segmentación con un plan superior",
|
||||
"unsaved_changes_warning": "Tienes cambios sin guardar en tu encuesta. ¿Quieres guardarlos antes de salir?",
|
||||
"until_they_submit_a_response": "Preguntar hasta que envíen una respuesta",
|
||||
"untitled_block": "Bloque sin título",
|
||||
"upgrade_notice_description": "Crea encuestas multilingües y desbloquea muchas más funciones",
|
||||
"upgrade_notice_title": "Desbloquea encuestas multilingües con un plan superior",
|
||||
"upload": "Subir",
|
||||
@@ -2070,7 +2084,6 @@
|
||||
"card_abandonment_survey": "Encuesta de abandono del carrito",
|
||||
"card_abandonment_survey_description": "Comprende las razones detrás del abandono del carrito en tu tienda web.",
|
||||
"card_abandonment_survey_question_1_button_label": "¡Claro!",
|
||||
"card_abandonment_survey_question_1_dismiss_button_label": "No, gracias.",
|
||||
"card_abandonment_survey_question_1_headline": "¿Tienes 2 minutos para ayudarnos a mejorar?",
|
||||
"card_abandonment_survey_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Hemos notado que dejaste algunos artículos en tu carrito. Nos encantaría entender por qué.</span></p>",
|
||||
"card_abandonment_survey_question_2_choice_1": "Costes de envío elevados",
|
||||
@@ -2157,12 +2170,10 @@
|
||||
"churn_survey_question_2_button_label": "Enviar",
|
||||
"churn_survey_question_2_headline": "¿Qué habría hecho que $[projectName] fuera más fácil de usar?",
|
||||
"churn_survey_question_3_button_label": "Obtener 30 % de descuento",
|
||||
"churn_survey_question_3_dismiss_button_label": "Omitir",
|
||||
"churn_survey_question_3_headline": "¡Obtén un 30 % de descuento para el próximo año!",
|
||||
"churn_survey_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Nos encantaría mantenerte como cliente. Estaremos encantados de ofrecerte un 30 % de descuento para el próximo año.</span></p>",
|
||||
"churn_survey_question_4_headline": "¿Qué funcionalidades echas en falta?",
|
||||
"churn_survey_question_5_button_label": "Enviar correo electrónico al CEO",
|
||||
"churn_survey_question_5_dismiss_button_label": "Omitir",
|
||||
"churn_survey_question_5_headline": "Lamentamos oír eso 😔 ¡Habla directamente con nuestra CEO!",
|
||||
"churn_survey_question_5_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Nuestro objetivo es proporcionar el mejor servicio al cliente posible. Por favor, envía un correo electrónico a nuestra CEO y ella personalmente se encargará de tu problema.</span></p>",
|
||||
"collect_feedback_description": "Recopila comentarios completos sobre tu producto o servicio.",
|
||||
@@ -2269,6 +2280,7 @@
|
||||
"csat_survey_question_3_headline": "Vaya, ¡lo sentimos! ¿Hay algo que podamos hacer para mejorar tu experiencia?",
|
||||
"csat_survey_question_3_placeholder": "Escribe tu respuesta aquí...",
|
||||
"cta_description": "Muestra información y anima a los usuarios a realizar una acción específica",
|
||||
"custom_survey_block_1_name": "Bloque 1",
|
||||
"custom_survey_description": "Crea una encuesta sin plantilla.",
|
||||
"custom_survey_name": "Empezar desde cero",
|
||||
"custom_survey_question_1_headline": "¿Qué te gustaría saber?",
|
||||
@@ -2355,7 +2367,6 @@
|
||||
"evaluate_a_product_idea_description": "Encuesta a usuarios sobre ideas de productos o funcionalidades. Obtén feedback rápidamente.",
|
||||
"evaluate_a_product_idea_name": "Evaluar una idea de producto",
|
||||
"evaluate_a_product_idea_question_1_button_label": "¡Vamos a ello!",
|
||||
"evaluate_a_product_idea_question_1_dismiss_button_label": "Omitir",
|
||||
"evaluate_a_product_idea_question_1_headline": "¡Nos encanta cómo utilizas $[projectName]! Nos gustaría conocer tu opinión sobre una idea de funcionalidad. ¿Tienes un minuto?",
|
||||
"evaluate_a_product_idea_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Respetamos tu tiempo y lo hemos hecho breve 🤸</span></p>",
|
||||
"evaluate_a_product_idea_question_2_headline": "¡Gracias! ¿Qué grado de dificultad o facilidad tienes hoy para [ÁREA DEL PROBLEMA]?",
|
||||
@@ -2364,7 +2375,6 @@
|
||||
"evaluate_a_product_idea_question_3_headline": "¿Qué es lo más difícil para ti cuando se trata de [ÁREA DEL PROBLEMA]?",
|
||||
"evaluate_a_product_idea_question_3_placeholder": "Escribe tu respuesta aquí...",
|
||||
"evaluate_a_product_idea_question_4_button_label": "Siguiente",
|
||||
"evaluate_a_product_idea_question_4_dismiss_button_label": "Omitir",
|
||||
"evaluate_a_product_idea_question_4_headline": "Estamos trabajando en una idea para ayudar con [ÁREA DEL PROBLEMA].",
|
||||
"evaluate_a_product_idea_question_4_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Inserta aquí el resumen del concepto. Añade los detalles necesarios pero mantenlo conciso y fácil de entender.</span></p>",
|
||||
"evaluate_a_product_idea_question_5_headline": "¿Qué valor tendría esta función para ti?",
|
||||
@@ -2414,7 +2424,6 @@
|
||||
"feedback_box_question_2_headline": "¿Qué está fallando?",
|
||||
"feedback_box_question_2_subheader": "Cuanto más detalle, mejor :)",
|
||||
"feedback_box_question_3_button_label": "Sí, notifícame",
|
||||
"feedback_box_question_3_dismiss_button_label": "No, gracias",
|
||||
"feedback_box_question_3_headline": "¿Quieres estar al tanto?",
|
||||
"feedback_box_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Solucionaremos esto lo antes posible. ¿Quieres que te avisemos cuando lo hayamos hecho?</span></p>",
|
||||
"feedback_box_question_4_button_label": "Solicitar función",
|
||||
@@ -2439,7 +2448,6 @@
|
||||
"identify_sign_up_barriers_description": "Ofrece un descuento para obtener información sobre las barreras de registro.",
|
||||
"identify_sign_up_barriers_name": "Identificar barreras de registro",
|
||||
"identify_sign_up_barriers_question_1_button_label": "Obtener 10 % de descuento",
|
||||
"identify_sign_up_barriers_question_1_dismiss_button_label": "No, gracias",
|
||||
"identify_sign_up_barriers_question_1_headline": "¡Responde a esta breve encuesta y obtén un 10 % de descuento!",
|
||||
"identify_sign_up_barriers_question_1_html": "Parece que estás considerando registrarte. Responde a cuatro preguntas y obtén un 10 % de descuento en cualquier plan.",
|
||||
"identify_sign_up_barriers_question_2_headline": "¿Qué probabilidad hay de que te registres en $[projectName]?",
|
||||
@@ -2462,7 +2470,6 @@
|
||||
"identify_sign_up_barriers_question_8_headline": "Por favor, explícalo:",
|
||||
"identify_sign_up_barriers_question_8_placeholder": "Escribe tu respuesta aquí...",
|
||||
"identify_sign_up_barriers_question_9_button_label": "Registrarse",
|
||||
"identify_sign_up_barriers_question_9_dismiss_button_label": "Omitir por ahora",
|
||||
"identify_sign_up_barriers_question_9_headline": "¡Gracias! Aquí está tu código: SIGNUPNOW10",
|
||||
"identify_sign_up_barriers_question_9_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Muchas gracias por tomarte el tiempo para compartir tu opinión 🙏</span></p>",
|
||||
"identify_upsell_opportunities_description": "Descubre cuánto tiempo ahorra tu producto a tu usuario. Úsalo para vender más.",
|
||||
@@ -2499,7 +2506,6 @@
|
||||
"improve_newsletter_content_question_2_headline": "¿Qué habría hecho más útil el boletín de esta semana?",
|
||||
"improve_newsletter_content_question_2_placeholder": "Escribe tu respuesta aquí...",
|
||||
"improve_newsletter_content_question_3_button_label": "¡Encantado de ayudar!",
|
||||
"improve_newsletter_content_question_3_dismiss_button_label": "Busca tus propios amigos",
|
||||
"improve_newsletter_content_question_3_headline": "¡Gracias! ❤️ Comparte el amor con UN amigo.",
|
||||
"improve_newsletter_content_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>¿Quién piensa como tú? ¡Nos harías un gran favor si compartieras el episodio de esta semana con tu amigo intelectual!</span></p>",
|
||||
"improve_trial_conversion_description": "Descubre por qué las personas cancelaron su prueba. Estos conocimientos te ayudan a mejorar tu embudo de conversión.",
|
||||
@@ -2514,7 +2520,6 @@
|
||||
"improve_trial_conversion_question_2_button_label": "Siguiente",
|
||||
"improve_trial_conversion_question_2_headline": "Lamentamos oír eso. ¿Cuál fue el mayor problema al usar $[projectName]?",
|
||||
"improve_trial_conversion_question_4_button_label": "Obtener 20 % de descuento",
|
||||
"improve_trial_conversion_question_4_dismiss_button_label": "Omitir",
|
||||
"improve_trial_conversion_question_4_headline": "¡Sentimos oírlo! Obtén un 20 % de descuento en el primer año.",
|
||||
"improve_trial_conversion_question_4_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Nos complace ofrecerte un 20 % de descuento en un plan anual.</span></p>",
|
||||
"improve_trial_conversion_question_5_button_label": "Siguiente",
|
||||
@@ -2678,7 +2683,6 @@
|
||||
"preview_survey_question_2_choice_2_label": "¡No, gracias!",
|
||||
"preview_survey_question_2_headline": "¿Quieres estar al tanto?",
|
||||
"preview_survey_welcome_card_headline": "¡Bienvenido!",
|
||||
"preview_survey_welcome_card_html": "Gracias por proporcionar tu opinión - ¡vamos allá!",
|
||||
"prioritize_features_description": "Identifica las funciones que tus usuarios necesitan más y menos.",
|
||||
"prioritize_features_name": "Priorizar funciones",
|
||||
"prioritize_features_question_1_choice_1": "Función 1",
|
||||
@@ -2704,7 +2708,6 @@
|
||||
"product_market_fit_superhuman": "Ajuste producto-mercado (Superhuman)",
|
||||
"product_market_fit_superhuman_description": "Mide el PMF evaluando cuán decepcionados estarían los usuarios si tu producto desapareciera.",
|
||||
"product_market_fit_superhuman_question_1_button_label": "¡Encantado de ayudar!",
|
||||
"product_market_fit_superhuman_question_1_dismiss_button_label": "No, gracias.",
|
||||
"product_market_fit_superhuman_question_1_headline": "¡Eres uno de nuestros usuarios avanzados! ¿Tienes 5 minutos?",
|
||||
"product_market_fit_superhuman_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Nos encantaría entender mejor tu experiencia como usuario. Compartir tu opinión ayuda mucho.</span></p>",
|
||||
"product_market_fit_superhuman_question_2_choice_1": "Nada decepcionado",
|
||||
@@ -2810,7 +2813,6 @@
|
||||
"site_abandonment_survey_description": "Comprende las razones detrás del abandono del sitio en tu tienda web.",
|
||||
"site_abandonment_survey_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Hemos notado que estás abandonando nuestro sitio sin realizar una compra. Nos encantaría entender por qué.</span></p>",
|
||||
"site_abandonment_survey_question_2_button_label": "¡Claro!",
|
||||
"site_abandonment_survey_question_2_dismiss_button_label": "No, gracias.",
|
||||
"site_abandonment_survey_question_2_headline": "¿Tienes un minuto?",
|
||||
"site_abandonment_survey_question_3_choice_1": "No encuentro lo que estoy buscando",
|
||||
"site_abandonment_survey_question_3_choice_2": "He encontrado un sitio mejor",
|
||||
@@ -2835,7 +2837,6 @@
|
||||
"site_abandonment_survey_question_7_label": "Sí, me gustaría recibir información.",
|
||||
"site_abandonment_survey_question_8_headline": "Por favor, comparte tu dirección de correo electrónico:",
|
||||
"site_abandonment_survey_question_9_headline": "¿Algún comentario o sugerencia adicional?",
|
||||
"skip": "Omitir",
|
||||
"smileys_survey_name": "Encuesta de emoticonos",
|
||||
"smileys_survey_question_1_headline": "¿Qué te parece $[projectName]?",
|
||||
"smileys_survey_question_1_lower_label": "No bueno",
|
||||
|
||||
+25
-24
@@ -1206,9 +1206,9 @@
|
||||
"add": "Ajouter +",
|
||||
"add_a_delay_or_auto_close_the_survey": "Ajouter un délai ou fermer automatiquement l'enquête",
|
||||
"add_a_four_digit_pin": "Ajoutez un code PIN à quatre chiffres.",
|
||||
"add_a_new_question_to_your_survey": "Ajouter une nouvelle question à votre enquête",
|
||||
"add_a_variable_to_calculate": "Ajouter une variable à calculer",
|
||||
"add_action_below": "Ajouter une action ci-dessous",
|
||||
"add_block": "Ajouter un bloc",
|
||||
"add_choice_below": "Ajouter une option ci-dessous",
|
||||
"add_color_coding": "Ajouter un code couleur",
|
||||
"add_color_coding_description": "Ajoutez des codes de couleur rouge, orange et vert aux options.",
|
||||
@@ -1229,8 +1229,8 @@
|
||||
"add_other": "Ajouter \"Autre",
|
||||
"add_photo_or_video": "Ajouter une photo ou une vidéo",
|
||||
"add_pin": "Ajouter un code PIN",
|
||||
"add_question": "Ajouter une question",
|
||||
"add_question_below": "Ajouter une question ci-dessous",
|
||||
"add_question_to_block": "Ajouter une question au bloc",
|
||||
"add_row": "Ajouter une ligne",
|
||||
"add_variable": "Ajouter une variable",
|
||||
"address_fields": "Champs d'adresse",
|
||||
@@ -1256,12 +1256,14 @@
|
||||
"automatically_mark_the_survey_as_complete_after": "Marquer automatiquement l'enquête comme terminée après",
|
||||
"back_button_label": "Label du bouton \"Retour''",
|
||||
"background_styling": "Style de fond",
|
||||
"block_deleted": "Bloc supprimé.",
|
||||
"block_duplicated": "Bloc dupliqué.",
|
||||
"bold": "Gras",
|
||||
"brand_color": "Couleur de marque",
|
||||
"brightness": "Luminosité",
|
||||
"button_external": "Activer le lien externe",
|
||||
"button_external_description": "Ajouter un bouton qui ouvre une URL externe dans un nouvel onglet",
|
||||
"button_label": "Label du bouton",
|
||||
"button_to_continue_in_survey": "Bouton pour continuer dans l'enquête",
|
||||
"button_to_link_to_external_url": "Bouton pour lier à une URL externe",
|
||||
"button_url": "URL du bouton",
|
||||
"cal_username": "Nom d'utilisateur Cal.com ou nom d'utilisateur/événement",
|
||||
"calculate": "Calculer",
|
||||
@@ -1300,6 +1302,7 @@
|
||||
"character_limit_toggle_title": "Ajouter des limites de caractères",
|
||||
"checkbox_label": "Étiquette de case à cocher",
|
||||
"choose_the_actions_which_trigger_the_survey": "Choisissez les actions qui déclenchent l'enquête.",
|
||||
"choose_the_first_question_on_your_block": "Choisissez la première question de votre bloc",
|
||||
"choose_where_to_run_the_survey": "Choisissez où réaliser l'enquête.",
|
||||
"city": "Ville",
|
||||
"close_survey_on_response_limit": "Fermer l'enquête sur la limite de réponse",
|
||||
@@ -1323,10 +1326,12 @@
|
||||
"create_group": "Créer un groupe",
|
||||
"create_your_own_survey": "Créez votre propre enquête",
|
||||
"css_selector": "Sélecteur CSS",
|
||||
"cta_button_label": "Libellé du bouton « CTA »",
|
||||
"custom_hostname": "Nom d'hôte personnalisé",
|
||||
"darken_or_lighten_background_of_your_choice": "Assombrir ou éclaircir l'arrière-plan de votre choix.",
|
||||
"date_format": "Format de date",
|
||||
"days_before_showing_this_survey_again": "jours après qu'une enquête soit affichée avant que cette enquête puisse apparaître.",
|
||||
"delete_block": "Supprimer le bloc",
|
||||
"delete_choice": "Supprimer l'option",
|
||||
"disable_the_visibility_of_survey_progress": "Désactiver la visibilité de la progression du sondage.",
|
||||
"display_an_estimate_of_completion_time_for_survey": "Afficher une estimation du temps de complétion pour l'enquête.",
|
||||
@@ -1338,9 +1343,12 @@
|
||||
"does_not_include_all_of": "n'inclut pas tout",
|
||||
"does_not_include_one_of": "n'inclut pas un de",
|
||||
"does_not_start_with": "Ne commence pas par",
|
||||
"duplicate_block": "Dupliquer le bloc",
|
||||
"duplicate_question": "Dupliquer la question",
|
||||
"edit_link": "Modifier le lien",
|
||||
"edit_recall": "Modifier le rappel",
|
||||
"edit_translations": "Modifier les traductions {lang}",
|
||||
"element_not_found": "Question non trouvée",
|
||||
"enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permettre aux répondants de changer de langue à tout moment. Nécessite au moins 2 langues actives.",
|
||||
"enable_recaptcha_to_protect_your_survey_from_spam": "La protection contre le spam utilise reCAPTCHA v3 pour filtrer les réponses indésirables.",
|
||||
"enable_spam_protection": "Protection contre le spam",
|
||||
@@ -1416,11 +1424,12 @@
|
||||
"hidden_field_used_in_recall": "Le champ caché \"{hiddenField}\" est rappelé dans la question {questionIndex}.",
|
||||
"hidden_field_used_in_recall_ending_card": "Le champ caché \"{hiddenField}\" est rappelé dans la carte de fin.",
|
||||
"hidden_field_used_in_recall_welcome": "Le champ caché \"{hiddenField}\" est rappelé dans la carte de bienvenue.",
|
||||
"hide_advanced_settings": "Cacher les paramètres avancés",
|
||||
"hide_back_button": "Masquer le bouton 'Retour'",
|
||||
"hide_back_button_description": "Ne pas afficher le bouton retour dans l'enquête",
|
||||
"hide_block_settings": "Masquer les paramètres du bloc",
|
||||
"hide_logo": "Cacher le logo",
|
||||
"hide_progress_bar": "Cacher la barre de progression",
|
||||
"hide_question_settings": "Masquer les paramètres de la question",
|
||||
"hide_the_logo_in_this_specific_survey": "Cacher le logo dans cette enquête spécifique",
|
||||
"hostname": "Nom d'hôte",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "À quel point voulez-vous que vos cartes soient funky dans les enquêtes {surveyTypeDerived}",
|
||||
@@ -1447,6 +1456,7 @@
|
||||
"is_clicked": "Est cliqué",
|
||||
"is_completely_submitted": "Est complètement soumis",
|
||||
"is_empty": "Est vide",
|
||||
"is_not_clicked": "N'est pas cliqué",
|
||||
"is_not_empty": "N'est pas vide",
|
||||
"is_not_set": "N'est pas défini",
|
||||
"is_partially_submitted": "Est partiellement soumis",
|
||||
@@ -1454,7 +1464,7 @@
|
||||
"is_skipped": "Est ignoré",
|
||||
"is_submitted": "Est soumis",
|
||||
"italic": "Italique",
|
||||
"jump_to_question": "Passer à la question",
|
||||
"jump_to_block": "Aller au bloc",
|
||||
"keep_current_order": "Conserver la commande actuelle",
|
||||
"keep_showing_while_conditions_match": "Continuer à afficher tant que les conditions correspondent",
|
||||
"key": "Clé",
|
||||
@@ -1468,16 +1478,18 @@
|
||||
"logic_error_warning": "Changer causera des erreurs logiques",
|
||||
"logic_error_warning_text": "Changer le type de question supprimera les conditions logiques de cette question.",
|
||||
"long_answer": "Longue réponse",
|
||||
"long_answer_toggle_description": "Permettre aux répondants d'écrire des réponses plus longues et sur plusieurs lignes.",
|
||||
"lower_label": "Étiquette inférieure",
|
||||
"manage_languages": "Gérer les langues",
|
||||
"matrix_all_fields": "Tous les champs",
|
||||
"matrix_rows": "Lignes",
|
||||
"max_file_size": "Taille maximale du fichier",
|
||||
"max_file_size_limit_is": "La taille maximale du fichier est",
|
||||
"move_question_to_block": "Déplacer la question vers le bloc",
|
||||
"multiply": "Multiplier *",
|
||||
"needed_for_self_hosted_cal_com_instance": "Nécessaire pour une instance Cal.com auto-hébergée",
|
||||
"next_button_label": "Label du bouton \"Suivant\"",
|
||||
"next_question": "Question suivante",
|
||||
"next_block": "Bloc suivant",
|
||||
"next_button_label": "Libellé du bouton « Suivant »",
|
||||
"no_hidden_fields_yet_add_first_one_below": "Aucun champ caché pour le moment. Ajoutez le premier ci-dessous.",
|
||||
"no_images_found_for": "Aucune image trouvée pour ''{query}\"",
|
||||
"no_languages_found_add_first_one_to_get_started": "Aucune langue trouvée. Ajoutez la première pour commencer.",
|
||||
@@ -1589,17 +1601,17 @@
|
||||
"set_the_global_placement_in_the_look_feel_settings": "Définissez le placement global dans les paramètres d'apparence.",
|
||||
"settings_saved_successfully": "Paramètres enregistrés avec succès",
|
||||
"seven_points": "7 points",
|
||||
"show_advanced_settings": "Afficher les paramètres avancés",
|
||||
"show_block_settings": "Afficher les paramètres du bloc",
|
||||
"show_button": "Afficher le bouton",
|
||||
"show_language_switch": "Afficher le changement de langue",
|
||||
"show_multiple_times": "Afficher un nombre limité de fois",
|
||||
"show_only_once": "Afficher une seule fois",
|
||||
"show_question_settings": "Afficher les paramètres de la question",
|
||||
"show_survey_maximum_of": "Afficher le maximum du sondage de",
|
||||
"show_survey_to_users": "Afficher l'enquête à % des utilisateurs",
|
||||
"show_to_x_percentage_of_targeted_users": "Afficher à {percentage}% des utilisateurs ciblés",
|
||||
"simple": "Simple",
|
||||
"six_points": "6 points",
|
||||
"skip_button_label": "Étiquette du bouton Ignorer",
|
||||
"smiley": "Sourire",
|
||||
"spam_protection_note": "La protection contre le spam ne fonctionne pas pour les enquêtes affichées avec les SDK iOS, React Native et Android. Cela cassera l'enquête.",
|
||||
"spam_protection_threshold_description": "Définir une valeur entre 0 et 1, les réponses en dessous de cette valeur seront rejetées.",
|
||||
@@ -1620,6 +1632,7 @@
|
||||
"survey_placement": "Placement de l'enquête",
|
||||
"survey_trigger": "Déclencheur d'enquête",
|
||||
"switch_multi_lanugage_on_to_get_started": "Activez le multilingue pour commencer 👉",
|
||||
"target_block_not_found": "Bloc cible non trouvé",
|
||||
"targeted": "Ciblé",
|
||||
"ten_points": "10 points",
|
||||
"the_survey_will_be_shown_multiple_times_until_they_respond": "Afficher au maximum le nombre de fois spécifié, ou jusqu'à ce qu'ils répondent (selon la première éventualité).",
|
||||
@@ -1639,6 +1652,7 @@
|
||||
"unlock_targeting_title": "Débloquez le ciblage avec un plan supérieur.",
|
||||
"unsaved_changes_warning": "Vous avez des modifications non enregistrées dans votre enquête. Souhaitez-vous les enregistrer avant de partir ?",
|
||||
"until_they_submit_a_response": "Demander jusqu'à ce qu'ils soumettent une réponse",
|
||||
"untitled_block": "Bloc sans titre",
|
||||
"upgrade_notice_description": "Créez des sondages multilingues et débloquez de nombreuses autres fonctionnalités",
|
||||
"upgrade_notice_title": "Débloquez les sondages multilingues avec un plan supérieur",
|
||||
"upload": "Télécharger",
|
||||
@@ -2070,7 +2084,6 @@
|
||||
"card_abandonment_survey": "Sondage sur l'abandon de panier",
|
||||
"card_abandonment_survey_description": "Comprendre les raisons derrière l'abandon de panier dans votre boutique en ligne.",
|
||||
"card_abandonment_survey_question_1_button_label": "Bien sûr !",
|
||||
"card_abandonment_survey_question_1_dismiss_button_label": "Non, merci.",
|
||||
"card_abandonment_survey_question_1_headline": "Avez-vous 2 minutes pour nous aider à nous améliorer ?",
|
||||
"card_abandonment_survey_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Nous avons remarqué que vous avez laissé des articles dans votre panier. Nous aimerions comprendre pourquoi.</span></p>",
|
||||
"card_abandonment_survey_question_2_choice_1": "Frais d'expédition élevés",
|
||||
@@ -2157,12 +2170,10 @@
|
||||
"churn_survey_question_2_button_label": "Envoyer",
|
||||
"churn_survey_question_2_headline": "Qu'est-ce qui aurait rendu $[projectName] plus facile à utiliser ?",
|
||||
"churn_survey_question_3_button_label": "Obtenez 30 % de réduction",
|
||||
"churn_survey_question_3_dismiss_button_label": "Sauter",
|
||||
"churn_survey_question_3_headline": "Obtenez 30 % de réduction pour l'année prochaine !",
|
||||
"churn_survey_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Nous aimerions vous garder comme client. Nous sommes heureux de vous offrir une remise de 30 % pour l'année prochaine.</span></p>",
|
||||
"churn_survey_question_4_headline": "Quelles fonctionnalités vous manquent ?",
|
||||
"churn_survey_question_5_button_label": "Envoyer un e-mail au PDG",
|
||||
"churn_survey_question_5_dismiss_button_label": "Sauter",
|
||||
"churn_survey_question_5_headline": "Je suis désolé d'apprendre cela 😔 Parlez directement à notre PDG !",
|
||||
"churn_survey_question_5_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Nous visons à fournir le meilleur service client possible. Veuillez envoyer un e-mail à notre PDG et elle s'occupera personnellement de votre problème.</span></p>",
|
||||
"collect_feedback_description": "Rassemblez des retours d'expérience complets sur votre produit ou service.",
|
||||
@@ -2269,6 +2280,7 @@
|
||||
"csat_survey_question_3_headline": "Ah, désolé ! Y a-t-il quelque chose que nous puissions faire pour améliorer votre expérience ?",
|
||||
"csat_survey_question_3_placeholder": "Entrez votre réponse ici...",
|
||||
"cta_description": "Afficher des informations et inciter les utilisateurs à effectuer une action spécifique",
|
||||
"custom_survey_block_1_name": "Bloc 1",
|
||||
"custom_survey_description": "Créez une enquête sans utiliser de modèle.",
|
||||
"custom_survey_name": "Tout créer moi-même",
|
||||
"custom_survey_question_1_headline": "Que voudriez-vous savoir ?",
|
||||
@@ -2355,7 +2367,6 @@
|
||||
"evaluate_a_product_idea_description": "Interrogez les utilisateurs sur des idées de produits ou de fonctionnalités. Obtenez des retours rapidement.",
|
||||
"evaluate_a_product_idea_name": "Évaluer une idée de produit",
|
||||
"evaluate_a_product_idea_question_1_button_label": "Faisons-le !",
|
||||
"evaluate_a_product_idea_question_1_dismiss_button_label": "Sauter",
|
||||
"evaluate_a_product_idea_question_1_headline": "Nous adorons la façon dont vous utilisez $[projectName] ! Nous aimerions avoir votre avis sur une idée de fonctionnalité. Avez-vous une minute ?",
|
||||
"evaluate_a_product_idea_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Nous respectons votre temps et nous avons fait court 🤸</span></p>",
|
||||
"evaluate_a_product_idea_question_2_headline": "Merci ! À quel point est-il difficile ou facile pour vous de [ZONE DE PROBLÈME] aujourd'hui ?",
|
||||
@@ -2364,7 +2375,6 @@
|
||||
"evaluate_a_product_idea_question_3_headline": "Qu'est-ce qui est le plus difficile pour vous en ce qui concerne [DOMAIN DE PROBLÈME] ?",
|
||||
"evaluate_a_product_idea_question_3_placeholder": "Entrez votre réponse ici...",
|
||||
"evaluate_a_product_idea_question_4_button_label": "Suivant",
|
||||
"evaluate_a_product_idea_question_4_dismiss_button_label": "Sauter",
|
||||
"evaluate_a_product_idea_question_4_headline": "Nous travaillons sur une idée pour aider avec [DOMAINES DE PROBLÈME].",
|
||||
"evaluate_a_product_idea_question_4_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Insérez le résumé du concept ici. Ajoutez les détails nécessaires tout en restant concis et facile à comprendre.</span></p>",
|
||||
"evaluate_a_product_idea_question_5_headline": "Quelle valeur cette fonctionnalité aurait-elle pour vous ?",
|
||||
@@ -2414,7 +2424,6 @@
|
||||
"feedback_box_question_2_headline": "Qu'est-ce qui est cassé ?",
|
||||
"feedback_box_question_2_subheader": "Plus il y a de détails, mieux c'est :)",
|
||||
"feedback_box_question_3_button_label": "Oui, prévenez-moi",
|
||||
"feedback_box_question_3_dismiss_button_label": "Non, merci",
|
||||
"feedback_box_question_3_headline": "Souhaitez-vous être informé ?",
|
||||
"feedback_box_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Nous allons régler cela dès que possible. Voulez-vous être informé lorsque ce sera fait ?</span></p>",
|
||||
"feedback_box_question_4_button_label": "Demander une fonctionnalité",
|
||||
@@ -2439,7 +2448,6 @@
|
||||
"identify_sign_up_barriers_description": "Offrir une remise pour recueillir des informations sur les obstacles à l'inscription.",
|
||||
"identify_sign_up_barriers_name": "Identifier les obstacles à l'inscription",
|
||||
"identify_sign_up_barriers_question_1_button_label": "Obtenez 10 % de réduction",
|
||||
"identify_sign_up_barriers_question_1_dismiss_button_label": "Non, merci",
|
||||
"identify_sign_up_barriers_question_1_headline": "Répondez à ce court sondage, obtenez 10 % de réduction !",
|
||||
"identify_sign_up_barriers_question_1_html": "Vous semblez envisager de vous inscrire. Répondez à quatre questions et obtenez 10 % sur n'importe quel plan.",
|
||||
"identify_sign_up_barriers_question_2_headline": "Quelle est la probabilité que vous vous inscriviez à $[projectName] ?",
|
||||
@@ -2462,7 +2470,6 @@
|
||||
"identify_sign_up_barriers_question_8_headline": "Veuillez expliquer :",
|
||||
"identify_sign_up_barriers_question_8_placeholder": "Entrez votre réponse ici...",
|
||||
"identify_sign_up_barriers_question_9_button_label": "S'inscrire",
|
||||
"identify_sign_up_barriers_question_9_dismiss_button_label": "Passer pour l'instant",
|
||||
"identify_sign_up_barriers_question_9_headline": "Merci ! Voici votre code : SIGNUPNOW10",
|
||||
"identify_sign_up_barriers_question_9_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Merci beaucoup d'avoir pris le temps de partager vos retours 🙏</span></p>",
|
||||
"identify_upsell_opportunities_description": "Découvrez combien de temps votre produit fait gagner à vos utilisateurs. Utilisez-le pour vendre davantage.",
|
||||
@@ -2499,7 +2506,6 @@
|
||||
"improve_newsletter_content_question_2_headline": "Qu'est-ce qui aurait rendu la newsletter de cette semaine plus utile ?",
|
||||
"improve_newsletter_content_question_2_placeholder": "Entrez votre réponse ici...",
|
||||
"improve_newsletter_content_question_3_button_label": "Ravi d'aider !",
|
||||
"improve_newsletter_content_question_3_dismiss_button_label": "Trouve tes propres amis",
|
||||
"improve_newsletter_content_question_3_headline": "Merci ! ❤️ Partage l'amour avec UN ami.",
|
||||
"improve_newsletter_content_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Qui pense comme vous ? Vous nous rendriez un grand service en partageant l'épisode de cette semaine avec votre ami cérébral !</span></p>",
|
||||
"improve_trial_conversion_description": "Découvrez pourquoi les gens ont arrêté leur essai. Ces informations vous aident à améliorer votre entonnoir.",
|
||||
@@ -2514,7 +2520,6 @@
|
||||
"improve_trial_conversion_question_2_button_label": "Suivant",
|
||||
"improve_trial_conversion_question_2_headline": "Désolé d'apprendre cela. Quel était le plus gros problème rencontré avec $[projectName] ?",
|
||||
"improve_trial_conversion_question_4_button_label": "Obtenez 20 % de réduction",
|
||||
"improve_trial_conversion_question_4_dismiss_button_label": "Sauter",
|
||||
"improve_trial_conversion_question_4_headline": "Désolé d'apprendre cela ! Bénéficiez de 20 % de réduction sur la première année.",
|
||||
"improve_trial_conversion_question_4_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Nous sommes heureux de vous offrir une remise de 20 % sur un plan annuel.</span></p>",
|
||||
"improve_trial_conversion_question_5_button_label": "Suivant",
|
||||
@@ -2678,7 +2683,6 @@
|
||||
"preview_survey_question_2_choice_2_label": "Non, merci !",
|
||||
"preview_survey_question_2_headline": "Souhaitez-vous être informé ?",
|
||||
"preview_survey_welcome_card_headline": "Bienvenue !",
|
||||
"preview_survey_welcome_card_html": "Merci pour vos retours - allons-y !",
|
||||
"prioritize_features_description": "Identifiez les fonctionnalités dont vos utilisateurs ont le plus et le moins besoin.",
|
||||
"prioritize_features_name": "Prioriser les fonctionnalités",
|
||||
"prioritize_features_question_1_choice_1": "Fonctionnalité 1",
|
||||
@@ -2704,7 +2708,6 @@
|
||||
"product_market_fit_superhuman": "Adéquation produit-marché (Superhuman)",
|
||||
"product_market_fit_superhuman_description": "Mesurez le PMF en évaluant à quel point les utilisateurs seraient déçus si votre produit disparaissait.",
|
||||
"product_market_fit_superhuman_question_1_button_label": "Ravi d'aider !",
|
||||
"product_market_fit_superhuman_question_1_dismiss_button_label": "Non, merci.",
|
||||
"product_market_fit_superhuman_question_1_headline": "Vous êtes l'un de nos utilisateurs avancés ! Avez-vous 5 minutes ?",
|
||||
"product_market_fit_superhuman_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Nous aimerions mieux comprendre votre expérience utilisateur. Partager vos idées nous aide beaucoup.</span></p>",
|
||||
"product_market_fit_superhuman_question_2_choice_1": "Pas du tout déçu",
|
||||
@@ -2810,7 +2813,6 @@
|
||||
"site_abandonment_survey_description": "Comprendre les raisons de l'abandon de site dans votre boutique en ligne.",
|
||||
"site_abandonment_survey_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Nous avons remarqué que vous quittez notre site sans effectuer d'achat. Nous aimerions comprendre pourquoi.</span></p>",
|
||||
"site_abandonment_survey_question_2_button_label": "Bien sûr !",
|
||||
"site_abandonment_survey_question_2_dismiss_button_label": "Non, merci.",
|
||||
"site_abandonment_survey_question_2_headline": "Avez-vous une minute ?",
|
||||
"site_abandonment_survey_question_3_choice_1": "Je ne trouve pas ce que je cherche",
|
||||
"site_abandonment_survey_question_3_choice_2": "Trouvé un meilleur site",
|
||||
@@ -2835,7 +2837,6 @@
|
||||
"site_abandonment_survey_question_7_label": "Oui, veuillez me contacter.",
|
||||
"site_abandonment_survey_question_8_headline": "Veuillez partager votre adresse e-mail :",
|
||||
"site_abandonment_survey_question_9_headline": "Avez-vous des commentaires ou des suggestions supplémentaires ?",
|
||||
"skip": "Sauter",
|
||||
"smileys_survey_name": "Sondage des Émoticônes",
|
||||
"smileys_survey_question_1_headline": "Que pensez-vous de $[projectName] ?",
|
||||
"smileys_survey_question_1_lower_label": "Pas bon",
|
||||
|
||||
+24
-23
@@ -1206,9 +1206,9 @@
|
||||
"add": "追加 +",
|
||||
"add_a_delay_or_auto_close_the_survey": "遅延を追加するか、フォームを自動的に閉じる",
|
||||
"add_a_four_digit_pin": "4桁のPINを追加",
|
||||
"add_a_new_question_to_your_survey": "フォームに新しい質問を追加",
|
||||
"add_a_variable_to_calculate": "計算する変数を追加",
|
||||
"add_action_below": "以下にアクションを追加",
|
||||
"add_block": "ブロックを追加",
|
||||
"add_choice_below": "以下に選択肢を追加",
|
||||
"add_color_coding": "色分けを追加",
|
||||
"add_color_coding_description": "オプションに赤、オレンジ、緑の色コードを追加します。",
|
||||
@@ -1229,8 +1229,8 @@
|
||||
"add_other": "「その他」を追加",
|
||||
"add_photo_or_video": "写真または動画を追加",
|
||||
"add_pin": "PINを追加",
|
||||
"add_question": "質問を追加",
|
||||
"add_question_below": "以下に質問を追加",
|
||||
"add_question_to_block": "ブロックに質問を追加",
|
||||
"add_row": "行を追加",
|
||||
"add_variable": "変数を追加",
|
||||
"address_fields": "住所フィールド",
|
||||
@@ -1256,12 +1256,14 @@
|
||||
"automatically_mark_the_survey_as_complete_after": "フォームを自動的に完了としてマークする",
|
||||
"back_button_label": "「戻る」ボタンのラベル",
|
||||
"background_styling": "背景のスタイル",
|
||||
"block_deleted": "ブロックが削除されました。",
|
||||
"block_duplicated": "ブロックが複製されました。",
|
||||
"bold": "太字",
|
||||
"brand_color": "ブランドカラー",
|
||||
"brightness": "明るさ",
|
||||
"button_external": "外部リンクを有効にする",
|
||||
"button_external_description": "新しいタブで外部URLを開くボタンを追加する",
|
||||
"button_label": "ボタンのラベル",
|
||||
"button_to_continue_in_survey": "フォームを続けるためのボタン",
|
||||
"button_to_link_to_external_url": "外部URLにリンクするためのボタン",
|
||||
"button_url": "ボタンURL",
|
||||
"cal_username": "Cal.comのユーザー名またはユーザー名/イベント",
|
||||
"calculate": "計算",
|
||||
@@ -1300,6 +1302,7 @@
|
||||
"character_limit_toggle_title": "文字数制限を追加",
|
||||
"checkbox_label": "チェックボックスのラベル",
|
||||
"choose_the_actions_which_trigger_the_survey": "フォームをトリガーするアクションを選択してください。",
|
||||
"choose_the_first_question_on_your_block": "ブロックの最初の質問を選択してください",
|
||||
"choose_where_to_run_the_survey": "フォームを実行する場所を選択してください。",
|
||||
"city": "市区町村",
|
||||
"close_survey_on_response_limit": "回答数の上限でフォームを閉じる",
|
||||
@@ -1323,10 +1326,12 @@
|
||||
"create_group": "グループを作成",
|
||||
"create_your_own_survey": "独自のフォームを作成",
|
||||
"css_selector": "CSSセレクター",
|
||||
"cta_button_label": "\"CTA\"ボタンのラベル",
|
||||
"custom_hostname": "カスタムホスト名",
|
||||
"darken_or_lighten_background_of_your_choice": "お好みの背景を暗くしたり明るくしたりします。",
|
||||
"date_format": "日付形式",
|
||||
"days_before_showing_this_survey_again": "任意のフォームが表示された後、このフォームが再表示されるまでの日数。",
|
||||
"delete_block": "ブロックを削除",
|
||||
"delete_choice": "選択肢を削除",
|
||||
"disable_the_visibility_of_survey_progress": "フォームの進捗状況の表示を無効にする。",
|
||||
"display_an_estimate_of_completion_time_for_survey": "フォームの完了時間の目安を表示",
|
||||
@@ -1338,9 +1343,12 @@
|
||||
"does_not_include_all_of": "のすべてを含まない",
|
||||
"does_not_include_one_of": "のいずれも含まない",
|
||||
"does_not_start_with": "で始まらない",
|
||||
"duplicate_block": "ブロックを複製",
|
||||
"duplicate_question": "質問を複製",
|
||||
"edit_link": "編集 リンク",
|
||||
"edit_recall": "リコールを編集",
|
||||
"edit_translations": "{lang} 翻訳を編集",
|
||||
"element_not_found": "質問が見つかりません",
|
||||
"enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "回答者がいつでも言語を切り替えられるようにします。最低2つのアクティブな言語が必要です。",
|
||||
"enable_recaptcha_to_protect_your_survey_from_spam": "スパム対策はreCAPTCHA v3を使用してスパム回答をフィルタリングします。",
|
||||
"enable_spam_protection": "スパム対策",
|
||||
@@ -1416,11 +1424,12 @@
|
||||
"hidden_field_used_in_recall": "隠し フィールド \"{hiddenField}\" が 質問 {questionIndex} で 呼び出され て います 。",
|
||||
"hidden_field_used_in_recall_ending_card": "隠し フィールド \"{hiddenField}\" が エンディング カード で 呼び出され て います。",
|
||||
"hidden_field_used_in_recall_welcome": "隠し フィールド \"{hiddenField}\" が ウェルカム カード で 呼び出され て います。",
|
||||
"hide_advanced_settings": "詳細設定を非表示",
|
||||
"hide_back_button": "「戻る」ボタンを非表示",
|
||||
"hide_back_button_description": "フォームに「戻る」ボタンを表示しない",
|
||||
"hide_block_settings": "ブロック設定を非表示",
|
||||
"hide_logo": "ロゴを非表示",
|
||||
"hide_progress_bar": "プログレスバーを非表示",
|
||||
"hide_question_settings": "質問設定を非表示",
|
||||
"hide_the_logo_in_this_specific_survey": "この特定のフォームでロゴを非表示にする",
|
||||
"hostname": "ホスト名",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "{surveyTypeDerived} フォームのカードをどれくらいユニークにしますか",
|
||||
@@ -1447,6 +1456,7 @@
|
||||
"is_clicked": "クリック済み",
|
||||
"is_completely_submitted": "完全に送信済み",
|
||||
"is_empty": "空である",
|
||||
"is_not_clicked": "未クリック",
|
||||
"is_not_empty": "空ではない",
|
||||
"is_not_set": "設定されていない",
|
||||
"is_partially_submitted": "部分的に送信済み",
|
||||
@@ -1454,7 +1464,7 @@
|
||||
"is_skipped": "スキップ済み",
|
||||
"is_submitted": "送信済み",
|
||||
"italic": "イタリック",
|
||||
"jump_to_question": "質問にジャンプ",
|
||||
"jump_to_block": "ブロックへジャンプ",
|
||||
"keep_current_order": "現在の順序を維持",
|
||||
"keep_showing_while_conditions_match": "条件が一致する間、表示し続ける",
|
||||
"key": "キー",
|
||||
@@ -1468,16 +1478,18 @@
|
||||
"logic_error_warning": "変更するとロジックエラーが発生します",
|
||||
"logic_error_warning_text": "質問の種類を変更すると、この質問のロジック条件が削除されます",
|
||||
"long_answer": "長文回答",
|
||||
"long_answer_toggle_description": "回答者が長文の複数行の回答を書けるようにします。",
|
||||
"lower_label": "下限ラベル",
|
||||
"manage_languages": "言語を管理",
|
||||
"matrix_all_fields": "すべてのフィールド",
|
||||
"matrix_rows": "行",
|
||||
"max_file_size": "最大ファイルサイズ",
|
||||
"max_file_size_limit_is": "最大ファイルサイズの上限は",
|
||||
"move_question_to_block": "質問をブロックに移動",
|
||||
"multiply": "乗算 *",
|
||||
"needed_for_self_hosted_cal_com_instance": "セルフホストのCal.comインスタンスに必要",
|
||||
"next_block": "次のブロック",
|
||||
"next_button_label": "「次へ」ボタンのラベル",
|
||||
"next_question": "次の質問",
|
||||
"no_hidden_fields_yet_add_first_one_below": "まだ非表示フィールドがありません。以下で最初のものを追加してください。",
|
||||
"no_images_found_for": "''{query}'' の画像が見つかりません",
|
||||
"no_languages_found_add_first_one_to_get_started": "言語が見つかりません。始めるには、最初のものを追加してください。",
|
||||
@@ -1589,17 +1601,17 @@
|
||||
"set_the_global_placement_in_the_look_feel_settings": "「デザイン」設定でグローバルな配置を設定します。",
|
||||
"settings_saved_successfully": "設定を正常に保存しました。",
|
||||
"seven_points": "7点",
|
||||
"show_advanced_settings": "詳細設定を表示",
|
||||
"show_block_settings": "ブロック設定を表示",
|
||||
"show_button": "ボタンを表示",
|
||||
"show_language_switch": "言語切り替えを表示",
|
||||
"show_multiple_times": "限られた回数表示する",
|
||||
"show_only_once": "一度だけ表示",
|
||||
"show_question_settings": "質問設定を表示",
|
||||
"show_survey_maximum_of": "フォームの最大表示回数",
|
||||
"show_survey_to_users": "ユーザーの {percentage}% にフォームを表示",
|
||||
"show_to_x_percentage_of_targeted_users": "ターゲットユーザーの {percentage}% に表示",
|
||||
"simple": "シンプル",
|
||||
"six_points": "6点",
|
||||
"skip_button_label": "スキップボタンのラベル",
|
||||
"smiley": "スマイリー",
|
||||
"spam_protection_note": "スパム対策は、iOS、React Native、およびAndroid SDKで表示されるフォームでは機能しません。フォームが壊れます。",
|
||||
"spam_protection_threshold_description": "値を0から1の間で設定してください。この値より低い回答は拒否されます。",
|
||||
@@ -1620,6 +1632,7 @@
|
||||
"survey_placement": "フォームの配置",
|
||||
"survey_trigger": "フォームのトリガー",
|
||||
"switch_multi_lanugage_on_to_get_started": "始めるには多言語をオンにしてください 👉",
|
||||
"target_block_not_found": "対象ブロックが見つかりません",
|
||||
"targeted": "ターゲット",
|
||||
"ten_points": "10点",
|
||||
"the_survey_will_be_shown_multiple_times_until_they_respond": "指定された回数まで、または回答があるまで表示します(どちらか先に達した方)。",
|
||||
@@ -1639,6 +1652,7 @@
|
||||
"unlock_targeting_title": "上位プランでターゲティングをアンロック",
|
||||
"unsaved_changes_warning": "フォームに未保存の変更があります。離れる前に保存しますか?",
|
||||
"until_they_submit_a_response": "回答が提出されるまで質問する",
|
||||
"untitled_block": "無題のブロック",
|
||||
"upgrade_notice_description": "多言語フォームを作成し、さらに多くの機能をアンロック",
|
||||
"upgrade_notice_title": "上位プランで多言語フォームをアンロック",
|
||||
"upload": "アップロード",
|
||||
@@ -2070,7 +2084,6 @@
|
||||
"card_abandonment_survey": "カート放棄アンケート",
|
||||
"card_abandonment_survey_description": "ウェブショップでカートが放棄される理由を理解する。",
|
||||
"card_abandonment_survey_question_1_button_label": "はい、お願いします!",
|
||||
"card_abandonment_survey_question_1_dismiss_button_label": "いいえ、結構です。",
|
||||
"card_abandonment_survey_question_1_headline": "改善のために2分お時間をいただけますか?",
|
||||
"card_abandonment_survey_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>カートに商品が残っているようです。理由をぜひ教えてください。</span></p>",
|
||||
"card_abandonment_survey_question_2_choice_1": "高い配送料",
|
||||
@@ -2157,12 +2170,10 @@
|
||||
"churn_survey_question_2_button_label": "送信",
|
||||
"churn_survey_question_2_headline": "$[projectName]をより使いやすくするにはどうすればよかったですか?",
|
||||
"churn_survey_question_3_button_label": "30%オフを取得",
|
||||
"churn_survey_question_3_dismiss_button_label": "スキップ",
|
||||
"churn_survey_question_3_headline": "来年1年間30%オフ!",
|
||||
"churn_survey_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>引き続きお客様でいていただきたいです。来年1年間30%の割引を提供します。</span></p>",
|
||||
"churn_survey_question_4_headline": "どのような機能が不足していますか?",
|
||||
"churn_survey_question_5_button_label": "CEOにメールを送信",
|
||||
"churn_survey_question_5_dismiss_button_label": "スキップ",
|
||||
"churn_survey_question_5_headline": "お聞きして申し訳ありません😔 直接CEOとお話しください!",
|
||||
"churn_survey_question_5_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>私たちは可能な限り最高のカスタマーサービスを提供することを目指しています。弊社のCEOにメールを送っていただければ、彼女が個人的に対応します。</span></p>",
|
||||
"collect_feedback_description": "あなたの製品やサービスに関する包括的なフィードバックを収集する。",
|
||||
@@ -2269,6 +2280,7 @@
|
||||
"csat_survey_question_3_headline": "申し訳ありません!体験を改善するために何かできることはありますか?",
|
||||
"csat_survey_question_3_placeholder": "ここに回答を入力してください...",
|
||||
"cta_description": "情報を表示し、特定の行動を促す",
|
||||
"custom_survey_block_1_name": "ブロック1",
|
||||
"custom_survey_description": "テンプレートを使わずにアンケートを作成する。",
|
||||
"custom_survey_name": "最初から始める",
|
||||
"custom_survey_question_1_headline": "何を知りたいですか?",
|
||||
@@ -2355,7 +2367,6 @@
|
||||
"evaluate_a_product_idea_description": "製品や機能のアイデアについてユーザーにアンケートをとる。迅速にフィードバックを得る。",
|
||||
"evaluate_a_product_idea_name": "製品アイデアの評価",
|
||||
"evaluate_a_product_idea_question_1_button_label": "ぜひ!",
|
||||
"evaluate_a_product_idea_question_1_dismiss_button_label": "スキップ",
|
||||
"evaluate_a_product_idea_question_1_headline": "あなたは$[projectName]の熱心なユーザーですね!機能のアイデアについて少しお話しいただけませんか?1分ほどお時間ありますか?",
|
||||
"evaluate_a_product_idea_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>あなたの時間を尊重し、短くしました。🤷</span></p>",
|
||||
"evaluate_a_product_idea_question_2_headline": "ありがとうございます!今日、[PROBLEM AREA]はあなたにとってどれくらい難しい、または簡単ですか?",
|
||||
@@ -2364,7 +2375,6 @@
|
||||
"evaluate_a_product_idea_question_3_headline": "[PROBLEM AREA]について、あなたにとって最も難しいことは何ですか?",
|
||||
"evaluate_a_product_idea_question_3_placeholder": "ここに回答を入力してください...",
|
||||
"evaluate_a_product_idea_question_4_button_label": "次へ",
|
||||
"evaluate_a_product_idea_question_4_dismiss_button_label": "スキップ",
|
||||
"evaluate_a_product_idea_question_4_headline": "私たちは[PROBLEM AREA]を助けるためのアイデアに取り組んでいます。",
|
||||
"evaluate_a_product_idea_question_4_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>ここにコンセプトの概要を挿入してください。必要な詳細を加えて、簡潔で理解しやすいものにしてください。</span></p>",
|
||||
"evaluate_a_product_idea_question_5_headline": "この機能はあなたにとってどのくらい価値がありますか?",
|
||||
@@ -2414,7 +2424,6 @@
|
||||
"feedback_box_question_2_headline": "何が壊れていますか?",
|
||||
"feedback_box_question_2_subheader": "詳細なほど良いです :)",
|
||||
"feedback_box_question_3_button_label": "はい、通知してください",
|
||||
"feedback_box_question_3_dismiss_button_label": "いいえ、結構です",
|
||||
"feedback_box_question_3_headline": "最新情報を知りたいですか?",
|
||||
"feedback_box_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>これをできるだけ早く修正します。修正されたら通知を希望しますか?</span></p>",
|
||||
"feedback_box_question_4_button_label": "機能をリクエスト",
|
||||
@@ -2439,7 +2448,6 @@
|
||||
"identify_sign_up_barriers_description": "サインアップの障壁に関する洞察を得るために割引を提供する。",
|
||||
"identify_sign_up_barriers_name": "サインアップの障壁を特定する",
|
||||
"identify_sign_up_barriers_question_1_button_label": "10%割引を取得",
|
||||
"identify_sign_up_barriers_question_1_dismiss_button_label": "いいえ、結構です",
|
||||
"identify_sign_up_barriers_question_1_headline": "この短いアンケートに答えて、10%オフをゲット!",
|
||||
"identify_sign_up_barriers_question_1_html": "あなたはサインアップを検討しているようですね。4つの質問に答えて、どのプランでも10%オフを獲得しましょう。",
|
||||
"identify_sign_up_barriers_question_2_headline": "$[projectName]にサインアップする可能性はどのくらいありますか?",
|
||||
@@ -2462,7 +2470,6 @@
|
||||
"identify_sign_up_barriers_question_8_headline": "説明してください:",
|
||||
"identify_sign_up_barriers_question_8_placeholder": "ここに回答を入力してください...",
|
||||
"identify_sign_up_barriers_question_9_button_label": "サインアップ",
|
||||
"identify_sign_up_barriers_question_9_dismiss_button_label": "今はスキップ",
|
||||
"identify_sign_up_barriers_question_9_headline": "ありがとうございます!コードはこちら:SIGNUPNOW10",
|
||||
"identify_sign_up_barriers_question_9_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>フィードバックを共有していただき、誠にありがとうございます 🙏</span></p>",
|
||||
"identify_upsell_opportunities_description": "あなたの製品がユーザーの時間をどのくらい節約しているかを見つける。アップセルに活用する。",
|
||||
@@ -2499,7 +2506,6 @@
|
||||
"improve_newsletter_content_question_2_headline": "今週のニュースレターをより役立つものにするにはどうすればよかったですか?",
|
||||
"improve_newsletter_content_question_2_placeholder": "ここに回答を入力してください...",
|
||||
"improve_newsletter_content_question_3_button_label": "喜んでお手伝いします!",
|
||||
"improve_newsletter_content_question_3_dismiss_button_label": "自分で友達を探してください",
|
||||
"improve_newsletter_content_question_3_headline": "ありがとう!❤️ 友達一人と愛を分かち合ってください。",
|
||||
"improve_newsletter_content_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>あなたと同じように考える人は誰ですか?今週のエピソードをあなたの親友と共有していただけると、私たちにとって非常に大きな助けになります!</span></p>",
|
||||
"improve_trial_conversion_description": "人々が試用期間を中止した理由を見つける。これらの洞察はファネルの改善に役立つ。",
|
||||
@@ -2514,7 +2520,6 @@
|
||||
"improve_trial_conversion_question_2_button_label": "次へ",
|
||||
"improve_trial_conversion_question_2_headline": "残念です。$[projectName]を使う上で最も大きな問題は何でしたか?",
|
||||
"improve_trial_conversion_question_4_button_label": "20%オフを取得",
|
||||
"improve_trial_conversion_question_4_dismiss_button_label": "スキップ",
|
||||
"improve_trial_conversion_question_4_headline": "残念です!初年度20%オフをゲット。",
|
||||
"improve_trial_conversion_question_4_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>年間プランで20%の割引を提供させていただきます。</span></p>",
|
||||
"improve_trial_conversion_question_5_button_label": "次へ",
|
||||
@@ -2678,7 +2683,6 @@
|
||||
"preview_survey_question_2_choice_2_label": "いいえ、結構です!",
|
||||
"preview_survey_question_2_headline": "最新情報を知りたいですか?",
|
||||
"preview_survey_welcome_card_headline": "ようこそ!",
|
||||
"preview_survey_welcome_card_html": "フィードバックありがとうございます - さあ、始めましょう!",
|
||||
"prioritize_features_description": "ユーザーが最も必要とする機能と最も必要としない機能を特定する。",
|
||||
"prioritize_features_name": "機能の優先順位付け",
|
||||
"prioritize_features_question_1_choice_1": "機能1",
|
||||
@@ -2704,7 +2708,6 @@
|
||||
"product_market_fit_superhuman": "プロダクトマーケットフィット(スーパーヒューマン)",
|
||||
"product_market_fit_superhuman_description": "製品がなくなったらユーザーがどれだけがっかりするかを評価することで、PMFを測定する。",
|
||||
"product_market_fit_superhuman_question_1_button_label": "喜んでお手伝いします!",
|
||||
"product_market_fit_superhuman_question_1_dismiss_button_label": "いいえ、結構です。",
|
||||
"product_market_fit_superhuman_question_1_headline": "あなたは私たちのパワーユーザーの一人です!5分お時間ありますか?",
|
||||
"product_market_fit_superhuman_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>あなたのユーザー体験についてもっと理解したいです。あなたの洞察は非常に役立ちます。</span></p>",
|
||||
"product_market_fit_superhuman_question_2_choice_1": "全くがっかりしない",
|
||||
@@ -2810,7 +2813,6 @@
|
||||
"site_abandonment_survey_description": "ウェブショップでサイトが放棄される理由を理解する。",
|
||||
"site_abandonment_survey_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>購入せずにサイトを離れようとしているようです。理由をぜひ教えてください。</span></p>",
|
||||
"site_abandonment_survey_question_2_button_label": "はい、お願いします!",
|
||||
"site_abandonment_survey_question_2_dismiss_button_label": "いいえ、結構です。",
|
||||
"site_abandonment_survey_question_2_headline": "少しお時間ありますか?",
|
||||
"site_abandonment_survey_question_3_choice_1": "探しているものが見つからない",
|
||||
"site_abandonment_survey_question_3_choice_2": "より良いサイトを見つけた",
|
||||
@@ -2835,7 +2837,6 @@
|
||||
"site_abandonment_survey_question_7_label": "はい、連絡を希望します。",
|
||||
"site_abandonment_survey_question_8_headline": "メールアドレスを教えてください:",
|
||||
"site_abandonment_survey_question_9_headline": "他に何かコメントや提案はありますか?",
|
||||
"skip": "スキップ",
|
||||
"smileys_survey_name": "スマイリーアンケート",
|
||||
"smileys_survey_question_1_headline": "$[projectName]は好きですか?",
|
||||
"smileys_survey_question_1_lower_label": "良くない",
|
||||
|
||||
+24
-23
@@ -1206,9 +1206,9 @@
|
||||
"add": "Voeg + toe",
|
||||
"add_a_delay_or_auto_close_the_survey": "Voeg een vertraging toe of sluit de enquête automatisch",
|
||||
"add_a_four_digit_pin": "Voeg een viercijferige pincode toe",
|
||||
"add_a_new_question_to_your_survey": "Voeg een nieuwe vraag toe aan uw enquête",
|
||||
"add_a_variable_to_calculate": "Voeg een variabele toe om te berekenen",
|
||||
"add_action_below": "Voeg hieronder een actie toe",
|
||||
"add_block": "Blok toevoegen",
|
||||
"add_choice_below": "Voeg hieronder keuze toe",
|
||||
"add_color_coding": "Kleurcodering toevoegen",
|
||||
"add_color_coding_description": "Voeg rode, oranje en groene kleurcodes toe aan de opties.",
|
||||
@@ -1229,8 +1229,8 @@
|
||||
"add_other": "Voeg 'Anders' toe",
|
||||
"add_photo_or_video": "Voeg foto of video toe",
|
||||
"add_pin": "Pincode toevoegen",
|
||||
"add_question": "Vraag toevoegen",
|
||||
"add_question_below": "Voeg hieronder een vraag toe",
|
||||
"add_question_to_block": "Vraag aan blok toevoegen",
|
||||
"add_row": "Rij toevoegen",
|
||||
"add_variable": "Variabele toevoegen",
|
||||
"address_fields": "Adresvelden",
|
||||
@@ -1256,12 +1256,14 @@
|
||||
"automatically_mark_the_survey_as_complete_after": "Markeer de enquête daarna automatisch als voltooid",
|
||||
"back_button_label": "Knoplabel 'Terug'",
|
||||
"background_styling": "Achtergrondstyling",
|
||||
"block_deleted": "Blok verwijderd.",
|
||||
"block_duplicated": "Blok gedupliceerd.",
|
||||
"bold": "Vetgedrukt",
|
||||
"brand_color": "Merk kleur",
|
||||
"brightness": "Helderheid",
|
||||
"button_external": "Externe link inschakelen",
|
||||
"button_external_description": "Voeg een knop toe die een externe URL in een nieuw tabblad opent",
|
||||
"button_label": "Knoplabel",
|
||||
"button_to_continue_in_survey": "Knop om door te gaan in de enquête",
|
||||
"button_to_link_to_external_url": "Knop om te linken naar externe URL",
|
||||
"button_url": "Knop-URL",
|
||||
"cal_username": "Cal.com-gebruikersnaam of gebruikersnaam/evenement",
|
||||
"calculate": "Berekenen",
|
||||
@@ -1300,6 +1302,7 @@
|
||||
"character_limit_toggle_title": "Tekenlimieten toevoegen",
|
||||
"checkbox_label": "Selectievakje-label",
|
||||
"choose_the_actions_which_trigger_the_survey": "Kies de acties die de enquête activeren.",
|
||||
"choose_the_first_question_on_your_block": "Kies de eerste vraag in je blok",
|
||||
"choose_where_to_run_the_survey": "Kies waar u de enquête wilt uitvoeren.",
|
||||
"city": "Stad",
|
||||
"close_survey_on_response_limit": "Sluit enquête over responslimiet",
|
||||
@@ -1323,10 +1326,12 @@
|
||||
"create_group": "Groep aanmaken",
|
||||
"create_your_own_survey": "Creëer uw eigen enquête",
|
||||
"css_selector": "CSS-kiezer",
|
||||
"cta_button_label": "\"CTA\" knoplabel",
|
||||
"custom_hostname": "Aangepaste hostnaam",
|
||||
"darken_or_lighten_background_of_your_choice": "Maak de achtergrond naar keuze donkerder of lichter.",
|
||||
"date_format": "Datumformaat",
|
||||
"days_before_showing_this_survey_again": "dagen nadat een enquête is getoond voordat deze enquête kan verschijnen.",
|
||||
"delete_block": "Blok verwijderen",
|
||||
"delete_choice": "Keuze verwijderen",
|
||||
"disable_the_visibility_of_survey_progress": "Schakel de zichtbaarheid van de voortgang van het onderzoek uit.",
|
||||
"display_an_estimate_of_completion_time_for_survey": "Geef een schatting weer van de voltooiingstijd voor het onderzoek",
|
||||
@@ -1338,9 +1343,12 @@
|
||||
"does_not_include_all_of": "Omvat niet alles",
|
||||
"does_not_include_one_of": "Bevat niet een van",
|
||||
"does_not_start_with": "Begint niet met",
|
||||
"duplicate_block": "Blok dupliceren",
|
||||
"duplicate_question": "Vraag dupliceren",
|
||||
"edit_link": "Link bewerken",
|
||||
"edit_recall": "Bewerken Terugroepen",
|
||||
"edit_translations": "Bewerk {lang} vertalingen",
|
||||
"element_not_found": "Vraag niet gevonden",
|
||||
"enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Sta respondenten toe om op elk moment van taal te wisselen. Vereist min. 2 actieve talen.",
|
||||
"enable_recaptcha_to_protect_your_survey_from_spam": "Spambeveiliging maakt gebruik van reCAPTCHA v3 om de spamreacties eruit te filteren.",
|
||||
"enable_spam_protection": "Spambescherming",
|
||||
@@ -1416,11 +1424,12 @@
|
||||
"hidden_field_used_in_recall": "Verborgen veld \"{hiddenField}\" wordt opgeroepen in vraag {questionIndex}.",
|
||||
"hidden_field_used_in_recall_ending_card": "Verborgen veld \"{hiddenField}\" wordt opgeroepen in de eindkaart",
|
||||
"hidden_field_used_in_recall_welcome": "Verborgen veld \"{hiddenField}\" wordt opgeroepen in de welkomstkaart.",
|
||||
"hide_advanced_settings": "Geavanceerde instellingen verbergen",
|
||||
"hide_back_button": "Knop 'Terug' verbergen",
|
||||
"hide_back_button_description": "Geef de terugknop niet weer in de enquête",
|
||||
"hide_block_settings": "Blokinstellingen verbergen",
|
||||
"hide_logo": "Logo verbergen",
|
||||
"hide_progress_bar": "Voortgangsbalk verbergen",
|
||||
"hide_question_settings": "Vraaginstellingen verbergen",
|
||||
"hide_the_logo_in_this_specific_survey": "Verberg het logo in deze specifieke enquête",
|
||||
"hostname": "Hostnaam",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Hoe funky wil je je kaarten hebben in {surveyTypeDerived} Enquêtes",
|
||||
@@ -1447,6 +1456,7 @@
|
||||
"is_clicked": "Er wordt geklikt",
|
||||
"is_completely_submitted": "Is volledig ingediend",
|
||||
"is_empty": "Is leeg",
|
||||
"is_not_clicked": "Er wordt niet geklikt",
|
||||
"is_not_empty": "Is niet leeg",
|
||||
"is_not_set": "Is niet ingesteld",
|
||||
"is_partially_submitted": "Is gedeeltelijk ingediend",
|
||||
@@ -1454,7 +1464,7 @@
|
||||
"is_skipped": "Wordt overgeslagen",
|
||||
"is_submitted": "Wordt ingediend",
|
||||
"italic": "Cursief",
|
||||
"jump_to_question": "Ga naar de vraag",
|
||||
"jump_to_block": "Spring naar blok",
|
||||
"keep_current_order": "Huidige bestelling behouden",
|
||||
"keep_showing_while_conditions_match": "Blijf weergeven zolang de omstandigheden overeenkomen",
|
||||
"key": "Sleutel",
|
||||
@@ -1468,16 +1478,18 @@
|
||||
"logic_error_warning": "Wijzigen zal logische fouten veroorzaken",
|
||||
"logic_error_warning_text": "Als u het vraagtype wijzigt, worden de logische voorwaarden van deze vraag verwijderd",
|
||||
"long_answer": "Lang antwoord",
|
||||
"long_answer_toggle_description": "Sta respondenten toe om langere antwoorden met meerdere regels te schrijven.",
|
||||
"lower_label": "Lager etiket",
|
||||
"manage_languages": "Beheer talen",
|
||||
"matrix_all_fields": "Alle velden",
|
||||
"matrix_rows": "Rijen",
|
||||
"max_file_size": "Maximale bestandsgrootte",
|
||||
"max_file_size_limit_is": "De maximale bestandsgrootte is",
|
||||
"move_question_to_block": "Vraag naar blok verplaatsen",
|
||||
"multiply": "Vermenigvuldig *",
|
||||
"needed_for_self_hosted_cal_com_instance": "Nodig voor een zelf-gehoste Cal.com-instantie",
|
||||
"next_block": "Volgend blok",
|
||||
"next_button_label": "Knoplabel 'Volgende'",
|
||||
"next_question": "Volgende vraag",
|
||||
"no_hidden_fields_yet_add_first_one_below": "Nog geen verborgen velden. Voeg de eerste hieronder toe.",
|
||||
"no_images_found_for": "Geen afbeeldingen gevonden voor ''{query}'",
|
||||
"no_languages_found_add_first_one_to_get_started": "Geen talen gevonden. Voeg de eerste toe om aan de slag te gaan.",
|
||||
@@ -1589,17 +1601,17 @@
|
||||
"set_the_global_placement_in_the_look_feel_settings": "Stel de globale plaatsing in de Look & Feel-instellingen in.",
|
||||
"settings_saved_successfully": "Instellingen succesvol opgeslagen.",
|
||||
"seven_points": "7 punten",
|
||||
"show_advanced_settings": "Toon geavanceerde instellingen",
|
||||
"show_block_settings": "Blokinstellingen tonen",
|
||||
"show_button": "Toon knop",
|
||||
"show_language_switch": "Toon taalwissel",
|
||||
"show_multiple_times": "Toon een beperkt aantal keren",
|
||||
"show_only_once": "Slechts één keer weergeven",
|
||||
"show_question_settings": "Vraaginstellingen tonen",
|
||||
"show_survey_maximum_of": "Toon onderzoek maximaal",
|
||||
"show_survey_to_users": "Enquête tonen aan % van de gebruikers",
|
||||
"show_to_x_percentage_of_targeted_users": "Toon aan {percentage}% van de getargete gebruikers",
|
||||
"simple": "Eenvoudig",
|
||||
"six_points": "6 punten",
|
||||
"skip_button_label": "Knoplabel overslaan",
|
||||
"smiley": "Smiley",
|
||||
"spam_protection_note": "Spambeveiliging werkt niet voor enquêtes die worden weergegeven met de iOS-, React Native- en Android SDK's. Het zal de enquête breken.",
|
||||
"spam_protection_threshold_description": "Stel een waarde in tussen 0 en 1, reacties onder deze waarde worden afgewezen.",
|
||||
@@ -1620,6 +1632,7 @@
|
||||
"survey_placement": "Enquête plaatsing",
|
||||
"survey_trigger": "Enquêtetrigger",
|
||||
"switch_multi_lanugage_on_to_get_started": "Schakel meertalen in om aan de slag te gaan 👉",
|
||||
"target_block_not_found": "Doelblok niet gevonden",
|
||||
"targeted": "Gericht",
|
||||
"ten_points": "10 punten",
|
||||
"the_survey_will_be_shown_multiple_times_until_they_respond": "Toon maximaal het opgegeven aantal keren, of totdat ze reageren (wat het eerst komt).",
|
||||
@@ -1639,6 +1652,7 @@
|
||||
"unlock_targeting_title": "Ontgrendel targeting met een hoger plan",
|
||||
"unsaved_changes_warning": "Er zijn niet-opgeslagen wijzigingen in uw enquête. Wilt u ze bewaren voordat u vertrekt?",
|
||||
"until_they_submit_a_response": "Vraag totdat ze een reactie indienen",
|
||||
"untitled_block": "Naamloos blok",
|
||||
"upgrade_notice_description": "Creëer meertalige enquêtes en ontgrendel nog veel meer functies",
|
||||
"upgrade_notice_title": "Ontgrendel meertalige enquêtes met een hoger plan",
|
||||
"upload": "Uploaden",
|
||||
@@ -2070,7 +2084,6 @@
|
||||
"card_abandonment_survey": "Enquête over verlaten winkelwagen",
|
||||
"card_abandonment_survey_description": "Begrijp de redenen achter het verlaten van uw winkelwagentje in uw webshop.",
|
||||
"card_abandonment_survey_question_1_button_label": "Zeker!",
|
||||
"card_abandonment_survey_question_1_dismiss_button_label": "Nee, bedankt.",
|
||||
"card_abandonment_survey_question_1_headline": "Heeft u 2 minuten om ons te helpen verbeteren?",
|
||||
"card_abandonment_survey_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We hebben gemerkt dat u een aantal artikelen in uw winkelwagen heeft achtergelaten. We willen graag begrijpen waarom.</span></p>",
|
||||
"card_abandonment_survey_question_2_choice_1": "Hoge verzendkosten",
|
||||
@@ -2157,12 +2170,10 @@
|
||||
"churn_survey_question_2_button_label": "Versturen",
|
||||
"churn_survey_question_2_headline": "Wat zou $[projectName] gebruiksvriendelijker hebben gemaakt?",
|
||||
"churn_survey_question_3_button_label": "Krijg 30% korting",
|
||||
"churn_survey_question_3_dismiss_button_label": "Overslaan",
|
||||
"churn_survey_question_3_headline": "Profiteer volgend jaar van 30% korting!",
|
||||
"churn_survey_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We willen u graag als klant behouden. Graag bieden wij u het komende jaar 30% korting aan.</span></p>",
|
||||
"churn_survey_question_4_headline": "Welke functies mis je?",
|
||||
"churn_survey_question_5_button_label": "Stuur een e-mail naar de CEO",
|
||||
"churn_survey_question_5_dismiss_button_label": "Overslaan",
|
||||
"churn_survey_question_5_headline": "Sorry om te horen 😔 Praat rechtstreeks met onze CEO!",
|
||||
"churn_survey_question_5_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Wij streven ernaar de best mogelijke klantenservice te bieden. Stuur een e-mail naar onze CEO en zij zal uw probleem persoonlijk behandelen.</span></p>",
|
||||
"collect_feedback_description": "Verzamel uitgebreide feedback over uw product of dienst.",
|
||||
@@ -2269,6 +2280,7 @@
|
||||
"csat_survey_question_3_headline": "Euh, sorry! Kunnen we iets doen om uw ervaring te verbeteren?",
|
||||
"csat_survey_question_3_placeholder": "Typ hier uw antwoord...",
|
||||
"cta_description": "Geef informatie weer en vraag gebruikers om een specifieke actie te ondernemen",
|
||||
"custom_survey_block_1_name": "Blok 1",
|
||||
"custom_survey_description": "Maak een enquête zonder sjabloon.",
|
||||
"custom_survey_name": "Begin helemaal opnieuw",
|
||||
"custom_survey_question_1_headline": "Wat zou je willen weten?",
|
||||
@@ -2355,7 +2367,6 @@
|
||||
"evaluate_a_product_idea_description": "Enquête gebruikers over product- of functie-ideeën. Krijg snel feedback.",
|
||||
"evaluate_a_product_idea_name": "Evalueer een productidee",
|
||||
"evaluate_a_product_idea_question_1_button_label": "Laten we het doen!",
|
||||
"evaluate_a_product_idea_question_1_dismiss_button_label": "Overslaan",
|
||||
"evaluate_a_product_idea_question_1_headline": "We vinden het geweldig hoe je $[projectName] gebruikt! We willen graag uw mening geven over een functie-idee. Heb je een minuut?",
|
||||
"evaluate_a_product_idea_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We respecteren uw tijd en hebben deze kort gehouden 🤸</span></p>",
|
||||
"evaluate_a_product_idea_question_2_headline": "Bedankt! Hoe moeilijk of gemakkelijk is het voor jou om vandaag [PROBLEM AREA] te zijn?",
|
||||
@@ -2364,7 +2375,6 @@
|
||||
"evaluate_a_product_idea_question_3_headline": "Wat is het moeilijkst voor jou als het gaat om [PROBLEM AREA]?",
|
||||
"evaluate_a_product_idea_question_3_placeholder": "Typ hier uw antwoord...",
|
||||
"evaluate_a_product_idea_question_4_button_label": "Volgende",
|
||||
"evaluate_a_product_idea_question_4_dismiss_button_label": "Overslaan",
|
||||
"evaluate_a_product_idea_question_4_headline": "We werken aan een idee om te helpen met [PROBLEM AREA].",
|
||||
"evaluate_a_product_idea_question_4_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Voeg hier een conceptoverzicht in. Voeg de nodige details toe, maar houd het beknopt en gemakkelijk te begrijpen.</span></p>",
|
||||
"evaluate_a_product_idea_question_5_headline": "Hoe waardevol zou deze functie voor jou zijn?",
|
||||
@@ -2414,7 +2424,6 @@
|
||||
"feedback_box_question_2_headline": "Wat is er kapot?",
|
||||
"feedback_box_question_2_subheader": "Hoe gedetailleerder, hoe beter :)",
|
||||
"feedback_box_question_3_button_label": "Ja, breng mij op de hoogte",
|
||||
"feedback_box_question_3_dismiss_button_label": "Nee, bedankt",
|
||||
"feedback_box_question_3_headline": "Wil je op de hoogte blijven?",
|
||||
"feedback_box_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We zullen dit zo snel mogelijk oplossen. Wil je op de hoogte worden gehouden wanneer we dat hebben gedaan?</span></p>",
|
||||
"feedback_box_question_4_button_label": "Functie aanvragen",
|
||||
@@ -2439,7 +2448,6 @@
|
||||
"identify_sign_up_barriers_description": "Bied een korting aan om inzicht te krijgen in de aanmeldingsbarrières.",
|
||||
"identify_sign_up_barriers_name": "Identificeer aanmeldingsbarrières",
|
||||
"identify_sign_up_barriers_question_1_button_label": "Krijg 10% korting",
|
||||
"identify_sign_up_barriers_question_1_dismiss_button_label": "Nee, bedankt",
|
||||
"identify_sign_up_barriers_question_1_headline": "Beantwoord deze korte enquête en ontvang 10% korting!",
|
||||
"identify_sign_up_barriers_question_1_html": "Het lijkt erop dat je overweegt om je aan te melden. Beantwoord vier vragen en ontvang 10% op elk abonnement.",
|
||||
"identify_sign_up_barriers_question_2_headline": "Hoe waarschijnlijk is het dat u zich aanmeldt voor $[projectName]?",
|
||||
@@ -2462,7 +2470,6 @@
|
||||
"identify_sign_up_barriers_question_8_headline": "Leg uit:",
|
||||
"identify_sign_up_barriers_question_8_placeholder": "Typ hier uw antwoord...",
|
||||
"identify_sign_up_barriers_question_9_button_label": "Aanmelden",
|
||||
"identify_sign_up_barriers_question_9_dismiss_button_label": "Overslaan voor nu",
|
||||
"identify_sign_up_barriers_question_9_headline": "Bedankt! Hier is uw code: SIGNUPNOW10",
|
||||
"identify_sign_up_barriers_question_9_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Hartelijk bedankt dat u de tijd heeft genomen om feedback te delen 🙏</span></p>",
|
||||
"identify_upsell_opportunities_description": "Ontdek hoeveel tijd uw product uw gebruiker bespaart. Gebruik het om te verkopen.",
|
||||
@@ -2499,7 +2506,6 @@
|
||||
"improve_newsletter_content_question_2_headline": "Wat zou de nieuwsbrief van deze week nuttiger hebben gemaakt?",
|
||||
"improve_newsletter_content_question_2_placeholder": "Typ hier uw antwoord...",
|
||||
"improve_newsletter_content_question_3_button_label": "Graag helpen!",
|
||||
"improve_newsletter_content_question_3_dismiss_button_label": "Vind je eigen vrienden",
|
||||
"improve_newsletter_content_question_3_headline": "Bedankt! ❤️ Verspreid de liefde met EEN vriend.",
|
||||
"improve_newsletter_content_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Wie denkt er zoals jij? Je zou ons een groot plezier doen als je de aflevering van deze week met je hersenvriend zou willen delen!</span></p>",
|
||||
"improve_trial_conversion_description": "Ontdek waarom mensen hun proces hebben stopgezet. Met deze inzichten kunt u uw trechter verbeteren.",
|
||||
@@ -2514,7 +2520,6 @@
|
||||
"improve_trial_conversion_question_2_button_label": "Volgende",
|
||||
"improve_trial_conversion_question_2_headline": "Sorry om te horen. Wat was het grootste probleem bij het gebruik van $[projectName]?",
|
||||
"improve_trial_conversion_question_4_button_label": "Krijg 20% korting",
|
||||
"improve_trial_conversion_question_4_dismiss_button_label": "Overslaan",
|
||||
"improve_trial_conversion_question_4_headline": "Sorry om te horen! Krijg het eerste jaar 20% korting.",
|
||||
"improve_trial_conversion_question_4_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We bieden u graag 20% korting op een jaarabonnement.</span></p>",
|
||||
"improve_trial_conversion_question_5_button_label": "Volgende",
|
||||
@@ -2678,7 +2683,6 @@
|
||||
"preview_survey_question_2_choice_2_label": "Nee, dank je!",
|
||||
"preview_survey_question_2_headline": "Wil je op de hoogte blijven?",
|
||||
"preview_survey_welcome_card_headline": "Welkom!",
|
||||
"preview_survey_welcome_card_html": "Bedankt voor het geven van uw feedback - laten we gaan!",
|
||||
"prioritize_features_description": "Identificeer functies die uw gebruikers het meest en het minst nodig hebben.",
|
||||
"prioritize_features_name": "Geef prioriteit aan functies",
|
||||
"prioritize_features_question_1_choice_1": "Kenmerk 1",
|
||||
@@ -2704,7 +2708,6 @@
|
||||
"product_market_fit_superhuman": "Productmarktfit (bovenmenselijk)",
|
||||
"product_market_fit_superhuman_description": "Meet PMF door te beoordelen hoe teleurgesteld gebruikers zouden zijn als uw product zou verdwijnen.",
|
||||
"product_market_fit_superhuman_question_1_button_label": "Graag helpen!",
|
||||
"product_market_fit_superhuman_question_1_dismiss_button_label": "Nee, bedankt.",
|
||||
"product_market_fit_superhuman_question_1_headline": "Jij bent een van onze hoofdgebruikers! Heb je 5 minuten?",
|
||||
"product_market_fit_superhuman_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We willen graag uw gebruikerservaring beter begrijpen. Het delen van uw inzichten helpt enorm.</span></p>",
|
||||
"product_market_fit_superhuman_question_2_choice_1": "Helemaal niet teleurgesteld",
|
||||
@@ -2810,7 +2813,6 @@
|
||||
"site_abandonment_survey_description": "Begrijp de redenen achter het verlaten van de site in uw webshop.",
|
||||
"site_abandonment_survey_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We hebben gemerkt dat u onze site verlaat zonder een aankoop te doen. We willen graag begrijpen waarom.</span></p>",
|
||||
"site_abandonment_survey_question_2_button_label": "Zeker!",
|
||||
"site_abandonment_survey_question_2_dismiss_button_label": "Nee, bedankt.",
|
||||
"site_abandonment_survey_question_2_headline": "Heb je even?",
|
||||
"site_abandonment_survey_question_3_choice_1": "Kan niet vinden wat ik zoek",
|
||||
"site_abandonment_survey_question_3_choice_2": "Een betere site gevonden",
|
||||
@@ -2835,7 +2837,6 @@
|
||||
"site_abandonment_survey_question_7_label": "Ja, neem alstublieft contact op.",
|
||||
"site_abandonment_survey_question_8_headline": "Deel alstublieft uw e-mailadres:",
|
||||
"site_abandonment_survey_question_9_headline": "Nog aanvullende opmerkingen of suggesties?",
|
||||
"skip": "Overslaan",
|
||||
"smileys_survey_name": "Smileys-enquête",
|
||||
"smileys_survey_question_1_headline": "Wat vind je van $[projectName]?",
|
||||
"smileys_survey_question_1_lower_label": "Niet goed",
|
||||
|
||||
+24
-23
@@ -1206,9 +1206,9 @@
|
||||
"add": "Adicionar +",
|
||||
"add_a_delay_or_auto_close_the_survey": "Adicione um atraso ou feche a pesquisa automaticamente",
|
||||
"add_a_four_digit_pin": "Adicione um PIN de quatro dígitos",
|
||||
"add_a_new_question_to_your_survey": "Adicionar uma nova pergunta à sua pesquisa",
|
||||
"add_a_variable_to_calculate": "Adicione uma variável para calcular",
|
||||
"add_action_below": "Adicionar ação abaixo",
|
||||
"add_block": "Adicionar bloco",
|
||||
"add_choice_below": "Adicionar opção abaixo",
|
||||
"add_color_coding": "Adicionar codificação por cores",
|
||||
"add_color_coding_description": "Adicione os códigos de cores vermelho, laranja e verde às opções.",
|
||||
@@ -1229,8 +1229,8 @@
|
||||
"add_other": "Adicionar \"Outro",
|
||||
"add_photo_or_video": "Adicionar foto ou video",
|
||||
"add_pin": "Adicionar PIN",
|
||||
"add_question": "Adicionar pergunta",
|
||||
"add_question_below": "Adicione a pergunta abaixo",
|
||||
"add_question_to_block": "Adicionar pergunta ao bloco",
|
||||
"add_row": "Adicionar linha",
|
||||
"add_variable": "Adicionar variável",
|
||||
"address_fields": "Campos de Endereço",
|
||||
@@ -1256,12 +1256,14 @@
|
||||
"automatically_mark_the_survey_as_complete_after": "Marcar automaticamente a pesquisa como concluída após",
|
||||
"back_button_label": "Voltar",
|
||||
"background_styling": "Estilo de Fundo",
|
||||
"block_deleted": "Bloco excluído.",
|
||||
"block_duplicated": "Bloco duplicado.",
|
||||
"bold": "Negrito",
|
||||
"brand_color": "Cor da marca",
|
||||
"brightness": "brilho",
|
||||
"button_external": "Habilitar link externo",
|
||||
"button_external_description": "Adicionar um botão que abre uma URL externa em uma nova aba",
|
||||
"button_label": "Rótulo do Botão",
|
||||
"button_to_continue_in_survey": "Botão para continuar na pesquisa",
|
||||
"button_to_link_to_external_url": "Botão para link externo",
|
||||
"button_url": "URL do Botão",
|
||||
"cal_username": "Nome de usuário do Cal.com ou nome de usuário/evento",
|
||||
"calculate": "Calcular",
|
||||
@@ -1300,6 +1302,7 @@
|
||||
"character_limit_toggle_title": "Adicionar limites de caracteres",
|
||||
"checkbox_label": "Rótulo da Caixa de Seleção",
|
||||
"choose_the_actions_which_trigger_the_survey": "Escolha as ações que disparam a pesquisa.",
|
||||
"choose_the_first_question_on_your_block": "Escolha a primeira pergunta do seu bloco",
|
||||
"choose_where_to_run_the_survey": "Escolha onde realizar a pesquisa.",
|
||||
"city": "cidade",
|
||||
"close_survey_on_response_limit": "Fechar pesquisa ao atingir limite de respostas",
|
||||
@@ -1323,10 +1326,12 @@
|
||||
"create_group": "Criar grupo",
|
||||
"create_your_own_survey": "Crie sua própria pesquisa",
|
||||
"css_selector": "Seletor CSS",
|
||||
"cta_button_label": "Rótulo do botão \"CTA\"",
|
||||
"custom_hostname": "Hostname personalizado",
|
||||
"darken_or_lighten_background_of_your_choice": "Escureça ou clareie o fundo da sua escolha.",
|
||||
"date_format": "Formato de data",
|
||||
"days_before_showing_this_survey_again": "dias após qualquer pesquisa ser mostrada antes que esta pesquisa possa aparecer.",
|
||||
"delete_block": "Excluir bloco",
|
||||
"delete_choice": "Deletar opção",
|
||||
"disable_the_visibility_of_survey_progress": "Desativar a visibilidade do progresso da pesquisa.",
|
||||
"display_an_estimate_of_completion_time_for_survey": "Mostrar uma estimativa de tempo de conclusão da pesquisa",
|
||||
@@ -1338,9 +1343,12 @@
|
||||
"does_not_include_all_of": "Não inclui todos de",
|
||||
"does_not_include_one_of": "Não inclui um de",
|
||||
"does_not_start_with": "Não começa com",
|
||||
"duplicate_block": "Duplicar bloco",
|
||||
"duplicate_question": "Duplicar pergunta",
|
||||
"edit_link": "Editar link",
|
||||
"edit_recall": "Editar Lembrete",
|
||||
"edit_translations": "Editar traduções de {lang}",
|
||||
"element_not_found": "Pergunta não encontrada",
|
||||
"enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permitir que os respondentes alterem o idioma a qualquer momento. Necessita de no mínimo 2 idiomas ativos.",
|
||||
"enable_recaptcha_to_protect_your_survey_from_spam": "A proteção contra spam usa o reCAPTCHA v3 para filtrar as respostas de spam.",
|
||||
"enable_spam_protection": "Proteção contra spam",
|
||||
@@ -1416,11 +1424,12 @@
|
||||
"hidden_field_used_in_recall": "Campo oculto \"{hiddenField}\" está sendo recordado na pergunta {questionIndex}.",
|
||||
"hidden_field_used_in_recall_ending_card": "Campo oculto \"{hiddenField}\" está sendo recordado no card de Encerramento.",
|
||||
"hidden_field_used_in_recall_welcome": "Campo oculto \"{hiddenField}\" está sendo recordado no card de Boas-Vindas.",
|
||||
"hide_advanced_settings": "Ocultar configurações avançadas",
|
||||
"hide_back_button": "Ocultar botão 'Voltar'",
|
||||
"hide_back_button_description": "Não exibir o botão de voltar na pesquisa",
|
||||
"hide_block_settings": "Ocultar configurações do bloco",
|
||||
"hide_logo": "Esconder logo",
|
||||
"hide_progress_bar": "Esconder barra de progresso",
|
||||
"hide_question_settings": "Ocultar configurações da pergunta",
|
||||
"hide_the_logo_in_this_specific_survey": "Esconder o logo nessa pesquisa específica",
|
||||
"hostname": "nome do host",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Quão descoladas você quer suas cartas em Pesquisas {surveyTypeDerived}",
|
||||
@@ -1447,6 +1456,7 @@
|
||||
"is_clicked": "É clicado",
|
||||
"is_completely_submitted": "Está completamente submetido",
|
||||
"is_empty": "Está vazio",
|
||||
"is_not_clicked": "Não é clicado",
|
||||
"is_not_empty": "Não está vazio",
|
||||
"is_not_set": "Não está definido",
|
||||
"is_partially_submitted": "Parcialmente enviado",
|
||||
@@ -1454,7 +1464,7 @@
|
||||
"is_skipped": "é pulado",
|
||||
"is_submitted": "é submetido",
|
||||
"italic": "Itálico",
|
||||
"jump_to_question": "Pular para a pergunta",
|
||||
"jump_to_block": "Pular para o bloco",
|
||||
"keep_current_order": "Manter pedido atual",
|
||||
"keep_showing_while_conditions_match": "Continue mostrando enquanto as condições corresponderem",
|
||||
"key": "chave",
|
||||
@@ -1468,16 +1478,18 @@
|
||||
"logic_error_warning": "Mudar vai causar erros de lógica",
|
||||
"logic_error_warning_text": "Mudar o tipo de pergunta vai remover as condições lógicas dessa pergunta",
|
||||
"long_answer": "resposta longa",
|
||||
"long_answer_toggle_description": "Permitir que os respondentes escrevam respostas mais longas e com várias linhas.",
|
||||
"lower_label": "Etiqueta Inferior",
|
||||
"manage_languages": "Gerenciar Idiomas",
|
||||
"matrix_all_fields": "Todos os campos",
|
||||
"matrix_rows": "Linhas",
|
||||
"max_file_size": "Tamanho máximo do arquivo",
|
||||
"max_file_size_limit_is": "Tamanho máximo do arquivo é",
|
||||
"move_question_to_block": "Mover pergunta para o bloco",
|
||||
"multiply": "Multiplicar *",
|
||||
"needed_for_self_hosted_cal_com_instance": "Necessário para uma instância auto-hospedada do Cal.com",
|
||||
"next_block": "Próximo bloco",
|
||||
"next_button_label": "Próximo",
|
||||
"next_question": "próxima pergunta",
|
||||
"no_hidden_fields_yet_add_first_one_below": "Ainda não há campos ocultos. Adicione o primeiro abaixo.",
|
||||
"no_images_found_for": "Nenhuma imagem encontrada para ''{query}\"",
|
||||
"no_languages_found_add_first_one_to_get_started": "Nenhum idioma encontrado. Adicione o primeiro para começar.",
|
||||
@@ -1589,17 +1601,17 @@
|
||||
"set_the_global_placement_in_the_look_feel_settings": "Defina o posicionamento global nas configurações de Aparência.",
|
||||
"settings_saved_successfully": "Configurações salvas com sucesso",
|
||||
"seven_points": "7 pontos",
|
||||
"show_advanced_settings": "Mostrar configurações avançadas",
|
||||
"show_block_settings": "Mostrar configurações do bloco",
|
||||
"show_button": "Mostrar Botão",
|
||||
"show_language_switch": "Mostrar troca de idioma",
|
||||
"show_multiple_times": "Mostrar um número limitado de vezes",
|
||||
"show_only_once": "Mostrar só uma vez",
|
||||
"show_question_settings": "Mostrar configurações da pergunta",
|
||||
"show_survey_maximum_of": "Mostrar no máximo",
|
||||
"show_survey_to_users": "Mostrar pesquisa para % dos usuários",
|
||||
"show_to_x_percentage_of_targeted_users": "Mostrar para {percentage}% dos usuários segmentados",
|
||||
"simple": "Simples",
|
||||
"six_points": "6 pontos",
|
||||
"skip_button_label": "Botão de Pular",
|
||||
"smiley": "Sorridente",
|
||||
"spam_protection_note": "A proteção contra spam não funciona para pesquisas exibidas com os SDKs iOS, React Native e Android. Isso vai quebrar a pesquisa.",
|
||||
"spam_protection_threshold_description": "Defina um valor entre 0 e 1, respostas abaixo desse valor serão rejeitadas.",
|
||||
@@ -1620,6 +1632,7 @@
|
||||
"survey_placement": "Posicionamento da Pesquisa",
|
||||
"survey_trigger": "Gatilho de Pesquisa",
|
||||
"switch_multi_lanugage_on_to_get_started": "Ative o modo multilíngue para começar 👉",
|
||||
"target_block_not_found": "Bloco de destino não encontrado",
|
||||
"targeted": "direcionado",
|
||||
"ten_points": "10 pontos",
|
||||
"the_survey_will_be_shown_multiple_times_until_they_respond": "Mostrar no máximo o número especificado de vezes, ou até que respondam (o que ocorrer primeiro).",
|
||||
@@ -1639,6 +1652,7 @@
|
||||
"unlock_targeting_title": "Desbloqueie o direcionamento com um plano superior",
|
||||
"unsaved_changes_warning": "Você tem alterações não salvas na sua pesquisa. Quer salvar antes de sair?",
|
||||
"until_they_submit_a_response": "Perguntar até que enviem uma resposta",
|
||||
"untitled_block": "Bloco sem título",
|
||||
"upgrade_notice_description": "Crie pesquisas multilíngues e desbloqueie muitas outras funcionalidades",
|
||||
"upgrade_notice_title": "Desbloqueie pesquisas multilíngues com um plano superior",
|
||||
"upload": "Enviar",
|
||||
@@ -2070,7 +2084,6 @@
|
||||
"card_abandonment_survey": "Pesquisa de Abandono de Carrinho",
|
||||
"card_abandonment_survey_description": "Entenda os motivos por trás do abandono de carrinho na sua loja online.",
|
||||
"card_abandonment_survey_question_1_button_label": "Claro!",
|
||||
"card_abandonment_survey_question_1_dismiss_button_label": "Não, valeu.",
|
||||
"card_abandonment_survey_question_1_headline": "Você tem 2 minutos para nos ajudar a melhorar?",
|
||||
"card_abandonment_survey_question_1_html": "Percebemos que você deixou alguns itens no seu carrinho. Adoraríamos entender o motivo.",
|
||||
"card_abandonment_survey_question_2_choice_1": "Custos de frete altos",
|
||||
@@ -2157,12 +2170,10 @@
|
||||
"churn_survey_question_2_button_label": "Enviar",
|
||||
"churn_survey_question_2_headline": "O que teria feito o $[projectName] mais fácil de usar?",
|
||||
"churn_survey_question_3_button_label": "Ganhe 30% de desconto",
|
||||
"churn_survey_question_3_dismiss_button_label": "Pular",
|
||||
"churn_survey_question_3_headline": "Ganhe 30% de desconto pelo próximo ano!",
|
||||
"churn_survey_question_3_html": "A gente adoraria te manter como cliente. Feliz em oferecer um desconto de 30% pro próximo ano.",
|
||||
"churn_survey_question_4_headline": "Quais recursos você está sentindo falta?",
|
||||
"churn_survey_question_5_button_label": "Enviar e-mail para o CEO",
|
||||
"churn_survey_question_5_dismiss_button_label": "Pular",
|
||||
"churn_survey_question_5_headline": "Que pena ouvir isso 😔 Fala direto com nosso CEO!",
|
||||
"churn_survey_question_5_html": "Nosso objetivo é oferecer o melhor atendimento ao cliente possível. Por favor, envie um e-mail para nossa CEO e ela vai cuidar pessoalmente do seu problema.",
|
||||
"collect_feedback_description": "Recolha feedback completo sobre seu produto ou serviço.",
|
||||
@@ -2269,6 +2280,7 @@
|
||||
"csat_survey_question_3_headline": "Ah, foi mal! Tem algo que a gente possa fazer pra melhorar sua experiência?",
|
||||
"csat_survey_question_3_placeholder": "Digite sua resposta aqui...",
|
||||
"cta_description": "Mostrar informações e pedir para os usuários tomarem uma ação específica",
|
||||
"custom_survey_block_1_name": "Bloco 1",
|
||||
"custom_survey_description": "Crie uma pesquisa sem modelo.",
|
||||
"custom_survey_name": "Começar do zero",
|
||||
"custom_survey_question_1_headline": "O que você gostaria de saber?",
|
||||
@@ -2355,7 +2367,6 @@
|
||||
"evaluate_a_product_idea_description": "Pesquise os usuários sobre ideias de produtos ou recursos. Obtenha feedback rapidamente.",
|
||||
"evaluate_a_product_idea_name": "Avaliar uma Ideia de Produto",
|
||||
"evaluate_a_product_idea_question_1_button_label": "Bora fazer isso!",
|
||||
"evaluate_a_product_idea_question_1_dismiss_button_label": "Pular",
|
||||
"evaluate_a_product_idea_question_1_headline": "A gente adora como você usa o $[projectName]! Queremos muito saber sua opinião sobre uma ideia de recurso. Tem um minutinho?",
|
||||
"evaluate_a_product_idea_question_1_html": "Respeitamos seu tempo e mantivemos curto 🤸",
|
||||
"evaluate_a_product_idea_question_2_headline": "Valeu! Quão difícil ou fácil é pra você [ÁREA DO PROBLEMA] hoje?",
|
||||
@@ -2364,7 +2375,6 @@
|
||||
"evaluate_a_product_idea_question_3_headline": "O que é mais difícil pra você quando se trata de [ÁREA DO PROBLEMA]?",
|
||||
"evaluate_a_product_idea_question_3_placeholder": "Digite sua resposta aqui...",
|
||||
"evaluate_a_product_idea_question_4_button_label": "Próximo",
|
||||
"evaluate_a_product_idea_question_4_dismiss_button_label": "Pular",
|
||||
"evaluate_a_product_idea_question_4_headline": "Estamos trabalhando em uma ideia para ajudar com [ÁREA DO PROBLEMA].",
|
||||
"evaluate_a_product_idea_question_4_html": "Insira um breve conceito aqui. Adicione os detalhes necessários, mas mantenha conciso e fácil de entender.",
|
||||
"evaluate_a_product_idea_question_5_headline": "Quão valiosa essa funcionalidade seria pra você?",
|
||||
@@ -2414,7 +2424,6 @@
|
||||
"feedback_box_question_2_headline": "O que tá quebrado?",
|
||||
"feedback_box_question_2_subheader": "Quanto mais detalhes, melhor :)",
|
||||
"feedback_box_question_3_button_label": "Sim, me avise",
|
||||
"feedback_box_question_3_dismiss_button_label": "Não, valeu",
|
||||
"feedback_box_question_3_headline": "Quer ficar por dentro?",
|
||||
"feedback_box_question_3_html": "Vamos consertar isso o mais rápido possível. Você quer ser avisado quando fizermos?",
|
||||
"feedback_box_question_4_button_label": "Solicitar recurso",
|
||||
@@ -2439,7 +2448,6 @@
|
||||
"identify_sign_up_barriers_description": "Ofereça um desconto pra entender melhor as barreiras de cadastro.",
|
||||
"identify_sign_up_barriers_name": "Identificar Barreiras de Cadastro",
|
||||
"identify_sign_up_barriers_question_1_button_label": "Ganhe 10% de desconto",
|
||||
"identify_sign_up_barriers_question_1_dismiss_button_label": "Não, valeu",
|
||||
"identify_sign_up_barriers_question_1_headline": "Responda essa pesquisa rápida e ganhe 10% de desconto!",
|
||||
"identify_sign_up_barriers_question_1_html": "Você parece estar pensando em se inscrever. Responda quatro perguntas e ganhe 10% de desconto em qualquer plano.",
|
||||
"identify_sign_up_barriers_question_2_headline": "Qual a chance de você se inscrever no $[projectName]?",
|
||||
@@ -2462,7 +2470,6 @@
|
||||
"identify_sign_up_barriers_question_8_headline": "Por favor, explica:",
|
||||
"identify_sign_up_barriers_question_8_placeholder": "Digite sua resposta aqui...",
|
||||
"identify_sign_up_barriers_question_9_button_label": "Cadastre-se",
|
||||
"identify_sign_up_barriers_question_9_dismiss_button_label": "Pular por enquanto",
|
||||
"identify_sign_up_barriers_question_9_headline": "Valeu! Aqui está seu código: SIGNUPNOW10",
|
||||
"identify_sign_up_barriers_question_9_html": "Valeu demais por tirar um tempinho pra compartilhar seu feedback 🙏",
|
||||
"identify_upsell_opportunities_description": "Descubra quanto tempo seu produto economiza para o usuário. Use isso para fazer upsell.",
|
||||
@@ -2499,7 +2506,6 @@
|
||||
"improve_newsletter_content_question_2_headline": "O que teria feito o boletim desta semana mais útil?",
|
||||
"improve_newsletter_content_question_2_placeholder": "Digite sua resposta aqui...",
|
||||
"improve_newsletter_content_question_3_button_label": "Feliz em ajudar!",
|
||||
"improve_newsletter_content_question_3_dismiss_button_label": "Encontre seus próprios amigos",
|
||||
"improve_newsletter_content_question_3_headline": "Valeu! ❤️ Espalhe o amor com UM amigo.",
|
||||
"improve_newsletter_content_question_3_html": "Quem pensa como você? Você faria um favorzão pra gente se compartilhasse o episódio dessa semana com seu amigo cérebro!",
|
||||
"improve_trial_conversion_description": "Descubra por que as pessoas pararam o teste. Esses insights ajudam a melhorar seu funil.",
|
||||
@@ -2514,7 +2520,6 @@
|
||||
"improve_trial_conversion_question_2_button_label": "Próximo",
|
||||
"improve_trial_conversion_question_2_headline": "Que chato ouvir isso. Qual foi o maior problema ao usar $[projectName]?",
|
||||
"improve_trial_conversion_question_4_button_label": "Ganhe 20% de desconto",
|
||||
"improve_trial_conversion_question_4_dismiss_button_label": "Pular",
|
||||
"improve_trial_conversion_question_4_headline": "Que pena ouvir isso! Ganhe 20% de desconto no primeiro ano.",
|
||||
"improve_trial_conversion_question_4_html": "Estamos felizes em te oferecer um desconto de 20% no plano anual.",
|
||||
"improve_trial_conversion_question_5_button_label": "Próximo",
|
||||
@@ -2678,7 +2683,6 @@
|
||||
"preview_survey_question_2_choice_2_label": "Não, obrigado!",
|
||||
"preview_survey_question_2_headline": "Quer ficar por dentro?",
|
||||
"preview_survey_welcome_card_headline": "Bem-vindo!",
|
||||
"preview_survey_welcome_card_html": "Valeu pelo feedback - bora lá!",
|
||||
"prioritize_features_description": "Identifique os recursos que seus usuários mais e menos precisam.",
|
||||
"prioritize_features_name": "Priorizar Funcionalidades",
|
||||
"prioritize_features_question_1_choice_1": "Recurso 1",
|
||||
@@ -2704,7 +2708,6 @@
|
||||
"product_market_fit_superhuman": "Ajuste do Produto ao Mercado (Superhuman)",
|
||||
"product_market_fit_superhuman_description": "Meça o PMF avaliando o quão desapontados os usuários ficariam se seu produto desaparecesse.",
|
||||
"product_market_fit_superhuman_question_1_button_label": "Feliz em ajudar!",
|
||||
"product_market_fit_superhuman_question_1_dismiss_button_label": "Não, valeu.",
|
||||
"product_market_fit_superhuman_question_1_headline": "Você é um dos nossos usuários top! Tem 5 minutinhos?",
|
||||
"product_market_fit_superhuman_question_1_html": "Adoraríamos entender melhor sua experiência como usuário. Compartilhar sua opinião ajuda muito.",
|
||||
"product_market_fit_superhuman_question_2_choice_1": "Nem um pouco decepcionado",
|
||||
@@ -2810,7 +2813,6 @@
|
||||
"site_abandonment_survey_description": "Entenda os motivos por trás do abandono de carrinho na sua loja online.",
|
||||
"site_abandonment_survey_question_1_html": "Percebemos que você está saindo do nosso site sem fazer uma compra. Adoraríamos entender o motivo.",
|
||||
"site_abandonment_survey_question_2_button_label": "Claro!",
|
||||
"site_abandonment_survey_question_2_dismiss_button_label": "Não, valeu.",
|
||||
"site_abandonment_survey_question_2_headline": "Você tem um minuto?",
|
||||
"site_abandonment_survey_question_3_choice_1": "Não consigo encontrar o que estou procurando",
|
||||
"site_abandonment_survey_question_3_choice_2": "Encontrei um site melhor",
|
||||
@@ -2835,7 +2837,6 @@
|
||||
"site_abandonment_survey_question_7_label": "Sim, por favor entre em contato.",
|
||||
"site_abandonment_survey_question_8_headline": "Por favor, compartilha seu e-mail:",
|
||||
"site_abandonment_survey_question_9_headline": "Algum comentário ou sugestão a mais?",
|
||||
"skip": "Pular",
|
||||
"smileys_survey_name": "Pesquisa de Smileys",
|
||||
"smileys_survey_question_1_headline": "O que você tá achando do $[projectName]?",
|
||||
"smileys_survey_question_1_lower_label": "Não tá bom",
|
||||
|
||||
+24
-23
@@ -1206,9 +1206,9 @@
|
||||
"add": "Adicionar +",
|
||||
"add_a_delay_or_auto_close_the_survey": "Adicionar um atraso ou fechar automaticamente o inquérito",
|
||||
"add_a_four_digit_pin": "Adicione um PIN de quatro dígitos",
|
||||
"add_a_new_question_to_your_survey": "Adicionar uma nova pergunta ao seu inquérito",
|
||||
"add_a_variable_to_calculate": "Adicionar uma variável para calcular",
|
||||
"add_action_below": "Adicionar ação abaixo",
|
||||
"add_block": "Adicionar bloco",
|
||||
"add_choice_below": "Adicionar escolha abaixo",
|
||||
"add_color_coding": "Adicionar codificação de cores",
|
||||
"add_color_coding_description": "Adicionar códigos de cores vermelho, laranja e verde às opções.",
|
||||
@@ -1229,8 +1229,8 @@
|
||||
"add_other": "Adicionar \"Outro\"",
|
||||
"add_photo_or_video": "Adicionar foto ou vídeo",
|
||||
"add_pin": "Adicionar PIN",
|
||||
"add_question": "Adicionar pergunta",
|
||||
"add_question_below": "Adicionar pergunta abaixo",
|
||||
"add_question_to_block": "Adicionar pergunta ao bloco",
|
||||
"add_row": "Adicionar linha",
|
||||
"add_variable": "Adicionar variável",
|
||||
"address_fields": "Campos de Endereço",
|
||||
@@ -1256,12 +1256,14 @@
|
||||
"automatically_mark_the_survey_as_complete_after": "Marcar automaticamente o inquérito como concluído após",
|
||||
"back_button_label": "Rótulo do botão \"Voltar\"",
|
||||
"background_styling": "Estilo de Fundo",
|
||||
"block_deleted": "Bloco eliminado.",
|
||||
"block_duplicated": "Bloco duplicado.",
|
||||
"bold": "Negrito",
|
||||
"brand_color": "Cor da marca",
|
||||
"brightness": "Brilho",
|
||||
"button_external": "Ativar link externo",
|
||||
"button_external_description": "Adicionar um botão que abre um URL externo num novo separador",
|
||||
"button_label": "Rótulo do botão",
|
||||
"button_to_continue_in_survey": "Botão para continuar na pesquisa",
|
||||
"button_to_link_to_external_url": "Botão para ligar a URL externa",
|
||||
"button_url": "URL do botão",
|
||||
"cal_username": "Nome de utilizador do Cal.com ou nome de utilizador/evento",
|
||||
"calculate": "Calcular",
|
||||
@@ -1300,6 +1302,7 @@
|
||||
"character_limit_toggle_title": "Adicionar limites de caracteres",
|
||||
"checkbox_label": "Rótulo da Caixa de Seleção",
|
||||
"choose_the_actions_which_trigger_the_survey": "Escolha as ações que desencadeiam o inquérito.",
|
||||
"choose_the_first_question_on_your_block": "Escolha a primeira pergunta no seu bloco",
|
||||
"choose_where_to_run_the_survey": "Escolha onde realizar o inquérito.",
|
||||
"city": "Cidade",
|
||||
"close_survey_on_response_limit": "Fechar inquérito no limite de respostas",
|
||||
@@ -1323,10 +1326,12 @@
|
||||
"create_group": "Criar grupo",
|
||||
"create_your_own_survey": "Crie o seu próprio inquérito",
|
||||
"css_selector": "Seletor CSS",
|
||||
"cta_button_label": "Etiqueta do botão \"CTA\"",
|
||||
"custom_hostname": "Nome do host personalizado",
|
||||
"darken_or_lighten_background_of_your_choice": "Escurecer ou clarear o fundo da sua escolha.",
|
||||
"date_format": "Formato da data",
|
||||
"days_before_showing_this_survey_again": "dias após qualquer inquérito ser mostrado antes que este inquérito possa aparecer.",
|
||||
"delete_block": "Eliminar bloco",
|
||||
"delete_choice": "Eliminar escolha",
|
||||
"disable_the_visibility_of_survey_progress": "Desativar a visibilidade do progresso da pesquisa.",
|
||||
"display_an_estimate_of_completion_time_for_survey": "Mostrar uma estimativa do tempo de conclusão do inquérito",
|
||||
@@ -1338,9 +1343,12 @@
|
||||
"does_not_include_all_of": "Não inclui todos de",
|
||||
"does_not_include_one_of": "Não inclui um de",
|
||||
"does_not_start_with": "Não começa com",
|
||||
"duplicate_block": "Duplicar bloco",
|
||||
"duplicate_question": "Duplicar pergunta",
|
||||
"edit_link": "Editar link",
|
||||
"edit_recall": "Editar Lembrete",
|
||||
"edit_translations": "Editar traduções {lang}",
|
||||
"element_not_found": "Pergunta não encontrada",
|
||||
"enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permitir que os inquiridos mudem de idioma a qualquer momento. Necessita de pelo menos 2 idiomas ativos.",
|
||||
"enable_recaptcha_to_protect_your_survey_from_spam": "A proteção contra spam usa o reCAPTCHA v3 para filtrar as respostas de spam.",
|
||||
"enable_spam_protection": "Proteção contra spam",
|
||||
@@ -1416,11 +1424,12 @@
|
||||
"hidden_field_used_in_recall": "Campo oculto \"{hiddenField}\" está a ser recordado na pergunta {questionIndex}.",
|
||||
"hidden_field_used_in_recall_ending_card": "Campo oculto \"{hiddenField}\" está a ser recordado no Cartão de Conclusão",
|
||||
"hidden_field_used_in_recall_welcome": "Campo oculto \"{hiddenField}\" está a ser recordado no cartão de boas-vindas.",
|
||||
"hide_advanced_settings": "Ocultar definições avançadas",
|
||||
"hide_back_button": "Ocultar botão 'Retroceder'",
|
||||
"hide_back_button_description": "Não mostrar o botão de retroceder no inquérito",
|
||||
"hide_block_settings": "Ocultar definições do bloco",
|
||||
"hide_logo": "Esconder logótipo",
|
||||
"hide_progress_bar": "Ocultar barra de progresso",
|
||||
"hide_question_settings": "Ocultar definições da pergunta",
|
||||
"hide_the_logo_in_this_specific_survey": "Ocultar o logótipo neste inquérito específico",
|
||||
"hostname": "Nome do host",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Quão extravagantes quer os seus cartões em Inquéritos {surveyTypeDerived}",
|
||||
@@ -1447,6 +1456,7 @@
|
||||
"is_clicked": "É clicado",
|
||||
"is_completely_submitted": "Está completamente submetido",
|
||||
"is_empty": "Está vazio",
|
||||
"is_not_clicked": "Não é clicado",
|
||||
"is_not_empty": "Não está vazio",
|
||||
"is_not_set": "Não está definido",
|
||||
"is_partially_submitted": "Está parcialmente submetido",
|
||||
@@ -1454,7 +1464,7 @@
|
||||
"is_skipped": "É ignorado",
|
||||
"is_submitted": "Está submetido",
|
||||
"italic": "Itálico",
|
||||
"jump_to_question": "Saltar para a pergunta",
|
||||
"jump_to_block": "Saltar para o bloco",
|
||||
"keep_current_order": "Manter ordem atual",
|
||||
"keep_showing_while_conditions_match": "Continuar a mostrar enquanto as condições corresponderem",
|
||||
"key": "Chave",
|
||||
@@ -1468,16 +1478,18 @@
|
||||
"logic_error_warning": "A alteração causará erros de lógica",
|
||||
"logic_error_warning_text": "Alterar o tipo de pergunta irá remover as condições lógicas desta pergunta",
|
||||
"long_answer": "Resposta longa",
|
||||
"long_answer_toggle_description": "Permitir que os inquiridos escrevam respostas mais longas e com várias linhas.",
|
||||
"lower_label": "Etiqueta Inferior",
|
||||
"manage_languages": "Gerir Idiomas",
|
||||
"matrix_all_fields": "Todos os campos",
|
||||
"matrix_rows": "Linhas",
|
||||
"max_file_size": "Tamanho máximo do ficheiro",
|
||||
"max_file_size_limit_is": "O limite do tamanho máximo do ficheiro é",
|
||||
"move_question_to_block": "Mover pergunta para o bloco",
|
||||
"multiply": "Multiplicar *",
|
||||
"needed_for_self_hosted_cal_com_instance": "Necessário para uma instância auto-hospedada do Cal.com",
|
||||
"next_block": "Bloco seguinte",
|
||||
"next_button_label": "Rótulo do botão \"Seguinte\"",
|
||||
"next_question": "Próxima pergunta",
|
||||
"no_hidden_fields_yet_add_first_one_below": "Ainda não há campos ocultos. Adicione o primeiro abaixo.",
|
||||
"no_images_found_for": "Não foram encontradas imagens para ''{query}\"",
|
||||
"no_languages_found_add_first_one_to_get_started": "Nenhuma língua encontrada. Adicione a primeira para começar.",
|
||||
@@ -1589,17 +1601,17 @@
|
||||
"set_the_global_placement_in_the_look_feel_settings": "Definir a colocação global nas definições de Aparência.",
|
||||
"settings_saved_successfully": "Definições guardadas com sucesso",
|
||||
"seven_points": "7 pontos",
|
||||
"show_advanced_settings": "Mostrar definições avançadas",
|
||||
"show_block_settings": "Mostrar definições do bloco",
|
||||
"show_button": "Mostrar Botão",
|
||||
"show_language_switch": "Mostrar alternador de idioma",
|
||||
"show_multiple_times": "Mostrar um número limitado de vezes",
|
||||
"show_only_once": "Mostrar apenas uma vez",
|
||||
"show_question_settings": "Mostrar definições da pergunta",
|
||||
"show_survey_maximum_of": "Mostrar inquérito máximo de",
|
||||
"show_survey_to_users": "Mostrar inquérito a % dos utilizadores",
|
||||
"show_to_x_percentage_of_targeted_users": "Mostrar a {percentage}% dos utilizadores alvo",
|
||||
"simple": "Simples",
|
||||
"six_points": "6 pontos",
|
||||
"skip_button_label": "Rótulo do botão Ignorar",
|
||||
"smiley": "Sorridente",
|
||||
"spam_protection_note": "A proteção contra spam não funciona para inquéritos exibidos com os SDKs iOS, React Native e Android. Isso irá quebrar o inquérito.",
|
||||
"spam_protection_threshold_description": "Defina um valor entre 0 e 1, respostas abaixo deste valor serão rejeitadas.",
|
||||
@@ -1620,6 +1632,7 @@
|
||||
"survey_placement": "Colocação do Inquérito",
|
||||
"survey_trigger": "Desencadeador de Inquérito",
|
||||
"switch_multi_lanugage_on_to_get_started": "Ative o modo multilingue para começar 👉",
|
||||
"target_block_not_found": "Bloco de destino não encontrado",
|
||||
"targeted": "Alvo",
|
||||
"ten_points": "10 pontos",
|
||||
"the_survey_will_be_shown_multiple_times_until_they_respond": "Mostrar no máximo o número especificado de vezes, ou até que respondam (o que ocorrer primeiro).",
|
||||
@@ -1639,6 +1652,7 @@
|
||||
"unlock_targeting_title": "Desbloqueie a segmentação com um plano superior",
|
||||
"unsaved_changes_warning": "Tem alterações não guardadas no seu inquérito. Gostaria de as guardar antes de sair?",
|
||||
"until_they_submit_a_response": "Perguntar até que submetam uma resposta",
|
||||
"untitled_block": "Bloco sem título",
|
||||
"upgrade_notice_description": "Crie inquéritos multilingues e desbloqueie muitas mais funcionalidades",
|
||||
"upgrade_notice_title": "Desbloqueie inquéritos multilingues com um plano superior",
|
||||
"upload": "Carregar",
|
||||
@@ -2070,7 +2084,6 @@
|
||||
"card_abandonment_survey": "Inquérito de Abandono de Carrinho",
|
||||
"card_abandonment_survey_description": "Compreenda as razões por trás do abandono do carrinho na sua loja online.",
|
||||
"card_abandonment_survey_question_1_button_label": "Claro!",
|
||||
"card_abandonment_survey_question_1_dismiss_button_label": "Não, obrigado.",
|
||||
"card_abandonment_survey_question_1_headline": "Tem 2 minutos para nos ajudar a melhorar?",
|
||||
"card_abandonment_survey_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Notámos que deixou alguns itens no seu carrinho. Gostaríamos de entender porquê.</span></p>",
|
||||
"card_abandonment_survey_question_2_choice_1": "Custos de envio elevados",
|
||||
@@ -2157,12 +2170,10 @@
|
||||
"churn_survey_question_2_button_label": "Enviar",
|
||||
"churn_survey_question_2_headline": "O que teria tornado $[projectName] mais fácil de usar?",
|
||||
"churn_survey_question_3_button_label": "Obtenha 30% de desconto",
|
||||
"churn_survey_question_3_dismiss_button_label": "Saltar",
|
||||
"churn_survey_question_3_headline": "Obtenha 30% de desconto no próximo ano!",
|
||||
"churn_survey_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Adoraríamos mantê-lo como cliente. Estamos felizes por lhe oferecer um desconto de 30% para o próximo ano.</span></p>",
|
||||
"churn_survey_question_4_headline": "Que funcionalidades lhe faltam?",
|
||||
"churn_survey_question_5_button_label": "Enviar email para o CEO",
|
||||
"churn_survey_question_5_dismiss_button_label": "Saltar",
|
||||
"churn_survey_question_5_headline": "Lamentamos ouvir isso 😔 Fale diretamente com o nosso CEO!",
|
||||
"churn_survey_question_5_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>O nosso objetivo é fornecer o melhor serviço ao cliente possível. Por favor, envie um email à nossa CEO e ela tratará pessoalmente do seu problema.</span></p>",
|
||||
"collect_feedback_description": "Recolha feedback abrangente sobre o seu produto ou serviço.",
|
||||
@@ -2269,6 +2280,7 @@
|
||||
"csat_survey_question_3_headline": "Oh, desculpe! Há algo que possamos fazer para melhorar a sua experiência?",
|
||||
"csat_survey_question_3_placeholder": "Escreva a sua resposta aqui...",
|
||||
"cta_description": "Exibir informações e solicitar aos utilizadores que tomem uma ação específica",
|
||||
"custom_survey_block_1_name": "Bloco 1",
|
||||
"custom_survey_description": "Crie um inquérito sem modelo.",
|
||||
"custom_survey_name": "Começar do zero",
|
||||
"custom_survey_question_1_headline": "O que gostaria de saber?",
|
||||
@@ -2355,7 +2367,6 @@
|
||||
"evaluate_a_product_idea_description": "Sonde os utilizadores sobre ideias de produtos ou funcionalidades. Obtenha feedback rapidamente.",
|
||||
"evaluate_a_product_idea_name": "Avaliar uma Ideia de Produto",
|
||||
"evaluate_a_product_idea_question_1_button_label": "Vamos a isso!",
|
||||
"evaluate_a_product_idea_question_1_dismiss_button_label": "Saltar",
|
||||
"evaluate_a_product_idea_question_1_headline": "Valorizamos muito a sua opinião. Tem um minuto para partilhar a sua opinião sobre uma funcionalidade?",
|
||||
"evaluate_a_product_idea_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Respeitamos o seu tempo e mantivemos isto curto 🤸</span></p>",
|
||||
"evaluate_a_product_idea_question_2_headline": "Obrigado! Quão difícil ou fácil é para si [PROBLEM AREA] hoje?",
|
||||
@@ -2364,7 +2375,6 @@
|
||||
"evaluate_a_product_idea_question_3_headline": "O que é mais difícil para si quando se trata de [PROBLEM AREA]?",
|
||||
"evaluate_a_product_idea_question_3_placeholder": "Escreva a sua resposta aqui...",
|
||||
"evaluate_a_product_idea_question_4_button_label": "Seguinte",
|
||||
"evaluate_a_product_idea_question_4_dismiss_button_label": "Saltar",
|
||||
"evaluate_a_product_idea_question_4_headline": "Estamos a trabalhar numa ideia para ajudar com [PROBLEM AREA].",
|
||||
"evaluate_a_product_idea_question_4_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Insira aqui o resumo do conceito. Adicione os detalhes necessários, mas mantenha-o conciso e fácil de entender.</span></p>",
|
||||
"evaluate_a_product_idea_question_5_headline": "Quão valiosa seria esta funcionalidade para si?",
|
||||
@@ -2414,7 +2424,6 @@
|
||||
"feedback_box_question_2_headline": "O que não está a correr bem?",
|
||||
"feedback_box_question_2_subheader": "Quanto mais detalhes, melhor :)",
|
||||
"feedback_box_question_3_button_label": "Sim, notifique-me",
|
||||
"feedback_box_question_3_dismiss_button_label": "Não, obrigado",
|
||||
"feedback_box_question_3_headline": "Quer manter-se atualizado?",
|
||||
"feedback_box_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Vamos resolver isto o mais rápido possível. Quer ser notificado quando o fizermos?</span></p>",
|
||||
"feedback_box_question_4_button_label": "Pedir funcionalidade",
|
||||
@@ -2439,7 +2448,6 @@
|
||||
"identify_sign_up_barriers_description": "Ofereça um desconto para obter informações sobre as barreiras de inscrição.",
|
||||
"identify_sign_up_barriers_name": "Identificar Barreiras de Inscrição",
|
||||
"identify_sign_up_barriers_question_1_button_label": "Obtenha 10% de desconto",
|
||||
"identify_sign_up_barriers_question_1_dismiss_button_label": "Não, obrigado",
|
||||
"identify_sign_up_barriers_question_1_headline": "Responda a este breve questionário, obtenha 10% de desconto!",
|
||||
"identify_sign_up_barriers_question_1_html": "Parece que está a considerar inscrever-se. Responda a quatro perguntas e obtenha 10% em qualquer plano.",
|
||||
"identify_sign_up_barriers_question_2_headline": "Qual é a probabilidade de se inscrever no $[projectName]?",
|
||||
@@ -2462,7 +2470,6 @@
|
||||
"identify_sign_up_barriers_question_8_headline": "Por favor, explique:",
|
||||
"identify_sign_up_barriers_question_8_placeholder": "Escreva a sua resposta aqui...",
|
||||
"identify_sign_up_barriers_question_9_button_label": "Inscrever-se",
|
||||
"identify_sign_up_barriers_question_9_dismiss_button_label": "Saltar por agora",
|
||||
"identify_sign_up_barriers_question_9_headline": "Obrigado! Aqui está o seu código: SIGNUPNOW10",
|
||||
"identify_sign_up_barriers_question_9_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Muito obrigado por partilhar o seu feedback connosco 🙏</span></p>",
|
||||
"identify_upsell_opportunities_description": "Descubra quanto tempo o seu produto poupa ao seu utilizador. Use isso para vender mais.",
|
||||
@@ -2499,7 +2506,6 @@
|
||||
"improve_newsletter_content_question_2_headline": "O que teria tornado a newsletter desta semana mais útil?",
|
||||
"improve_newsletter_content_question_2_placeholder": "Escreva a sua resposta aqui...",
|
||||
"improve_newsletter_content_question_3_button_label": "Feliz por ajudar!",
|
||||
"improve_newsletter_content_question_3_dismiss_button_label": "Encontre os seus próprios amigos",
|
||||
"improve_newsletter_content_question_3_headline": "Obrigado! ❤️ Espalhe o amor com UM amigo.",
|
||||
"improve_newsletter_content_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Quem pensa como tu? Farias-nos um grande favor se partilhasses o episódio desta semana com o teu amigo cérebro!</span></p>",
|
||||
"improve_trial_conversion_description": "Descubra por que as pessoas não acabaram o inquérito. Estes insights ajudam a melhorar o seu funil.",
|
||||
@@ -2514,7 +2520,6 @@
|
||||
"improve_trial_conversion_question_2_button_label": "Seguinte",
|
||||
"improve_trial_conversion_question_2_headline": "Lamentamos saber. Qual foi o maior problema ao usar $[projectName]?",
|
||||
"improve_trial_conversion_question_4_button_label": "Obtenha 20% de desconto",
|
||||
"improve_trial_conversion_question_4_dismiss_button_label": "Saltar",
|
||||
"improve_trial_conversion_question_4_headline": "Lamentamos saber! Obtenha 20% de desconto no primeiro ano.",
|
||||
"improve_trial_conversion_question_4_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Estamos felizes por lhe oferecer um desconto de 20% num plano anual.</span></p>",
|
||||
"improve_trial_conversion_question_5_button_label": "Seguinte",
|
||||
@@ -2678,7 +2683,6 @@
|
||||
"preview_survey_question_2_choice_2_label": "Não, obrigado!",
|
||||
"preview_survey_question_2_headline": "Quer manter-se atualizado?",
|
||||
"preview_survey_welcome_card_headline": "Bem-vindo!",
|
||||
"preview_survey_welcome_card_html": "Obrigado por fornecer o seu feedback - vamos a isso!",
|
||||
"prioritize_features_description": "Identifique as funcionalidades que os seus utilizadores precisam mais e menos.",
|
||||
"prioritize_features_name": "Priorizar Funcionalidades",
|
||||
"prioritize_features_question_1_choice_1": "Funcionalidade 1",
|
||||
@@ -2704,7 +2708,6 @@
|
||||
"product_market_fit_superhuman": "Adequação do Produto ao Mercado (Superhuman)",
|
||||
"product_market_fit_superhuman_description": "Meça a adequação do produto ao mercado avaliando o quão desapontados os utilizadores ficariam se o seu produto desaparecesse.",
|
||||
"product_market_fit_superhuman_question_1_button_label": "Feliz por ajudar!",
|
||||
"product_market_fit_superhuman_question_1_dismiss_button_label": "Não, obrigado.",
|
||||
"product_market_fit_superhuman_question_1_headline": "É um dos nossos utilizadores avançados! Tem 5 minutos?",
|
||||
"product_market_fit_superhuman_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Gostaríamos de entender melhor a sua experiência de utilizador. Partilhar a sua opinião ajuda muito.</span></p>",
|
||||
"product_market_fit_superhuman_question_2_choice_1": "Nada desapontado",
|
||||
@@ -2810,7 +2813,6 @@
|
||||
"site_abandonment_survey_description": "Compreenda as razões por trás do abandono do site na sua loja online.",
|
||||
"site_abandonment_survey_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Notámos que está a sair do nosso site sem fazer uma compra. Gostaríamos de entender porquê.</span></p>",
|
||||
"site_abandonment_survey_question_2_button_label": "Claro!",
|
||||
"site_abandonment_survey_question_2_dismiss_button_label": "Não, obrigado.",
|
||||
"site_abandonment_survey_question_2_headline": "Tem um minuto?",
|
||||
"site_abandonment_survey_question_3_choice_1": "Não consigo encontrar o que procuro",
|
||||
"site_abandonment_survey_question_3_choice_2": "Encontrei um site melhor",
|
||||
@@ -2835,7 +2837,6 @@
|
||||
"site_abandonment_survey_question_7_label": "Sim, por favor entre em contacto.",
|
||||
"site_abandonment_survey_question_8_headline": "Por favor, partilhe o seu endereço de email:",
|
||||
"site_abandonment_survey_question_9_headline": "Algum comentário ou sugestão adicional?",
|
||||
"skip": "Saltar",
|
||||
"smileys_survey_name": "Inquérito Sorridente",
|
||||
"smileys_survey_question_1_headline": "Como gosta de $[projectName]?",
|
||||
"smileys_survey_question_1_lower_label": "Não é bom",
|
||||
|
||||
+24
-23
@@ -1206,9 +1206,9 @@
|
||||
"add": "Adaugă +",
|
||||
"add_a_delay_or_auto_close_the_survey": "Adăugați o întârziere sau închideți automat sondajul",
|
||||
"add_a_four_digit_pin": "Adăugați un cod PIN din patru cifre",
|
||||
"add_a_new_question_to_your_survey": "Adaugă o nouă întrebare la sondajul tău",
|
||||
"add_a_variable_to_calculate": "Adaugă o variabilă pentru calcul",
|
||||
"add_action_below": "Adăugați acțiune mai jos",
|
||||
"add_block": "Adaugă bloc",
|
||||
"add_choice_below": "Adaugă opțiunea de mai jos",
|
||||
"add_color_coding": "Adăugați codificare color",
|
||||
"add_color_coding_description": "Adăugați coduri de culoare roșu, portocaliu și verde la opțiuni.",
|
||||
@@ -1229,8 +1229,8 @@
|
||||
"add_other": "Adăugați \"Altele\"",
|
||||
"add_photo_or_video": "Adaugă fotografie sau video",
|
||||
"add_pin": "Adaugă PIN",
|
||||
"add_question": "Adaugă întrebare",
|
||||
"add_question_below": "Adaugă întrebare mai jos",
|
||||
"add_question_to_block": "Adaugă întrebare în bloc",
|
||||
"add_row": "Adăugați rând",
|
||||
"add_variable": "Adaugă variabilă",
|
||||
"address_fields": "Câmpuri Adresă",
|
||||
@@ -1256,12 +1256,14 @@
|
||||
"automatically_mark_the_survey_as_complete_after": "Marcați automat sondajul ca finalizat după",
|
||||
"back_button_label": "Etichetă buton \"Înapoi\"",
|
||||
"background_styling": "Stilizare fundal",
|
||||
"block_deleted": "Bloc șters.",
|
||||
"block_duplicated": "Bloc duplicat.",
|
||||
"bold": "Îngroșat",
|
||||
"brand_color": "Culoarea brandului",
|
||||
"brightness": "Luminozitate",
|
||||
"button_external": "Activează link extern",
|
||||
"button_external_description": "Adaugă un buton care deschide un URL extern într-o filă nouă",
|
||||
"button_label": "Etichetă buton",
|
||||
"button_to_continue_in_survey": "Buton pentru a continua în sondaj",
|
||||
"button_to_link_to_external_url": "Buton pentru a face legătura la un URL extern",
|
||||
"button_url": "URL Buton",
|
||||
"cal_username": "Utilizator Cal.com sau utilizator/eveniment",
|
||||
"calculate": "Calculați",
|
||||
@@ -1300,6 +1302,7 @@
|
||||
"character_limit_toggle_title": "Adăugați limite de caractere",
|
||||
"checkbox_label": "Etichetă casetă de selectare",
|
||||
"choose_the_actions_which_trigger_the_survey": "Alegeți acțiunile care declanșează sondajul.",
|
||||
"choose_the_first_question_on_your_block": "Alege prima întrebare din blocul tău",
|
||||
"choose_where_to_run_the_survey": "Alegeți unde să rulați chestionarul.",
|
||||
"city": "Oraș",
|
||||
"close_survey_on_response_limit": "Închideți sondajul la limită de răspunsuri",
|
||||
@@ -1323,10 +1326,12 @@
|
||||
"create_group": "Creează grup",
|
||||
"create_your_own_survey": "Creează-ți propriul chestionar",
|
||||
"css_selector": "Selector CSS",
|
||||
"cta_button_label": "Eticheta butonului \"CTA\"",
|
||||
"custom_hostname": "Gazdă personalizată",
|
||||
"darken_or_lighten_background_of_your_choice": "Întunecați sau luminați fundalul după preferințe.",
|
||||
"date_format": "Format dată",
|
||||
"days_before_showing_this_survey_again": "zile după afișarea oricărui sondaj înainte ca acest sondaj să poată apărea din nou.",
|
||||
"delete_block": "Șterge blocul",
|
||||
"delete_choice": "Șterge alegerea",
|
||||
"disable_the_visibility_of_survey_progress": "Dezactivați vizibilitatea progresului sondajului",
|
||||
"display_an_estimate_of_completion_time_for_survey": "Afișează o estimare a timpului de finalizare pentru sondaj",
|
||||
@@ -1338,9 +1343,12 @@
|
||||
"does_not_include_all_of": "Nu include toate",
|
||||
"does_not_include_one_of": "Nu include una dintre",
|
||||
"does_not_start_with": "Nu începe cu",
|
||||
"duplicate_block": "Duplicați blocul",
|
||||
"duplicate_question": "Duplică întrebarea",
|
||||
"edit_link": "Editare legătură",
|
||||
"edit_recall": "Editează Referințele",
|
||||
"edit_translations": "Editează traducerile {lang}",
|
||||
"element_not_found": "Întrebarea nu a fost găsită",
|
||||
"enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permite respondenților să schimbe limba în orice moment. Necesită minimum 2 limbi active.",
|
||||
"enable_recaptcha_to_protect_your_survey_from_spam": "Protecția împotriva spamului folosește reCAPTCHA v3 pentru a filtra răspunsurile de spam.",
|
||||
"enable_spam_protection": "Protecția împotriva spamului",
|
||||
@@ -1416,11 +1424,12 @@
|
||||
"hidden_field_used_in_recall": "Câmpul ascuns \"{hiddenField}\" este reamintit în întrebarea {questionIndex}.",
|
||||
"hidden_field_used_in_recall_ending_card": "Câmpul ascuns \"{hiddenField}\" este reamintit în Cardul de Încheiere.",
|
||||
"hidden_field_used_in_recall_welcome": "Câmpul ascuns \"{hiddenField}\" este reamintit în cardul de bun venit.",
|
||||
"hide_advanced_settings": "Ascunde setări avansate",
|
||||
"hide_back_button": "Ascunde butonul 'Înapoi'",
|
||||
"hide_back_button_description": "Nu afișa butonul Înapoi în sondaj",
|
||||
"hide_block_settings": "Ascunde setările blocului",
|
||||
"hide_logo": "Ascunde logo",
|
||||
"hide_progress_bar": "Ascunde bara de progres",
|
||||
"hide_question_settings": "Ascunde setările întrebării",
|
||||
"hide_the_logo_in_this_specific_survey": "Ascunde logo-ul în acest chestionar specific",
|
||||
"hostname": "Nume gazdă",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Cât de funky doriți să fie cardurile dumneavoastră în sondajele de tip {surveyTypeDerived}",
|
||||
@@ -1447,6 +1456,7 @@
|
||||
"is_clicked": "Este apăsat",
|
||||
"is_completely_submitted": "Este complet trimis",
|
||||
"is_empty": "Este gol",
|
||||
"is_not_clicked": "Nu este apăsat",
|
||||
"is_not_empty": "Nu este gol",
|
||||
"is_not_set": "Nu este setat",
|
||||
"is_partially_submitted": "Este parțial trimis",
|
||||
@@ -1454,7 +1464,7 @@
|
||||
"is_skipped": "Este sărit",
|
||||
"is_submitted": "Este trimis",
|
||||
"italic": "Cursiv",
|
||||
"jump_to_question": "Sări la întrebare",
|
||||
"jump_to_block": "Sari la bloc",
|
||||
"keep_current_order": "Păstrați ordinea actuală",
|
||||
"keep_showing_while_conditions_match": "Continuă să afișezi cât timp condițiile se potrivesc",
|
||||
"key": "Cheie",
|
||||
@@ -1468,16 +1478,18 @@
|
||||
"logic_error_warning": "Schimbarea va provoca erori de logică",
|
||||
"logic_error_warning_text": "Schimbarea tipului de întrebare va elimina condițiile de logică din această întrebare",
|
||||
"long_answer": "Răspuns lung",
|
||||
"long_answer_toggle_description": "Permite respondenților să scrie răspunsuri mai lungi, pe mai multe rânduri.",
|
||||
"lower_label": "Etichetă inferioară",
|
||||
"manage_languages": "Gestionați limbile",
|
||||
"matrix_all_fields": "Toate câmpurile",
|
||||
"matrix_rows": "Rânduri",
|
||||
"max_file_size": "Dimensiune maximă fișier",
|
||||
"max_file_size_limit_is": "Limita dimensiunii maxime a fișierului este",
|
||||
"move_question_to_block": "Mută întrebarea în bloc",
|
||||
"multiply": "Multiplicare",
|
||||
"needed_for_self_hosted_cal_com_instance": "Necesar pentru un exemplu autogăzduit Cal.com",
|
||||
"next_block": "Blocul următor",
|
||||
"next_button_label": "Etichetă buton \"Următorul\"",
|
||||
"next_question": "Întrebarea următoare",
|
||||
"no_hidden_fields_yet_add_first_one_below": "Nu există încă câmpuri ascunse. Adăugați primul mai jos.",
|
||||
"no_images_found_for": "Nicio imagine găsită pentru ''{query}\"",
|
||||
"no_languages_found_add_first_one_to_get_started": "Nu s-au găsit limbi. Adaugă prima pentru a începe.",
|
||||
@@ -1589,17 +1601,17 @@
|
||||
"set_the_global_placement_in_the_look_feel_settings": "Setați amplasarea globală în setările Aspect & Stil.",
|
||||
"settings_saved_successfully": "Setările au fost salvate cu succes.",
|
||||
"seven_points": "7 puncte",
|
||||
"show_advanced_settings": "Afișați setările avansate",
|
||||
"show_block_settings": "Afișează setările blocului",
|
||||
"show_button": "Afișează butonul",
|
||||
"show_language_switch": "Afișează comutatorul de limbă",
|
||||
"show_multiple_times": "Afișează de mai multe ori",
|
||||
"show_only_once": "Afișează doar o dată",
|
||||
"show_question_settings": "Afișează setările întrebării",
|
||||
"show_survey_maximum_of": "Afișează sondajul de maxim",
|
||||
"show_survey_to_users": "Afișați sondajul la % din utilizatori",
|
||||
"show_to_x_percentage_of_targeted_users": "Afișați la {percentage}% din utilizatorii vizați",
|
||||
"simple": "Simplu",
|
||||
"six_points": "6 puncte",
|
||||
"skip_button_label": "Etichetă buton \"Omitere\"",
|
||||
"smiley": "Smiley",
|
||||
"spam_protection_note": "Protecția împotriva spamului nu funcționează pentru sondajele afișate folosind SDK-urile iOS, React Native și Android. Va întrerupe sondajul.",
|
||||
"spam_protection_threshold_description": "Setați valoarea între 0 și 1, răspunsurile sub această valoare vor fi respinse.",
|
||||
@@ -1620,6 +1632,7 @@
|
||||
"survey_placement": "Amplasarea sondajului",
|
||||
"survey_trigger": "Declanșator sondaj",
|
||||
"switch_multi_lanugage_on_to_get_started": "Comutați pe modul multilingv pentru a începe 👉",
|
||||
"target_block_not_found": "Blocul țintă nu a fost găsit",
|
||||
"targeted": "Ţintite",
|
||||
"ten_points": "10 puncte",
|
||||
"the_survey_will_be_shown_multiple_times_until_they_respond": "Afișează de cel mult numărul specificat de ori sau până când răspund (oricare dintre acestea survine prima).",
|
||||
@@ -1639,6 +1652,7 @@
|
||||
"unlock_targeting_title": "Deblocați țintirea cu un plan superior",
|
||||
"unsaved_changes_warning": "Aveți modificări nesalvate în sondajul dumneavoastră. Doriți să le salvați înainte de a pleca?",
|
||||
"until_they_submit_a_response": "Întreabă până când trimit un răspuns",
|
||||
"untitled_block": "Bloc fără titlu",
|
||||
"upgrade_notice_description": "Creați sondaje multilingve și deblocați multe alte caracteristici",
|
||||
"upgrade_notice_title": "Deblocați sondajele multilingve cu un plan superior",
|
||||
"upload": "Încărcați",
|
||||
@@ -2070,7 +2084,6 @@
|
||||
"card_abandonment_survey": "Chestionar de abandonare a coșului",
|
||||
"card_abandonment_survey_description": "Înțelegeți motivele abandonării coșului în magazinul dvs. online.",
|
||||
"card_abandonment_survey_question_1_button_label": "Sigur!",
|
||||
"card_abandonment_survey_question_1_dismiss_button_label": "Nu, mulţumesc",
|
||||
"card_abandonment_survey_question_1_headline": "Aveți 2 minute pentru a ne ajuta să îmbunătățim?",
|
||||
"card_abandonment_survey_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Am observat că ați lăsat câteva articole în coșul dvs. Ne-ar plăcea să înțelegem de ce.</span></p>",
|
||||
"card_abandonment_survey_question_2_choice_1": "Costuri mari de transport",
|
||||
@@ -2157,12 +2170,10 @@
|
||||
"churn_survey_question_2_button_label": "Trimite",
|
||||
"churn_survey_question_2_headline": "Ce ar fi făcut $[projectName] mai ușor de utilizat?",
|
||||
"churn_survey_question_3_button_label": "Obțineți 30% reducere",
|
||||
"churn_survey_question_3_dismiss_button_label": "Omite",
|
||||
"churn_survey_question_3_headline": "Obțineți 30% reducere pentru anul următor!",
|
||||
"churn_survey_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Ne-ar plăcea să rămâi clientul nostru. Suntem bucuroși să îți oferim o reducere de 30% pentru anul următor.</span></p>",
|
||||
"churn_survey_question_4_headline": "Ce funcționalități vă lipsesc?",
|
||||
"churn_survey_question_5_button_label": "Trimite email către CEO",
|
||||
"churn_survey_question_5_dismiss_button_label": "Omite",
|
||||
"churn_survey_question_5_headline": "Îmi pare rău să aud 😔 Vorbiți direct cu CEO-ul nostru!",
|
||||
"churn_survey_question_5_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Ne străduim să oferim cel mai bun serviciu pentru clienți. Vă rugăm să trimiteți un e-mail CEO-ului nostru și ea va rezolva personal problema dumneavoastră.</span></p>",
|
||||
"collect_feedback_description": "Colectați feedback complet despre produsul sau serviciul dumneavoastră.",
|
||||
@@ -2269,6 +2280,7 @@
|
||||
"csat_survey_question_3_headline": "Of, îmi pare rău! Există ceva ce putem face pentru a-ți îmbunătăți experiența?",
|
||||
"csat_survey_question_3_placeholder": "Tastează răspunsul aici...",
|
||||
"cta_description": "Afișează informații și solicită utilizatorilor să ia o acțiune specifică",
|
||||
"custom_survey_block_1_name": "Bloc 1",
|
||||
"custom_survey_description": "Creează un sondaj fără șablon.",
|
||||
"custom_survey_name": "Începe de la zero",
|
||||
"custom_survey_question_1_headline": "Ce ați dori să știți?",
|
||||
@@ -2355,7 +2367,6 @@
|
||||
"evaluate_a_product_idea_description": "Sondaj utilizatorilor despre idei de produse sau caracteristici. Obține rapid feedback.",
|
||||
"evaluate_a_product_idea_name": "Evaluează o Idee de Produs",
|
||||
"evaluate_a_product_idea_question_1_button_label": "S-o facem!",
|
||||
"evaluate_a_product_idea_question_1_dismiss_button_label": "Omite",
|
||||
"evaluate_a_product_idea_question_1_headline": "Ne place cum folosești $[projectName]! Am dori să discutăm despre o idee de funcționalitate. Ai un minut?",
|
||||
"evaluate_a_product_idea_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Respectăm timpul dumneavoastră și am păstrat-o scurt 🤸</span></p>",
|
||||
"evaluate_a_product_idea_question_2_headline": "Mulțumesc! Cât de dificil sau ușor este pentru tine să [PROBLEM AREA] astăzi?",
|
||||
@@ -2364,7 +2375,6 @@
|
||||
"evaluate_a_product_idea_question_3_headline": "Ce este cel mai dificil pentru tine când vine vorba de [PROBLEM AREA]?",
|
||||
"evaluate_a_product_idea_question_3_placeholder": "Tastează răspunsul aici...",
|
||||
"evaluate_a_product_idea_question_4_button_label": "Următorul",
|
||||
"evaluate_a_product_idea_question_4_dismiss_button_label": "Omite",
|
||||
"evaluate_a_product_idea_question_4_headline": "Lucrăm la o idee pentru a ajuta cu [PROBLEM AREA].",
|
||||
"evaluate_a_product_idea_question_4_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Introduceți aici conceptul scurt. Adăugați detalii necesare, dar păstrați-l concis și ușor de înțeles.</span></p>",
|
||||
"evaluate_a_product_idea_question_5_headline": "Cât de valoroasă ar fi această funcționalitate pentru tine?",
|
||||
@@ -2414,7 +2424,6 @@
|
||||
"feedback_box_question_2_headline": "Ce nu merge bine?",
|
||||
"feedback_box_question_2_subheader": "Cu cât mai multe detalii, cu atât mai bine :)",
|
||||
"feedback_box_question_3_button_label": "Da, notificați-mă",
|
||||
"feedback_box_question_3_dismiss_button_label": "Nu, mulţumesc",
|
||||
"feedback_box_question_3_headline": "Vrei să fii în temă?",
|
||||
"feedback_box_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Vom remedia această problemă cât mai curând posibil. Doriți să fiți notificat când am făcut-o?</span></p>",
|
||||
"feedback_box_question_4_button_label": "Solicitare funcționalitate",
|
||||
@@ -2439,7 +2448,6 @@
|
||||
"identify_sign_up_barriers_description": "Oferiți o reducere pentru a obține informații despre barierele de înscriere.",
|
||||
"identify_sign_up_barriers_name": "Identificați barierele de înscriere",
|
||||
"identify_sign_up_barriers_question_1_button_label": "Obține reducere de 10%",
|
||||
"identify_sign_up_barriers_question_1_dismiss_button_label": "Nu, mulţumesc",
|
||||
"identify_sign_up_barriers_question_1_headline": "Răspunde acestui scurt sondaj, primește 10% reducere!",
|
||||
"identify_sign_up_barriers_question_1_html": "Se pare că sunteți pe cale să vă înregistrați. Răspundeți la patru întrebări și obțineți 10% reducere la orice plan.",
|
||||
"identify_sign_up_barriers_question_2_headline": "Cât de probabil este să vă înscrieți pentru $[projectName]?",
|
||||
@@ -2462,7 +2470,6 @@
|
||||
"identify_sign_up_barriers_question_8_headline": "Vă rugăm să explicați:",
|
||||
"identify_sign_up_barriers_question_8_placeholder": "Tastează răspunsul aici...",
|
||||
"identify_sign_up_barriers_question_9_button_label": "Înregistrare",
|
||||
"identify_sign_up_barriers_question_9_dismiss_button_label": "Sari pentru moment",
|
||||
"identify_sign_up_barriers_question_9_headline": "Mulțumim! Iată codul tău: SIGNUPNOW10",
|
||||
"identify_sign_up_barriers_question_9_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Mulțumim mult pentru că ai luat timp pentru a împărtăși feedback 🙏</span></p>",
|
||||
"identify_upsell_opportunities_description": "Aflați cât timp economisește produsul dumneavoastră pentru utilizatori. Folosiți această informație pentru a face upsell.",
|
||||
@@ -2499,7 +2506,6 @@
|
||||
"improve_newsletter_content_question_2_headline": "Ce ar fi făcut ca newsletter-ul din această săptămână să fie mai util?",
|
||||
"improve_newsletter_content_question_2_placeholder": "Tastează răspunsul aici...",
|
||||
"improve_newsletter_content_question_3_button_label": "Bucuros să ajut!",
|
||||
"improve_newsletter_content_question_3_dismiss_button_label": "Găsește-ți proprii prieteni",
|
||||
"improve_newsletter_content_question_3_headline": "Mulțumim! ❤️ Răspândește iubirea către un prieten.",
|
||||
"improve_newsletter_content_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Cine gândește ca tine? Ne-ai face o mare favoare dacă ai împărtăși episodul acestei săptămâni cu prietenul tău de creier!</span></p>",
|
||||
"improve_trial_conversion_description": "Află de ce oamenii au încetat perioada de încercare. Aceste informații te ajută să îți îmbunătățești procesul de achiziție.",
|
||||
@@ -2514,7 +2520,6 @@
|
||||
"improve_trial_conversion_question_2_button_label": "Următorul",
|
||||
"improve_trial_conversion_question_2_headline": "Ne pare rău să auzim asta. Care a fost cea mai mare problemă folosind $[projectName]?",
|
||||
"improve_trial_conversion_question_4_button_label": "Obțineți 20% reducere",
|
||||
"improve_trial_conversion_question_4_dismiss_button_label": "Omite",
|
||||
"improve_trial_conversion_question_4_headline": "Ne pare rău să auzim asta! Obțineți 20% reducere în primul an.",
|
||||
"improve_trial_conversion_question_4_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Suntem bucuroși să vă oferim o reducere de 20% la un plan anual.</span></p>",
|
||||
"improve_trial_conversion_question_5_button_label": "Următorul",
|
||||
@@ -2678,7 +2683,6 @@
|
||||
"preview_survey_question_2_choice_2_label": "Nu, mulţumesc!",
|
||||
"preview_survey_question_2_headline": "Vrei să fii în temă?",
|
||||
"preview_survey_welcome_card_headline": "Bun venit!",
|
||||
"preview_survey_welcome_card_html": "Mulțumesc pentru feedback-ul dvs - să începem!",
|
||||
"prioritize_features_description": "Identificați caracteristicile de care utilizatorii dumneavoastră au cel mai mult și cel mai puțin nevoie.",
|
||||
"prioritize_features_name": "Prioritizați caracteristicile",
|
||||
"prioritize_features_question_1_choice_1": "Caracteristica 1",
|
||||
@@ -2704,7 +2708,6 @@
|
||||
"product_market_fit_superhuman": "Product Market Fit (Superhuman)",
|
||||
"product_market_fit_superhuman_description": "Măsurați PMF evaluând cât de dezamăgiți ar fi utilizatorii dacă produsul dvs. ar dispărea.",
|
||||
"product_market_fit_superhuman_question_1_button_label": "Bucuros să ajut!",
|
||||
"product_market_fit_superhuman_question_1_dismiss_button_label": "Nu, mulţumesc",
|
||||
"product_market_fit_superhuman_question_1_headline": "Ești unul dintre utilizatorii noștri fideli! Ai 5 minute?",
|
||||
"product_market_fit_superhuman_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Ne-ar plăcea să înțelegem mai bine experiența dvs. ca utilizator. Împărtășirea opiniei dvs. ajută foarte mult.</span></p>",
|
||||
"product_market_fit_superhuman_question_2_choice_1": "Deloc dezamăgit",
|
||||
@@ -2810,7 +2813,6 @@
|
||||
"site_abandonment_survey_description": "Înțelegeți motivele abandonării site-ului în magazinul dvs. online.",
|
||||
"site_abandonment_survey_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>Am observat că părăsiți site-ul nostru fără să faceți o achiziție. Ne-ar plăcea să înțelegem de ce.</span></p>",
|
||||
"site_abandonment_survey_question_2_button_label": "Desigur!",
|
||||
"site_abandonment_survey_question_2_dismiss_button_label": "Nu, mulţumesc",
|
||||
"site_abandonment_survey_question_2_headline": "Ai un minut?",
|
||||
"site_abandonment_survey_question_3_choice_1": "Nu găsesc ce caut",
|
||||
"site_abandonment_survey_question_3_choice_2": "Găsit un site mai bun",
|
||||
@@ -2835,7 +2837,6 @@
|
||||
"site_abandonment_survey_question_7_label": "Da, vă rog să-mi trimiteți mesaj.",
|
||||
"site_abandonment_survey_question_8_headline": "Vă rugăm să împărtășiți adresa de email:",
|
||||
"site_abandonment_survey_question_9_headline": "Comentarii sau sugestii suplimentare?",
|
||||
"skip": "Omite",
|
||||
"smileys_survey_name": "Sondaj Smileys",
|
||||
"smileys_survey_question_1_headline": "Cum îți place $[projectName]?",
|
||||
"smileys_survey_question_1_lower_label": "Nu este bine",
|
||||
|
||||
@@ -1206,9 +1206,9 @@
|
||||
"add": "添加 +",
|
||||
"add_a_delay_or_auto_close_the_survey": "添加 延迟 或 自动 关闭 调查",
|
||||
"add_a_four_digit_pin": "添加 一个 四 位 数 PIN",
|
||||
"add_a_new_question_to_your_survey": "添加一个新问题到您的调查中",
|
||||
"add_a_variable_to_calculate": "添加 变量 以 计算",
|
||||
"add_action_below": "在下面添加操作",
|
||||
"add_block": "添加区块",
|
||||
"add_choice_below": "在下方添加选项",
|
||||
"add_color_coding": "添加 颜色 编码",
|
||||
"add_color_coding_description": "添加 红色 、橙色 和 绿色 颜色 编码 到 选项。",
|
||||
@@ -1229,8 +1229,8 @@
|
||||
"add_other": "添加 \"其他\"",
|
||||
"add_photo_or_video": "添加 照片 或 视频",
|
||||
"add_pin": "添加 PIN",
|
||||
"add_question": "添加问题",
|
||||
"add_question_below": "在下面 添加 问题",
|
||||
"add_question_to_block": "添加问题到区块",
|
||||
"add_row": "添加 行",
|
||||
"add_variable": "添加 变量",
|
||||
"address_fields": "地址字段",
|
||||
@@ -1256,12 +1256,14 @@
|
||||
"automatically_mark_the_survey_as_complete_after": "自动 标记 调查 为 完成 在",
|
||||
"back_button_label": "\"返回\" 按钮标签",
|
||||
"background_styling": "背景 样式",
|
||||
"block_deleted": "区块已删除。",
|
||||
"block_duplicated": "区块已复制。",
|
||||
"bold": "粗体",
|
||||
"brand_color": "品牌 颜色",
|
||||
"brightness": "亮度",
|
||||
"button_external": "启用外部链接",
|
||||
"button_external_description": "添加一个按钮,在新标签页中打开外部URL",
|
||||
"button_label": "按钮标签",
|
||||
"button_to_continue_in_survey": "按钮 继续 调查",
|
||||
"button_to_link_to_external_url": "按钮 链接 到 外部 URL",
|
||||
"button_url": "按钮 URL",
|
||||
"cal_username": "Cal.com 用户名 或 用户名/事件",
|
||||
"calculate": "计算",
|
||||
@@ -1300,6 +1302,7 @@
|
||||
"character_limit_toggle_title": "添加 字符限制",
|
||||
"checkbox_label": "复选框 标签",
|
||||
"choose_the_actions_which_trigger_the_survey": "选择 触发 调查 的 动作 。",
|
||||
"choose_the_first_question_on_your_block": "选择区块中的第一个问题",
|
||||
"choose_where_to_run_the_survey": "选择 调查 运行 的 位置 。",
|
||||
"city": "城市",
|
||||
"close_survey_on_response_limit": "在响应限制时关闭 调查",
|
||||
@@ -1323,10 +1326,12 @@
|
||||
"create_group": "创建 群组",
|
||||
"create_your_own_survey": "创建 你 的 调查",
|
||||
"css_selector": "CSS 选择器",
|
||||
"cta_button_label": "“CTA”按钮标签",
|
||||
"custom_hostname": "自 定 义 主 机 名",
|
||||
"darken_or_lighten_background_of_your_choice": "根据 您 的 选择 暗化 或 亮化 背景。",
|
||||
"date_format": "日期格式",
|
||||
"days_before_showing_this_survey_again": "在显示此调查之前,需等待的天数。",
|
||||
"delete_block": "删除区块",
|
||||
"delete_choice": "删除 选择",
|
||||
"disable_the_visibility_of_survey_progress": "禁用问卷 进度 的可见性。",
|
||||
"display_an_estimate_of_completion_time_for_survey": "显示 调查 预计 完成 时间",
|
||||
@@ -1338,9 +1343,12 @@
|
||||
"does_not_include_all_of": "不包括所有 ",
|
||||
"does_not_include_one_of": "不包括一 个",
|
||||
"does_not_start_with": "不 以 开头",
|
||||
"duplicate_block": "复制区块",
|
||||
"duplicate_question": "复制问题",
|
||||
"edit_link": "编辑 链接",
|
||||
"edit_recall": "编辑 调用",
|
||||
"edit_translations": "编辑 {lang} 翻译",
|
||||
"element_not_found": "未找到问题",
|
||||
"enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "允许受访者在调查过程中随时切换语言。需要至少启用两种语言。",
|
||||
"enable_recaptcha_to_protect_your_survey_from_spam": "垃圾 邮件 保护 使用 reCAPTCHA v3 来 过滤 掉 垃圾 响应 。",
|
||||
"enable_spam_protection": "垃圾 邮件 保护",
|
||||
@@ -1416,11 +1424,12 @@
|
||||
"hidden_field_used_in_recall": "隐藏 字段 \"{hiddenField}\" 正在召回于问题 {questionIndex}。",
|
||||
"hidden_field_used_in_recall_ending_card": "隐藏 字段 \"{hiddenField}\" 正在召回于结束 卡",
|
||||
"hidden_field_used_in_recall_welcome": "隐藏 字段 \"{hiddenField}\" 正在召回于欢迎 卡 。",
|
||||
"hide_advanced_settings": "隐藏 高级设置",
|
||||
"hide_back_button": "隐藏 \"返回\" 按钮",
|
||||
"hide_back_button_description": "不 显示 调查 中 的 返回 按钮",
|
||||
"hide_block_settings": "隐藏区块设置",
|
||||
"hide_logo": "隐藏 徽标",
|
||||
"hide_progress_bar": "隐藏 进度 条",
|
||||
"hide_question_settings": "隐藏问题设置",
|
||||
"hide_the_logo_in_this_specific_survey": "隐藏此特定调查中的 logo",
|
||||
"hostname": "主 机 名",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "在 {surveyTypeDerived} 调查 中,您 想要 卡片 多么 有趣",
|
||||
@@ -1447,6 +1456,7 @@
|
||||
"is_clicked": "已点击",
|
||||
"is_completely_submitted": "已完全提交",
|
||||
"is_empty": "是 空",
|
||||
"is_not_clicked": "未点击",
|
||||
"is_not_empty": "不是 空",
|
||||
"is_not_set": "未设置",
|
||||
"is_partially_submitted": "部分提交",
|
||||
@@ -1454,7 +1464,7 @@
|
||||
"is_skipped": "已跳过",
|
||||
"is_submitted": "已提交",
|
||||
"italic": "斜体",
|
||||
"jump_to_question": "跳 转 到 问题",
|
||||
"jump_to_block": "跳转到区块",
|
||||
"keep_current_order": "保持 当前 顺序",
|
||||
"keep_showing_while_conditions_match": "条件 符合 时 保持 显示",
|
||||
"key": "键",
|
||||
@@ -1468,16 +1478,18 @@
|
||||
"logic_error_warning": "更改 将 导致 逻辑 错误",
|
||||
"logic_error_warning_text": "更改问题类型 会 移除 此问题 的 逻辑条件",
|
||||
"long_answer": "长答案",
|
||||
"long_answer_toggle_description": "允许受访者填写较长的多行答案。",
|
||||
"lower_label": "下限标签",
|
||||
"manage_languages": "管理 语言",
|
||||
"matrix_all_fields": "所有字段",
|
||||
"matrix_rows": "行",
|
||||
"max_file_size": "最大 文件 大小",
|
||||
"max_file_size_limit_is": "最大 文件 大小 限制 是",
|
||||
"move_question_to_block": "将问题移动到区块",
|
||||
"multiply": "乘 *",
|
||||
"needed_for_self_hosted_cal_com_instance": "需要用于 自建 Cal.com 实例",
|
||||
"next_block": "下一块",
|
||||
"next_button_label": "\"下一步\" 按钮标签",
|
||||
"next_question": "下一个问题",
|
||||
"no_hidden_fields_yet_add_first_one_below": "还没有隐藏字段。 在下面添加第一个。",
|
||||
"no_images_found_for": "未找到与 \"{query}\" 相关的图片",
|
||||
"no_languages_found_add_first_one_to_get_started": "没有找到语言。添加第一个以开始。",
|
||||
@@ -1589,17 +1601,17 @@
|
||||
"set_the_global_placement_in_the_look_feel_settings": "在外观 设置中 设置 全局 放置。",
|
||||
"settings_saved_successfully": "设置 保存 成功",
|
||||
"seven_points": "7 分",
|
||||
"show_advanced_settings": "显示 高级设置",
|
||||
"show_block_settings": "显示区块设置",
|
||||
"show_button": "显示 按钮",
|
||||
"show_language_switch": "显示 语言 切换",
|
||||
"show_multiple_times": "显示有限次数",
|
||||
"show_only_once": "仅 显示 一次",
|
||||
"show_question_settings": "显示问题设置",
|
||||
"show_survey_maximum_of": "显示 调查 最大 一次",
|
||||
"show_survey_to_users": "显示 问卷 给 % 的 用户",
|
||||
"show_to_x_percentage_of_targeted_users": "显示 给 {percentage}% 的 目标 用户",
|
||||
"simple": "简单",
|
||||
"six_points": "6 分",
|
||||
"skip_button_label": "\"跳过\" 按钮标签",
|
||||
"smiley": "笑脸",
|
||||
"spam_protection_note": "垃圾 邮件 保护 对于 与 iOS 、 React Native 和 Android SDK 一起 显示 的 调查 无效 。 它 将 破坏 调查。",
|
||||
"spam_protection_threshold_description": "设置 值 在 0 和 1 之间,响应 低于 此 值 将 被 拒绝。",
|
||||
@@ -1620,6 +1632,7 @@
|
||||
"survey_placement": "调查 放置",
|
||||
"survey_trigger": "调查 触发",
|
||||
"switch_multi_lanugage_on_to_get_started": "打开多语言以开始 👉",
|
||||
"target_block_not_found": "未找到目标区块",
|
||||
"targeted": "定位",
|
||||
"ten_points": "10 分",
|
||||
"the_survey_will_be_shown_multiple_times_until_they_respond": "最多显示指定次数,或直到他们回应(以先到者为准)。",
|
||||
@@ -1639,6 +1652,7 @@
|
||||
"unlock_targeting_title": "通过 更 高级 划解锁 定位",
|
||||
"unsaved_changes_warning": "您在调查中有未保存的更改。离开前是否要保存?",
|
||||
"until_they_submit_a_response": "持续显示直到提交回应",
|
||||
"untitled_block": "未命名区块",
|
||||
"upgrade_notice_description": "创建 多语言 调查 并 解锁 更多 功能",
|
||||
"upgrade_notice_title": "解锁 更高 计划 中 的 多语言 调查",
|
||||
"upload": "上传",
|
||||
@@ -2070,7 +2084,6 @@
|
||||
"card_abandonment_survey": "购物车 被遗弃 调查",
|
||||
"card_abandonment_survey_description": "了解 在 你的 网上商店 购物车 被遗弃 的 背后 原因。",
|
||||
"card_abandonment_survey_question_1_button_label": "确定!",
|
||||
"card_abandonment_survey_question_1_dismiss_button_label": "不,谢谢",
|
||||
"card_abandonment_survey_question_1_headline": "你有 2 分钟来帮助我们改进吗?",
|
||||
"card_abandonment_survey_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>我们 注意到 你在 购物车 中 留下 了一些 商品。我们 很 想 了解 为什么。</span></p>",
|
||||
"card_abandonment_survey_question_2_choice_1": "高 运输 成本",
|
||||
@@ -2157,12 +2170,10 @@
|
||||
"churn_survey_question_2_button_label": "发送",
|
||||
"churn_survey_question_2_headline": "什么 能 让 $[projectName] 更 加 容易 使用 ?",
|
||||
"churn_survey_question_3_button_label": "获取 30% 折扣",
|
||||
"churn_survey_question_3_dismiss_button_label": "跳过",
|
||||
"churn_survey_question_3_headline": "明 年 获 30% 优惠 !",
|
||||
"churn_survey_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>我们 希望 您 可以 继续 成为 我们 的 客户。很 乐意 为 您 提供 下 一年 30% 的 折扣。</span></p>",
|
||||
"churn_survey_question_4_headline": "您 缺少 什么 功能 ?",
|
||||
"churn_survey_question_5_button_label": "发送 邮件 给 CEO",
|
||||
"churn_survey_question_5_dismiss_button_label": "跳过",
|
||||
"churn_survey_question_5_headline": "很 遗憾 听到 😔 直接 与 我们 的 CEO 交谈 !",
|
||||
"churn_survey_question_5_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>我们 旨在 提供 最佳 的 客户服务。请 发送 电邮 给 我们 的 CEO,她 将 会 亲自 处理 您 的 问题。</span></p>",
|
||||
"collect_feedback_description": "收集有关产品或服务的全面反馈。",
|
||||
@@ -2269,6 +2280,7 @@
|
||||
"csat_survey_question_3_headline": "糟糕, 对不起!我们可以做些什么来改善您的体验?",
|
||||
"csat_survey_question_3_placeholder": "在此输入您的答案...",
|
||||
"cta_description": "显示 信息 并 提示用户采取 特定行动",
|
||||
"custom_survey_block_1_name": "模块 1",
|
||||
"custom_survey_description": "创建 一个 没有 模板 的 调查。",
|
||||
"custom_survey_name": "从零开始",
|
||||
"custom_survey_question_1_headline": "你 想 知道 什么?",
|
||||
@@ -2355,7 +2367,6 @@
|
||||
"evaluate_a_product_idea_description": "调查 用户关于 产品 或 功能 创意 。快速 获得 反馈 。",
|
||||
"evaluate_a_product_idea_name": "评估产品创意",
|
||||
"evaluate_a_product_idea_question_1_button_label": "干吧!",
|
||||
"evaluate_a_product_idea_question_1_dismiss_button_label": "跳过",
|
||||
"evaluate_a_product_idea_question_1_headline": "我们 喜欢 你 的 $[projectName] 使用 方式!我们 想 听听 你 对 功能 创意 的 想法。有 空 吗?",
|
||||
"evaluate_a_product_idea_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>我们 尊重 您 的 时间 并 简短 🤸</span></p>",
|
||||
"evaluate_a_product_idea_question_2_headline": "谢谢! 今天 [PROBLEM AREA] 对你来说 有多困难 或 容易?",
|
||||
@@ -2364,7 +2375,6 @@
|
||||
"evaluate_a_product_idea_question_3_headline": "在 [PROBLEM AREA] 方面,什么 是 你 最大 的 困难?",
|
||||
"evaluate_a_product_idea_question_3_placeholder": "在此输入您的答案...",
|
||||
"evaluate_a_product_idea_question_4_button_label": "下一步",
|
||||
"evaluate_a_product_idea_question_4_dismiss_button_label": "跳过",
|
||||
"evaluate_a_product_idea_question_4_headline": "我们 正在 研究 一个 想法,来 帮助 解决 [PROBLEM AREA]。",
|
||||
"evaluate_a_product_idea_question_4_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>在此处 插入 概念简介。 添加必要 细节 ,但 保持 简洁 易懂 。</span></p>",
|
||||
"evaluate_a_product_idea_question_5_headline": "这个 功能 对 你 的 价值 如何 ?",
|
||||
@@ -2414,7 +2424,6 @@
|
||||
"feedback_box_question_2_headline": "出了 什么 问题?",
|
||||
"feedback_box_question_2_subheader": "细节越多越好 :)",
|
||||
"feedback_box_question_3_button_label": "是的,通知我",
|
||||
"feedback_box_question_3_dismiss_button_label": "不,谢谢",
|
||||
"feedback_box_question_3_headline": "想 了解 最新信息吗?",
|
||||
"feedback_box_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>我们会 尽快 解决 此问题。 您 想 在 我们 解决 后 收到 通知 吗?</span></p>",
|
||||
"feedback_box_question_4_button_label": "请求功能",
|
||||
@@ -2439,7 +2448,6 @@
|
||||
"identify_sign_up_barriers_description": "提供折扣以收集有关 注册障碍 的见解。",
|
||||
"identify_sign_up_barriers_name": "识别 注册 障碍",
|
||||
"identify_sign_up_barriers_question_1_button_label": "获取 10% 折扣",
|
||||
"identify_sign_up_barriers_question_1_dismiss_button_label": "不,谢谢",
|
||||
"identify_sign_up_barriers_question_1_headline": "回答 这项 简短 调查 ,享受 9 折 优惠 !",
|
||||
"identify_sign_up_barriers_question_1_html": "您 似乎 正在 考虑 注册。回答 四个 问题,任意 计划 打折 10%。",
|
||||
"identify_sign_up_barriers_question_2_headline": "您有多大可能性会注册 $[projectName] ?",
|
||||
@@ -2462,7 +2470,6 @@
|
||||
"identify_sign_up_barriers_question_8_headline": "请解释:",
|
||||
"identify_sign_up_barriers_question_8_placeholder": "在此输入您的答案...",
|
||||
"identify_sign_up_barriers_question_9_button_label": "注册",
|
||||
"identify_sign_up_barriers_question_9_dismiss_button_label": "暂时跳过",
|
||||
"identify_sign_up_barriers_question_9_headline": "谢谢!这是 你的 代码:SIGNUPNOW10",
|
||||
"identify_sign_up_barriers_question_9_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>非常 感谢 您 抽出 时间 分享 反馈 🙏</span></p>",
|
||||
"identify_upsell_opportunities_description": "找出 你的 产品 为 用户 节省 了 多少 时间。 用 它 来 促进 销售 。",
|
||||
@@ -2499,7 +2506,6 @@
|
||||
"improve_newsletter_content_question_2_headline": "这 期 的 时事通讯 需 要 哪 些 改进 会 更 有 帮助?",
|
||||
"improve_newsletter_content_question_2_placeholder": "在 此 输入 您 的 答案...",
|
||||
"improve_newsletter_content_question_3_button_label": "乐意帮忙!",
|
||||
"improve_newsletter_content_question_3_dismiss_button_label": "寻找 你 的 朋友",
|
||||
"improve_newsletter_content_question_3_headline": "谢谢!❤️ 传播 给 一个 朋友。",
|
||||
"improve_newsletter_content_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>谁 和 你 想法 相似? 如果 你 能 将 本周 的 节目 分享 给 你 的 智力 朋友,那 就 太 帮 了 我们 一个 大忙 了!</span></p>",
|
||||
"improve_trial_conversion_description": "找出用户为何停止试用。这些洞察有助于优化您的引导漏斗。",
|
||||
@@ -2514,7 +2520,6 @@
|
||||
"improve_trial_conversion_question_2_button_label": "下一步",
|
||||
"improve_trial_conversion_question_2_headline": "很抱歉 听到。使用 $[projectName] 时 最大的 问题 是 什么?",
|
||||
"improve_trial_conversion_question_4_button_label": "获取 20% 折扣",
|
||||
"improve_trial_conversion_question_4_dismiss_button_label": "跳过",
|
||||
"improve_trial_conversion_question_4_headline": "很抱歉 听到!首年 可 获 20% 优惠。",
|
||||
"improve_trial_conversion_question_4_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>我们 很 乐意 为 您 提供 年 度 计划 20% 的 折扣。</span></p>",
|
||||
"improve_trial_conversion_question_5_button_label": "下一步",
|
||||
@@ -2678,7 +2683,6 @@
|
||||
"preview_survey_question_2_choice_2_label": "不,谢谢!",
|
||||
"preview_survey_question_2_headline": "想 了解 最新信息吗?",
|
||||
"preview_survey_welcome_card_headline": "欢迎!",
|
||||
"preview_survey_welcome_card_html": "感谢 提供 您 的 反馈 - 一起 出发!",
|
||||
"prioritize_features_description": "确定 用户 最 需要 和 最 不 需要 的 功能。",
|
||||
"prioritize_features_name": "优先 功能",
|
||||
"prioritize_features_question_1_choice_1": "功能 1",
|
||||
@@ -2704,7 +2708,6 @@
|
||||
"product_market_fit_superhuman": "产品 市场 适配 (Superhuman)",
|
||||
"product_market_fit_superhuman_description": "评估 如果 你的 产品 消失,用户 会有 多 失望 来 衡量 产品市场适配。",
|
||||
"product_market_fit_superhuman_question_1_button_label": "乐意帮忙!",
|
||||
"product_market_fit_superhuman_question_1_dismiss_button_label": "不,谢谢",
|
||||
"product_market_fit_superhuman_question_1_headline": "您是我们的重度用户!您有 5 分钟吗?",
|
||||
"product_market_fit_superhuman_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>我们 很 想 更好 地 了解 你的 用户 体验。 分享 你的 洞察 助益 良多。</span></p>",
|
||||
"product_market_fit_superhuman_question_2_choice_1": "一点也不失望",
|
||||
@@ -2810,7 +2813,6 @@
|
||||
"site_abandonment_survey_description": "了解 在 你的 网上商店 网站 被放弃 的 原因。",
|
||||
"site_abandonment_survey_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>我们 注意到 你 正在 离开 我们 的 网站 而 没有 进行 购买。我们 很 想 了解 为什么。</span></p>",
|
||||
"site_abandonment_survey_question_2_button_label": "确定!",
|
||||
"site_abandonment_survey_question_2_dismiss_button_label": "不,谢谢",
|
||||
"site_abandonment_survey_question_2_headline": "你 有 空 吗?",
|
||||
"site_abandonment_survey_question_3_choice_1": "找不到 我 要 找 的 东西",
|
||||
"site_abandonment_survey_question_3_choice_2": "找到了 更好的 网站",
|
||||
@@ -2835,7 +2837,6 @@
|
||||
"site_abandonment_survey_question_7_label": "是 的, 请 联系 我。",
|
||||
"site_abandonment_survey_question_8_headline": "请 分享您的 电子邮件地址:",
|
||||
"site_abandonment_survey_question_9_headline": "还有其他的意见或建议吗?",
|
||||
"skip": "跳过",
|
||||
"smileys_survey_name": "笑脸 调查",
|
||||
"smileys_survey_question_1_headline": "您 如何 喜欢 $[projectName]?",
|
||||
"smileys_survey_question_1_lower_label": "不 好",
|
||||
|
||||
@@ -1206,9 +1206,9 @@
|
||||
"add": "新增 +",
|
||||
"add_a_delay_or_auto_close_the_survey": "新增延遲或自動關閉問卷",
|
||||
"add_a_four_digit_pin": "新增四位數 PIN 碼",
|
||||
"add_a_new_question_to_your_survey": "在您的問卷中新增一個新問題",
|
||||
"add_a_variable_to_calculate": "新增要計算的變數",
|
||||
"add_action_below": "在下方新增操作",
|
||||
"add_block": "新增區塊",
|
||||
"add_choice_below": "在下方新增選項",
|
||||
"add_color_coding": "新增顏色編碼",
|
||||
"add_color_coding_description": "為選項新增紅色、橘色和綠色顏色代碼。",
|
||||
@@ -1229,8 +1229,8 @@
|
||||
"add_other": "新增「其他」",
|
||||
"add_photo_or_video": "新增照片或影片",
|
||||
"add_pin": "新增 PIN 碼",
|
||||
"add_question": "新增問題",
|
||||
"add_question_below": "在下方新增問題",
|
||||
"add_question_to_block": "新增問題到區塊",
|
||||
"add_row": "新增列",
|
||||
"add_variable": "新增變數",
|
||||
"address_fields": "地址欄位",
|
||||
@@ -1256,12 +1256,14 @@
|
||||
"automatically_mark_the_survey_as_complete_after": "在指定時間後自動將問卷標記為完成",
|
||||
"back_button_label": "「返回」按鈕標籤",
|
||||
"background_styling": "背景樣式設定",
|
||||
"block_deleted": "區塊已刪除。",
|
||||
"block_duplicated": "區塊已複製。",
|
||||
"bold": "粗體",
|
||||
"brand_color": "品牌顏色",
|
||||
"brightness": "亮度",
|
||||
"button_external": "啟用外部連結",
|
||||
"button_external_description": "新增一個按鈕,在新分頁中開啟外部網址",
|
||||
"button_label": "按鈕標籤",
|
||||
"button_to_continue_in_survey": "問卷中繼續的按鈕",
|
||||
"button_to_link_to_external_url": "連結到外部網址的按鈕",
|
||||
"button_url": "按鈕網址",
|
||||
"cal_username": "Cal.com 使用者名稱或使用者名稱/事件",
|
||||
"calculate": "計算",
|
||||
@@ -1300,6 +1302,7 @@
|
||||
"character_limit_toggle_title": "新增字元限制",
|
||||
"checkbox_label": "核取方塊標籤",
|
||||
"choose_the_actions_which_trigger_the_survey": "選擇觸發問卷的操作。",
|
||||
"choose_the_first_question_on_your_block": "選擇此區塊的第一個問題",
|
||||
"choose_where_to_run_the_survey": "選擇在哪裡執行問卷。",
|
||||
"city": "城市",
|
||||
"close_survey_on_response_limit": "在回應次數上限關閉問卷",
|
||||
@@ -1323,10 +1326,12 @@
|
||||
"create_group": "建立群組",
|
||||
"create_your_own_survey": "建立您自己的問卷",
|
||||
"css_selector": "CSS 選取器",
|
||||
"cta_button_label": "「CTA」按鈕標籤",
|
||||
"custom_hostname": "自訂主機名稱",
|
||||
"darken_or_lighten_background_of_your_choice": "變暗或變亮您選擇的背景。",
|
||||
"date_format": "日期格式",
|
||||
"days_before_showing_this_survey_again": "在顯示此問卷之前,需等待其他問卷顯示後的天數。",
|
||||
"delete_block": "刪除區塊",
|
||||
"delete_choice": "刪除選項",
|
||||
"disable_the_visibility_of_survey_progress": "停用問卷進度的可見性。",
|
||||
"display_an_estimate_of_completion_time_for_survey": "顯示問卷的估計完成時間",
|
||||
@@ -1338,9 +1343,12 @@
|
||||
"does_not_include_all_of": "不包含全部",
|
||||
"does_not_include_one_of": "不包含其中之一",
|
||||
"does_not_start_with": "不以...開頭",
|
||||
"duplicate_block": "複製區塊",
|
||||
"duplicate_question": "複製問題",
|
||||
"edit_link": "編輯 連結",
|
||||
"edit_recall": "編輯回憶",
|
||||
"edit_translations": "編輯 '{'language'}' 翻譯",
|
||||
"element_not_found": "找不到問題",
|
||||
"enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "允許受訪者隨時切換語言。需要至少啟用兩種語言。",
|
||||
"enable_recaptcha_to_protect_your_survey_from_spam": "垃圾郵件保護使用 reCAPTCHA v3 過濾垃圾回應。",
|
||||
"enable_spam_protection": "垃圾郵件保護",
|
||||
@@ -1416,11 +1424,12 @@
|
||||
"hidden_field_used_in_recall": "隱藏欄位 \"{hiddenField}\" 於問題 {questionIndex} 中被召回。",
|
||||
"hidden_field_used_in_recall_ending_card": "隱藏欄位 \"{hiddenField}\" 於結束卡中被召回。",
|
||||
"hidden_field_used_in_recall_welcome": "隱藏欄位 \"{hiddenField}\" 於歡迎卡中被召回。",
|
||||
"hide_advanced_settings": "隱藏進階設定",
|
||||
"hide_back_button": "隱藏「Back」按鈕",
|
||||
"hide_back_button_description": "不要在問卷中顯示返回按鈕",
|
||||
"hide_block_settings": "隱藏區塊設定",
|
||||
"hide_logo": "隱藏標誌",
|
||||
"hide_progress_bar": "隱藏進度列",
|
||||
"hide_question_settings": "隱藏問題設定",
|
||||
"hide_the_logo_in_this_specific_survey": "在此特定問卷中隱藏標誌",
|
||||
"hostname": "主機名稱",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "您希望 '{'surveyTypeDerived'}' 問卷中的卡片有多酷炫",
|
||||
@@ -1447,6 +1456,7 @@
|
||||
"is_clicked": "已點擊",
|
||||
"is_completely_submitted": "已完全提交",
|
||||
"is_empty": "是空的",
|
||||
"is_not_clicked": "未點擊",
|
||||
"is_not_empty": "不是空的",
|
||||
"is_not_set": "未設定",
|
||||
"is_partially_submitted": "已部分提交",
|
||||
@@ -1454,7 +1464,7 @@
|
||||
"is_skipped": "已跳過",
|
||||
"is_submitted": "已提交",
|
||||
"italic": "斜體",
|
||||
"jump_to_question": "跳至問題",
|
||||
"jump_to_block": "跳至區塊",
|
||||
"keep_current_order": "保留目前順序",
|
||||
"keep_showing_while_conditions_match": "在條件符合時持續顯示",
|
||||
"key": "金鑰",
|
||||
@@ -1468,16 +1478,18 @@
|
||||
"logic_error_warning": "變更將導致邏輯錯誤",
|
||||
"logic_error_warning_text": "變更問題類型將會從此問題中移除邏輯條件",
|
||||
"long_answer": "長回答",
|
||||
"long_answer_toggle_description": "允許受訪者撰寫較長的多行回答。",
|
||||
"lower_label": "下標籤",
|
||||
"manage_languages": "管理語言",
|
||||
"matrix_all_fields": "所有欄位",
|
||||
"matrix_rows": "列",
|
||||
"max_file_size": "最大檔案大小",
|
||||
"max_file_size_limit_is": "最大檔案大小限制為",
|
||||
"move_question_to_block": "將問題移至區塊",
|
||||
"multiply": "乘 *",
|
||||
"needed_for_self_hosted_cal_com_instance": "自行託管 Cal.com 執行個體時需要",
|
||||
"next_block": "下一個區塊",
|
||||
"next_button_label": "「下一步」按鈕標籤",
|
||||
"next_question": "下一個問題",
|
||||
"no_hidden_fields_yet_add_first_one_below": "尚無隱藏欄位。在下方新增第一個隱藏欄位。",
|
||||
"no_images_found_for": "找不到「'{'query'}'」的圖片",
|
||||
"no_languages_found_add_first_one_to_get_started": "找不到語言。新增第一個語言以開始使用。",
|
||||
@@ -1589,17 +1601,17 @@
|
||||
"set_the_global_placement_in_the_look_feel_settings": "在「外觀與風格」設定中設定整體位置。",
|
||||
"settings_saved_successfully": "設定已成功儲存",
|
||||
"seven_points": "7 分",
|
||||
"show_advanced_settings": "顯示進階設定",
|
||||
"show_block_settings": "顯示區塊設定",
|
||||
"show_button": "顯示按鈕",
|
||||
"show_language_switch": "顯示語言切換",
|
||||
"show_multiple_times": "顯示有限次數",
|
||||
"show_only_once": "僅顯示一次",
|
||||
"show_question_settings": "顯示問題設定",
|
||||
"show_survey_maximum_of": "最多顯示問卷",
|
||||
"show_survey_to_users": "將問卷顯示給 % 的使用者",
|
||||
"show_to_x_percentage_of_targeted_users": "顯示給 '{'percentage'}'% 的目標使用者",
|
||||
"simple": "簡單",
|
||||
"six_points": "6 分",
|
||||
"skip_button_label": "「跳過」按鈕標籤",
|
||||
"smiley": "表情符號",
|
||||
"spam_protection_note": "垃圾郵件保護不適用於使用 iOS、React Native 和 Android SDK 顯示的問卷。它會破壞問卷。",
|
||||
"spam_protection_threshold_description": "設置值在 0 和 1 之間,低於此值的回應將被拒絕。",
|
||||
@@ -1620,6 +1632,7 @@
|
||||
"survey_placement": "問卷位置",
|
||||
"survey_trigger": "問卷觸發器",
|
||||
"switch_multi_lanugage_on_to_get_started": "開啟多語言以開始使用 👉",
|
||||
"target_block_not_found": "找不到目標區塊",
|
||||
"targeted": "目標",
|
||||
"ten_points": "10 分",
|
||||
"the_survey_will_be_shown_multiple_times_until_they_respond": "最多顯示指定次數,或直到他們回應(以先達成者為準)。",
|
||||
@@ -1639,6 +1652,7 @@
|
||||
"unlock_targeting_title": "使用更高等級的方案解鎖目標設定",
|
||||
"unsaved_changes_warning": "您的問卷中有未儲存的變更。您要先儲存它們再離開嗎?",
|
||||
"until_they_submit_a_response": "持續詢問直到提交回應",
|
||||
"untitled_block": "未命名區塊",
|
||||
"upgrade_notice_description": "建立多語言問卷並解鎖更多功能",
|
||||
"upgrade_notice_title": "使用更高等級的方案解鎖多語言問卷",
|
||||
"upload": "上傳",
|
||||
@@ -2070,7 +2084,6 @@
|
||||
"card_abandonment_survey": "購物車放棄問卷",
|
||||
"card_abandonment_survey_description": "瞭解您網路商店中購物車放棄的原因。",
|
||||
"card_abandonment_survey_question_1_button_label": "當然!",
|
||||
"card_abandonment_survey_question_1_dismiss_button_label": "不用了,謝謝。",
|
||||
"card_abandonment_survey_question_1_headline": "您有 2 分鐘的時間來協助我們改進嗎?",
|
||||
"card_abandonment_survey_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>我們注意到您在購物車中留下了一些商品。我們很想瞭解原因。</span></p>",
|
||||
"card_abandonment_survey_question_2_choice_1": "運費高昂",
|
||||
@@ -2157,12 +2170,10 @@
|
||||
"churn_survey_question_2_button_label": "發送",
|
||||
"churn_survey_question_2_headline": "是什麼讓 {projectName} 更易於使用?",
|
||||
"churn_survey_question_3_button_label": "獲得 30% 折扣",
|
||||
"churn_survey_question_3_dismiss_button_label": "跳過",
|
||||
"churn_survey_question_3_headline": "在未來一年獲得 30% 的折扣!",
|
||||
"churn_survey_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>我們很樂意讓您成為客戶。我們很樂意在未來一年提供 30% 的折扣。</span></p>",
|
||||
"churn_survey_question_4_headline": "您缺少哪些功能?",
|
||||
"churn_survey_question_5_button_label": "發送電子郵件給 CEO",
|
||||
"churn_survey_question_5_dismiss_button_label": "跳過",
|
||||
"churn_survey_question_5_headline": "很抱歉聽到 😔 直接與我們的 CEO 對話!",
|
||||
"churn_survey_question_5_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>我們旨在提供最佳的客戶服務。請發送電子郵件給我們的 CEO,她將親自處理您的問題。</span></p>",
|
||||
"collect_feedback_description": "收集有關您的產品或服務的全面回饋。",
|
||||
@@ -2269,6 +2280,7 @@
|
||||
"csat_survey_question_3_headline": "唉,抱歉!我們是否有任何可以改善您體驗的地方?",
|
||||
"csat_survey_question_3_placeholder": "在此輸入您的答案...",
|
||||
"cta_description": "顯示資訊並提示使用者採取特定操作",
|
||||
"custom_survey_block_1_name": "區塊 1",
|
||||
"custom_survey_description": "建立沒有範本的問卷。",
|
||||
"custom_survey_name": "從頭開始",
|
||||
"custom_survey_question_1_headline": "您想瞭解什麼?",
|
||||
@@ -2355,7 +2367,6 @@
|
||||
"evaluate_a_product_idea_description": "調查使用者對產品或功能想法的意見。快速取得回饋。",
|
||||
"evaluate_a_product_idea_name": "評估產品想法",
|
||||
"evaluate_a_product_idea_question_1_button_label": "開始吧!",
|
||||
"evaluate_a_product_idea_question_1_dismiss_button_label": "跳過",
|
||||
"evaluate_a_product_idea_question_1_headline": "我們喜歡您使用 {projectName} 的方式!我們很樂意請教您一個功能想法。您有時間嗎?",
|
||||
"evaluate_a_product_idea_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>我們尊重您的時間,並盡量簡短 🤸</span></p>",
|
||||
"evaluate_a_product_idea_question_2_headline": "謝謝!您今天達成 [問題區域] 的難易程度如何?",
|
||||
@@ -2364,7 +2375,6 @@
|
||||
"evaluate_a_product_idea_question_3_headline": "當您處理 [問題區域] 時,最困難的事情是什麼?",
|
||||
"evaluate_a_product_idea_question_3_placeholder": "在此輸入您的答案...",
|
||||
"evaluate_a_product_idea_question_4_button_label": "下一步",
|
||||
"evaluate_a_product_idea_question_4_dismiss_button_label": "跳過",
|
||||
"evaluate_a_product_idea_question_4_headline": "我們正在努力解決協助處理 [問題區域] 的想法。",
|
||||
"evaluate_a_product_idea_question_4_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>在此處插入概念簡介。新增必要的詳細資料,但保持簡潔易懂。</span></p>",
|
||||
"evaluate_a_product_idea_question_5_headline": "此功能對您有多大的價值?",
|
||||
@@ -2414,7 +2424,6 @@
|
||||
"feedback_box_question_2_headline": "哪裡壞了?",
|
||||
"feedback_box_question_2_subheader": "越詳細越好 :)",
|
||||
"feedback_box_question_3_button_label": "是,通知我",
|
||||
"feedback_box_question_3_dismiss_button_label": "不用了,謝謝",
|
||||
"feedback_box_question_3_headline": "要隨時掌握最新資訊嗎?",
|
||||
"feedback_box_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>我們將盡快修復此問題。您想要在我們完成修復時收到通知嗎?</span></p>",
|
||||
"feedback_box_question_4_button_label": "要求功能",
|
||||
@@ -2439,7 +2448,6 @@
|
||||
"identify_sign_up_barriers_description": "提供折扣以收集有關註冊障礙的洞察。",
|
||||
"identify_sign_up_barriers_name": "識別註冊障礙",
|
||||
"identify_sign_up_barriers_question_1_button_label": "獲得 10% 折扣",
|
||||
"identify_sign_up_barriers_question_1_dismiss_button_label": "不用了,謝謝",
|
||||
"identify_sign_up_barriers_question_1_headline": "回答這個簡短的問卷,即可獲得 10% 的折扣!",
|
||||
"identify_sign_up_barriers_question_1_html": "您似乎正在考慮註冊。回答四個問題,即可在任何方案中獲得 10% 的折扣。",
|
||||
"identify_sign_up_barriers_question_2_headline": "您註冊 {projectName} 的可能性有多高?",
|
||||
@@ -2462,7 +2470,6 @@
|
||||
"identify_sign_up_barriers_question_8_headline": "請說明:",
|
||||
"identify_sign_up_barriers_question_8_placeholder": "在此輸入您的答案...",
|
||||
"identify_sign_up_barriers_question_9_button_label": "註冊",
|
||||
"identify_sign_up_barriers_question_9_dismiss_button_label": "暫時跳過",
|
||||
"identify_sign_up_barriers_question_9_headline": "謝謝!這是您的程式碼:SIGNUPNOW10",
|
||||
"identify_sign_up_barriers_question_9_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>非常感謝您撥冗分享回饋 🙏</span></p>",
|
||||
"identify_upsell_opportunities_description": "找出您的產品為使用者節省了多少時間。使用它來追加銷售。",
|
||||
@@ -2499,7 +2506,6 @@
|
||||
"improve_newsletter_content_question_2_headline": "是什麼讓本週的電子報更有幫助?",
|
||||
"improve_newsletter_content_question_2_placeholder": "在此輸入您的答案...",
|
||||
"improve_newsletter_content_question_3_button_label": "樂意協助!",
|
||||
"improve_newsletter_content_question_3_dismiss_button_label": "自己找朋友",
|
||||
"improve_newsletter_content_question_3_headline": "謝謝!❤️ 與一位朋友分享。",
|
||||
"improve_newsletter_content_question_3_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>誰的想法和您一樣?如果您與您的一位好朋友分享本週的內容,這會對我們有很大幫助!</span></p>",
|
||||
"improve_trial_conversion_description": "找出人們停止試用的原因。這些洞察可幫助您改善轉換程序。",
|
||||
@@ -2514,7 +2520,6 @@
|
||||
"improve_trial_conversion_question_2_button_label": "下一步",
|
||||
"improve_trial_conversion_question_2_headline": "很抱歉聽到。使用 {projectName} 時,最大的問題是什麼?",
|
||||
"improve_trial_conversion_question_4_button_label": "獲得 20% 折扣",
|
||||
"improve_trial_conversion_question_4_dismiss_button_label": "跳過",
|
||||
"improve_trial_conversion_question_4_headline": "很抱歉聽到!在第一年獲得 20% 的折扣。",
|
||||
"improve_trial_conversion_question_4_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>我們很樂意為您提供年度方案的 20% 折扣。</span></p>",
|
||||
"improve_trial_conversion_question_5_button_label": "下一步",
|
||||
@@ -2678,7 +2683,6 @@
|
||||
"preview_survey_question_2_choice_2_label": "不用了,謝謝!",
|
||||
"preview_survey_question_2_headline": "想要緊跟最新動態嗎?",
|
||||
"preview_survey_welcome_card_headline": "歡迎!",
|
||||
"preview_survey_welcome_card_html": "感謝您提供回饋 - 開始吧!",
|
||||
"prioritize_features_description": "找出您的使用者最需要和最不需要的功能。",
|
||||
"prioritize_features_name": "優先排序功能",
|
||||
"prioritize_features_question_1_choice_1": "功能 1",
|
||||
@@ -2704,7 +2708,6 @@
|
||||
"product_market_fit_superhuman": "產品市場匹配度 (Superhuman)",
|
||||
"product_market_fit_superhuman_description": "藉由評估使用者在您的產品消失時會有多失望來衡量 PMF。",
|
||||
"product_market_fit_superhuman_question_1_button_label": "樂意協助!",
|
||||
"product_market_fit_superhuman_question_1_dismiss_button_label": "不用了,謝謝。",
|
||||
"product_market_fit_superhuman_question_1_headline": "您是我們的進階使用者之一!您有 5 分鐘的時間嗎?",
|
||||
"product_market_fit_superhuman_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>我們很樂意更瞭解您的使用者體驗。分享您的洞察力有很大幫助。</span></p>",
|
||||
"product_market_fit_superhuman_question_2_choice_1": "完全不會失望",
|
||||
@@ -2810,7 +2813,6 @@
|
||||
"site_abandonment_survey_description": "瞭解您網站商店中網站放棄的原因。",
|
||||
"site_abandonment_survey_question_1_html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>我們注意到您在未進行購買的情況下離開了我們的網站。我們很想瞭解原因。</span></p>",
|
||||
"site_abandonment_survey_question_2_button_label": "當然!",
|
||||
"site_abandonment_survey_question_2_dismiss_button_label": "不用了,謝謝。",
|
||||
"site_abandonment_survey_question_2_headline": "您有時間嗎?",
|
||||
"site_abandonment_survey_question_3_choice_1": "找不到我要找的東西",
|
||||
"site_abandonment_survey_question_3_choice_2": "找到更好的網站",
|
||||
@@ -2835,7 +2837,6 @@
|
||||
"site_abandonment_survey_question_7_label": "是的,請聯絡我。",
|
||||
"site_abandonment_survey_question_8_headline": "請分享您的電子郵件地址:",
|
||||
"site_abandonment_survey_question_9_headline": "任何其他意見或建議?",
|
||||
"skip": "跳過",
|
||||
"smileys_survey_name": "表情符號問卷",
|
||||
"smileys_survey_question_1_headline": "您覺得 {projectName} 如何?",
|
||||
"smileys_survey_question_1_lower_label": "不好",
|
||||
|
||||
+21
-21
@@ -3,42 +3,42 @@
|
||||
import { CheckCircle2Icon, ChevronsDownIcon, XCircleIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TResponseData } from "@formbricks/types/responses";
|
||||
import { TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { parseRecallInfo } from "@/lib/utils/recall";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||
|
||||
interface QuestionSkipProps {
|
||||
skippedQuestions: string[] | undefined;
|
||||
interface ElementSkipProps {
|
||||
skippedElements: string[] | undefined;
|
||||
status: string;
|
||||
questions: TSurveyQuestion[];
|
||||
isFirstQuestionAnswered?: boolean;
|
||||
elements: TSurveyElement[];
|
||||
isFirstElementAnswered?: boolean;
|
||||
responseData: TResponseData;
|
||||
}
|
||||
|
||||
export const QuestionSkip = ({
|
||||
skippedQuestions,
|
||||
export const ElementSkip = ({
|
||||
skippedElements,
|
||||
status,
|
||||
questions,
|
||||
isFirstQuestionAnswered,
|
||||
elements,
|
||||
isFirstElementAnswered,
|
||||
responseData,
|
||||
}: QuestionSkipProps) => {
|
||||
}: ElementSkipProps) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
{skippedQuestions && (
|
||||
{skippedElements && (
|
||||
<div className="my-2 flex w-full px-2 text-sm text-slate-400">
|
||||
{status === "welcomeCard" && (
|
||||
<div className="mb-2 flex">
|
||||
{
|
||||
<div
|
||||
className={`relative flex ${
|
||||
isFirstQuestionAnswered ? "h-[100%]" : "h-[200%]"
|
||||
isFirstElementAnswered ? "h-[100%]" : "h-[200%]"
|
||||
} w-0.5 items-center justify-center`}
|
||||
style={{
|
||||
background:
|
||||
"repeating-linear-gradient(rgb(148, 163, 184), rgb(148, 163, 184) 5px, transparent 5px, transparent 8px)", // adjust the values to fit your design
|
||||
"repeating-linear-gradient(rgb(148, 163, 184), rgb(148, 163, 184) 5px, transparent 5px, transparent 8px)",
|
||||
}}>
|
||||
<CheckCircle2Icon className="p-0.25 absolute top-0 w-[1.5rem] min-w-[1.5rem] rounded-full bg-white text-slate-400" />
|
||||
</div>
|
||||
@@ -52,9 +52,9 @@ export const QuestionSkip = ({
|
||||
className="flex w-0.5 items-center justify-center"
|
||||
style={{
|
||||
background:
|
||||
"repeating-linear-gradient(to bottom, rgb(148 163 184), rgb(148 163 184) 8px, transparent 5px, transparent 15px)", // adjust the values to fit your design
|
||||
"repeating-linear-gradient(to bottom, rgb(148 163 184), rgb(148 163 184) 8px, transparent 5px, transparent 15px)",
|
||||
}}>
|
||||
{skippedQuestions.length > 1 && (
|
||||
{skippedElements.length > 1 && (
|
||||
<TooltipProvider delayDuration={50}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -70,13 +70,13 @@ export const QuestionSkip = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-6 flex flex-col">
|
||||
{skippedQuestions?.map((questionId) => {
|
||||
{skippedElements?.map((questionId) => {
|
||||
return (
|
||||
<p className="my-2" key={questionId}>
|
||||
{getTextContent(
|
||||
parseRecallInfo(
|
||||
getLocalizedValue(
|
||||
questions.find((question) => question.id === questionId)?.headline ?? {
|
||||
elements.find((question) => question.id === questionId)?.headline ?? {
|
||||
default: "",
|
||||
},
|
||||
"default"
|
||||
@@ -96,7 +96,7 @@ export const QuestionSkip = ({
|
||||
className="flex w-0.5 flex-grow items-start justify-center"
|
||||
style={{
|
||||
background:
|
||||
"repeating-linear-gradient(to bottom, rgb(148 163 184), rgb(148 163 184) 2px, transparent 2px, transparent 10px)", // adjust the 2px to change dot size and 10px to change space between dots
|
||||
"repeating-linear-gradient(to bottom, rgb(148 163 184), rgb(148 163 184) 2px, transparent 2px, transparent 10px)",
|
||||
}}>
|
||||
<div className="flex">
|
||||
<XCircleIcon className="min-h-[1.5rem] min-w-[1.5rem] rounded-full bg-white text-slate-400" />
|
||||
@@ -108,14 +108,14 @@ export const QuestionSkip = ({
|
||||
className="mb-2 w-fit rounded-lg bg-slate-100 px-2 font-medium text-slate-700">
|
||||
{t("environments.surveys.responses.survey_closed")}
|
||||
</p>
|
||||
{skippedQuestions &&
|
||||
skippedQuestions.map((questionId) => {
|
||||
{skippedElements &&
|
||||
skippedElements.map((questionId) => {
|
||||
return (
|
||||
<p className="my-2" key={questionId}>
|
||||
{getTextContent(
|
||||
parseRecallInfo(
|
||||
getLocalizedValue(
|
||||
questions.find((question) => question.id === questionId)?.headline ?? {
|
||||
elements.find((question) => question.id === questionId)?.headline ?? {
|
||||
default: "",
|
||||
},
|
||||
"default"
|
||||
+26
-33
@@ -1,14 +1,8 @@
|
||||
import { CheckCheckIcon, MousePointerClickIcon, PhoneIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { TResponseDataValue } from "@formbricks/types/responses";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyMatrixQuestion,
|
||||
TSurveyPictureSelectionQuestion,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
TSurveyRatingQuestion,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { getLanguageCode, getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { getChoiceIdByValue } from "@/lib/response/utils";
|
||||
@@ -24,7 +18,7 @@ import { ResponseBadges } from "@/modules/ui/components/response-badges";
|
||||
|
||||
interface RenderResponseProps {
|
||||
responseData: TResponseDataValue;
|
||||
question: TSurveyQuestion;
|
||||
element: TSurveyElement;
|
||||
survey: TSurvey;
|
||||
language: string | null;
|
||||
isExpanded?: boolean;
|
||||
@@ -33,7 +27,7 @@ interface RenderResponseProps {
|
||||
|
||||
export const RenderResponse: React.FC<RenderResponseProps> = ({
|
||||
responseData,
|
||||
question,
|
||||
element,
|
||||
survey,
|
||||
language,
|
||||
isExpanded = true,
|
||||
@@ -54,21 +48,20 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
|
||||
return String(data);
|
||||
}
|
||||
};
|
||||
const questionType = question.type;
|
||||
switch (questionType) {
|
||||
case TSurveyQuestionTypeEnum.Rating:
|
||||
switch (element.type) {
|
||||
case TSurveyElementTypeEnum.Rating:
|
||||
if (typeof responseData === "number") {
|
||||
return (
|
||||
<RatingResponse
|
||||
scale={question.scale}
|
||||
scale={element.scale}
|
||||
answer={responseData}
|
||||
range={question.range}
|
||||
addColors={(question as TSurveyRatingQuestion).isColorCodingEnabled}
|
||||
range={element.range}
|
||||
addColors={element.isColorCodingEnabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
break;
|
||||
case TSurveyQuestionTypeEnum.Date:
|
||||
case TSurveyElementTypeEnum.Date:
|
||||
if (typeof responseData === "string") {
|
||||
const parsedDate = new Date(responseData);
|
||||
|
||||
@@ -77,11 +70,11 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
|
||||
return <p className="ph-no-capture my-1 truncate font-normal text-slate-700">{formattedDate}</p>;
|
||||
}
|
||||
break;
|
||||
case TSurveyQuestionTypeEnum.PictureSelection:
|
||||
case TSurveyElementTypeEnum.PictureSelection:
|
||||
if (Array.isArray(responseData)) {
|
||||
return (
|
||||
<PictureSelectionResponse
|
||||
choices={(question as TSurveyPictureSelectionQuestion).choices}
|
||||
choices={element.choices}
|
||||
selected={responseData}
|
||||
isExpanded={isExpanded}
|
||||
showId={showId}
|
||||
@@ -89,16 +82,16 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
|
||||
);
|
||||
}
|
||||
break;
|
||||
case TSurveyQuestionTypeEnum.FileUpload:
|
||||
case TSurveyElementTypeEnum.FileUpload:
|
||||
if (Array.isArray(responseData)) {
|
||||
return <FileUploadResponse selected={responseData} />;
|
||||
}
|
||||
break;
|
||||
case TSurveyQuestionTypeEnum.Matrix:
|
||||
case TSurveyElementTypeEnum.Matrix:
|
||||
if (typeof responseData === "object" && !Array.isArray(responseData)) {
|
||||
return (
|
||||
<>
|
||||
{(question as TSurveyMatrixQuestion).rows.map((row) => {
|
||||
{element.rows.map((row) => {
|
||||
const languagCode = getLanguageCode(survey.languages, language);
|
||||
const rowValueInSelectedLanguage = getLocalizedValue(row.label, languagCode);
|
||||
if (!responseData[rowValueInSelectedLanguage]) return null;
|
||||
@@ -112,14 +105,14 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
|
||||
);
|
||||
}
|
||||
break;
|
||||
case TSurveyQuestionTypeEnum.Address:
|
||||
case TSurveyQuestionTypeEnum.ContactInfo:
|
||||
case TSurveyElementTypeEnum.Address:
|
||||
case TSurveyElementTypeEnum.ContactInfo:
|
||||
if (Array.isArray(responseData)) {
|
||||
return <ArrayResponse value={responseData} />;
|
||||
}
|
||||
break;
|
||||
|
||||
case TSurveyQuestionTypeEnum.Cal:
|
||||
case TSurveyElementTypeEnum.Cal:
|
||||
if (typeof responseData === "string" || typeof responseData === "number") {
|
||||
return (
|
||||
<ResponseBadges
|
||||
@@ -131,7 +124,7 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
|
||||
);
|
||||
}
|
||||
break;
|
||||
case TSurveyQuestionTypeEnum.Consent:
|
||||
case TSurveyElementTypeEnum.Consent:
|
||||
if (typeof responseData === "string" || typeof responseData === "number") {
|
||||
return (
|
||||
<ResponseBadges
|
||||
@@ -143,7 +136,7 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
|
||||
);
|
||||
}
|
||||
break;
|
||||
case TSurveyQuestionTypeEnum.CTA:
|
||||
case TSurveyElementTypeEnum.CTA:
|
||||
if (typeof responseData === "string" || typeof responseData === "number") {
|
||||
return (
|
||||
<ResponseBadges
|
||||
@@ -155,11 +148,11 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
|
||||
);
|
||||
}
|
||||
break;
|
||||
case TSurveyQuestionTypeEnum.MultipleChoiceMulti:
|
||||
case TSurveyQuestionTypeEnum.MultipleChoiceSingle:
|
||||
case TSurveyQuestionTypeEnum.Ranking:
|
||||
case TSurveyElementTypeEnum.MultipleChoiceMulti:
|
||||
case TSurveyElementTypeEnum.MultipleChoiceSingle:
|
||||
case TSurveyElementTypeEnum.Ranking:
|
||||
if (typeof responseData === "string" || typeof responseData === "number") {
|
||||
const choiceId = getChoiceIdByValue(responseData.toString(), question);
|
||||
const choiceId = getChoiceIdByValue(responseData.toString(), element);
|
||||
return (
|
||||
<ResponseBadges
|
||||
items={[{ value: responseData.toString(), id: choiceId }]}
|
||||
@@ -169,12 +162,12 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
|
||||
);
|
||||
} else if (Array.isArray(responseData)) {
|
||||
const itemsArray = responseData.map((choice) => {
|
||||
const choiceId = getChoiceIdByValue(choice, question);
|
||||
const choiceId = getChoiceIdByValue(choice, element);
|
||||
return { value: choice, id: choiceId };
|
||||
});
|
||||
return (
|
||||
<>
|
||||
{questionType === TSurveyQuestionTypeEnum.Ranking ? (
|
||||
{element.type === TSurveyElementTypeEnum.Ranking ? (
|
||||
<RankingResponse value={itemsArray} isExpanded={isExpanded} showId={showId} />
|
||||
) : (
|
||||
<ResponseBadges items={itemsArray} isExpanded={isExpanded} showId={showId} />
|
||||
|
||||
+13
-11
@@ -8,9 +8,10 @@ import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { parseRecallInfo } from "@/lib/utils/recall";
|
||||
import { ResponseCardQuotas } from "@/modules/ee/quotas/components/single-response-card-quotas";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { isValidValue } from "../util";
|
||||
import { ElementSkip } from "./ElementSkip";
|
||||
import { HiddenFields } from "./HiddenFields";
|
||||
import { QuestionSkip } from "./QuestionSkip";
|
||||
import { RenderResponse } from "./RenderResponse";
|
||||
import { ResponseVariables } from "./ResponseVariables";
|
||||
import { VerifiedEmail } from "./VerifiedEmail";
|
||||
@@ -26,7 +27,8 @@ export const SingleResponseCardBody = ({
|
||||
response,
|
||||
skippedQuestions,
|
||||
}: SingleResponseCardBodyProps) => {
|
||||
const isFirstQuestionAnswered = response.data[survey.questions[0].id] ? true : false;
|
||||
const elements = getElementsFromBlocks(survey.blocks);
|
||||
const isFirstElementAnswered = elements[0] ? !!response.data[elements[0].id] : false;
|
||||
const { t } = useTranslation();
|
||||
const formatTextWithSlashes = (text: string) => {
|
||||
// Updated regex to match content between #/ and \#
|
||||
@@ -52,11 +54,11 @@ export const SingleResponseCardBody = ({
|
||||
return (
|
||||
<div className="p-6">
|
||||
{survey.welcomeCard.enabled && (
|
||||
<QuestionSkip
|
||||
skippedQuestions={[]}
|
||||
questions={survey.questions}
|
||||
<ElementSkip
|
||||
skippedElements={[]}
|
||||
elements={elements}
|
||||
status={"welcomeCard"}
|
||||
isFirstQuestionAnswered={isFirstQuestionAnswered}
|
||||
isFirstElementAnswered={isFirstElementAnswered}
|
||||
responseData={response.data}
|
||||
/>
|
||||
)}
|
||||
@@ -64,7 +66,7 @@ export const SingleResponseCardBody = ({
|
||||
{survey.isVerifyEmailEnabled && response.data["verifiedEmail"] && (
|
||||
<VerifiedEmail responseData={response.data} />
|
||||
)}
|
||||
{survey.questions.map((question) => {
|
||||
{elements.map((question) => {
|
||||
const skipped = skippedQuestions.find((skippedQuestionElement) =>
|
||||
skippedQuestionElement.includes(question.id)
|
||||
);
|
||||
@@ -92,7 +94,7 @@ export const SingleResponseCardBody = ({
|
||||
</p>
|
||||
<div dir="auto">
|
||||
<RenderResponse
|
||||
question={question}
|
||||
element={question}
|
||||
survey={survey}
|
||||
responseData={response.data[question.id]}
|
||||
language={response.language}
|
||||
@@ -101,9 +103,9 @@ export const SingleResponseCardBody = ({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<QuestionSkip
|
||||
skippedQuestions={skipped}
|
||||
questions={survey.questions}
|
||||
<ElementSkip
|
||||
skippedElements={skipped}
|
||||
elements={elements}
|
||||
responseData={response.data}
|
||||
status={
|
||||
response.finished ||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { TResponse, TResponseWithQuotas } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { TUser, TUserLocale } from "@formbricks/types/user";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { DecrementQuotasCheckbox } from "@/modules/ui/components/decrement-quotas-checkbox";
|
||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||
import { deleteResponseAction, getResponseAction } from "./actions";
|
||||
@@ -49,6 +50,9 @@ export const SingleResponseCard = ({
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const skippedQuestions: string[][] = useMemo(() => {
|
||||
// Derive questions from blocks
|
||||
const questions = getElementsFromBlocks(survey.blocks);
|
||||
|
||||
const flushTemp = (temp: string[], result: string[][], shouldReverse = false) => {
|
||||
if (temp.length > 0) {
|
||||
if (shouldReverse) temp.reverse();
|
||||
@@ -61,7 +65,7 @@ export const SingleResponseCard = ({
|
||||
const result: string[][] = [];
|
||||
let temp: string[] = [];
|
||||
|
||||
for (const question of survey.questions) {
|
||||
for (const question of questions) {
|
||||
if (isValidValue(response.data[question.id])) {
|
||||
flushTemp(temp, result);
|
||||
} else {
|
||||
@@ -76,8 +80,8 @@ export const SingleResponseCard = ({
|
||||
const result: string[][] = [];
|
||||
let temp: string[] = [];
|
||||
|
||||
for (let index = survey.questions.length - 1; index >= 0; index--) {
|
||||
const question = survey.questions[index];
|
||||
for (let index = questions.length - 1; index >= 0; index--) {
|
||||
const question = questions[index];
|
||||
const hasNoData = !response.data[question.id];
|
||||
const shouldSkip = hasNoData && (result.length === 0 || !isValidValue(response.data[question.id]));
|
||||
|
||||
@@ -92,7 +96,7 @@ export const SingleResponseCard = ({
|
||||
};
|
||||
|
||||
return response.finished ? processFinishedResponse() : processUnfinishedResponse();
|
||||
}, [response.id, response.finished, response.data, survey.questions]);
|
||||
}, [response.finished, response.data, survey.blocks]);
|
||||
|
||||
const handleDeleteResponse = async () => {
|
||||
setIsDeleting(true);
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { TResponseData } from "@formbricks/types/responses";
|
||||
import {
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionChoice,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurveyQuestionChoice } from "@formbricks/types/surveys/types";
|
||||
import { MAX_OTHER_OPTION_LENGTH } from "@/lib/constants";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
|
||||
@@ -34,7 +31,7 @@ export const validateOtherOptionLengthForMultipleChoice = ({
|
||||
responseLanguage,
|
||||
}: {
|
||||
responseData?: TResponseData;
|
||||
surveyQuestions: TSurveyQuestion[];
|
||||
surveyQuestions: TSurveyElement[];
|
||||
responseLanguage?: string;
|
||||
}): string | undefined => {
|
||||
if (!responseData) return undefined;
|
||||
@@ -43,8 +40,8 @@ export const validateOtherOptionLengthForMultipleChoice = ({
|
||||
if (!question) continue;
|
||||
|
||||
const isMultiChoice =
|
||||
question.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti ||
|
||||
question.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle;
|
||||
question.type === TSurveyElementTypeEnum.MultipleChoiceMulti ||
|
||||
question.type === TSurveyElementTypeEnum.MultipleChoiceSingle;
|
||||
|
||||
if (!isMultiChoice) continue;
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import {
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { MAX_OTHER_OPTION_LENGTH } from "@/lib/constants";
|
||||
import { validateOtherOptionLength, validateOtherOptionLengthForMultipleChoice } from "../question";
|
||||
import { validateOtherOptionLength, validateOtherOptionLengthForMultipleChoice } from "../element";
|
||||
|
||||
vi.mock("@/lib/i18n/utils", () => ({
|
||||
getLocalizedValue: vi.fn().mockImplementation((value, language) => {
|
||||
@@ -11,6 +11,7 @@ export const getSurveyQuestions = reactCache(async (surveyId: string) => {
|
||||
select: {
|
||||
environmentId: true,
|
||||
questions: true,
|
||||
blocks: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user