mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-11 19:12:06 -05:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75f05f85e9 | |||
| 74405cc05f |
+6
-102
@@ -1,3 +1,9 @@
|
||||
import { getQuotasSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/survey";
|
||||
import { getDisplayCountBySurveyId } from "@/lib/display/service";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { getResponseCountBySurveyId } from "@/lib/response/service";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { evaluateLogic, performActions } from "@/lib/surveyLogic/utils";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
@@ -10,12 +16,6 @@ import {
|
||||
TSurveyQuestionTypeEnum,
|
||||
TSurveySummary,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { getQuotasSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/survey";
|
||||
import { getDisplayCountBySurveyId } from "@/lib/display/service";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { getResponseCountBySurveyId } from "@/lib/response/service";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { evaluateLogic, performActions } from "@/lib/surveyLogic/utils";
|
||||
import {
|
||||
getQuestionSummary,
|
||||
getResponsesForSummary,
|
||||
@@ -376,102 +376,6 @@ describe("getQuestionSummary", () => {
|
||||
expect(openTextSummary?.samples[0].value).toBe("Open answer");
|
||||
});
|
||||
|
||||
test("summarizes OpenText questions with array answer format", async () => {
|
||||
const responsesWithArray = [
|
||||
{
|
||||
id: "r1",
|
||||
data: { q_open: ["Answer 1", "Answer 2", "Answer 3"] },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: "en",
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
];
|
||||
const summary = await getQuestionSummary(survey, responsesWithArray, mockDropOff);
|
||||
const openTextSummary = summary.find((s: any) => s.question?.id === "q_open");
|
||||
expect(openTextSummary?.type).toBe(TSurveyQuestionTypeEnum.OpenText);
|
||||
expect(openTextSummary?.responseCount).toBe(1);
|
||||
// @ts-expect-error
|
||||
expect(openTextSummary?.samples[0].value).toBe("Answer 1, Answer 2, Answer 3");
|
||||
});
|
||||
|
||||
test("summarizes OpenText questions with array containing null/empty values", async () => {
|
||||
const responsesWithPartialArray = [
|
||||
{
|
||||
id: "r1",
|
||||
data: { q_open: ["Valid answer", null, "", "Another answer", undefined] },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: "en",
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
];
|
||||
const summary = await getQuestionSummary(survey, responsesWithPartialArray, mockDropOff);
|
||||
const openTextSummary = summary.find((s: any) => s.question?.id === "q_open");
|
||||
expect(openTextSummary?.type).toBe(TSurveyQuestionTypeEnum.OpenText);
|
||||
expect(openTextSummary?.responseCount).toBe(1);
|
||||
// @ts-expect-error - filters out null, empty string, and undefined
|
||||
expect(openTextSummary?.samples[0].value).toBe("Valid answer, Another answer");
|
||||
});
|
||||
|
||||
test("summarizes OpenText questions ignoring empty arrays", async () => {
|
||||
const responsesWithEmptyArray = [
|
||||
{
|
||||
id: "r1",
|
||||
data: { q_open: [] },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: "en",
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
];
|
||||
const summary = await getQuestionSummary(survey, responsesWithEmptyArray, mockDropOff);
|
||||
const openTextSummary = summary.find((s: any) => s.question?.id === "q_open");
|
||||
expect(openTextSummary?.type).toBe(TSurveyQuestionTypeEnum.OpenText);
|
||||
expect(openTextSummary?.responseCount).toBe(0);
|
||||
// @ts-expect-error
|
||||
expect(openTextSummary?.samples).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("summarizes OpenText questions with mixed string and array responses", async () => {
|
||||
const mixedResponses = [
|
||||
{
|
||||
id: "r1",
|
||||
data: { q_open: "String answer" },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: "en",
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
{
|
||||
id: "r2",
|
||||
data: { q_open: ["Array", "answer"] },
|
||||
updatedAt: new Date(),
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
language: "en",
|
||||
ttc: {},
|
||||
finished: true,
|
||||
},
|
||||
];
|
||||
const summary = await getQuestionSummary(survey, mixedResponses, mockDropOff);
|
||||
const openTextSummary = summary.find((s: any) => s.question?.id === "q_open");
|
||||
expect(openTextSummary?.type).toBe(TSurveyQuestionTypeEnum.OpenText);
|
||||
expect(openTextSummary?.responseCount).toBe(2);
|
||||
// @ts-expect-error
|
||||
expect(openTextSummary?.samples[0].value).toBe("String answer");
|
||||
// @ts-expect-error
|
||||
expect(openTextSummary?.samples[1].value).toBe("Array, answer");
|
||||
});
|
||||
|
||||
test("summarizes MultipleChoiceSingle questions", async () => {
|
||||
const summary = await getQuestionSummary(survey, responses, mockDropOff);
|
||||
const multiSingleSummary = summary.find((s: any) => s.question?.id === "q_multi_single");
|
||||
|
||||
+2
-9
@@ -322,18 +322,11 @@ export const getQuestionSummary = async (
|
||||
let values: TSurveyQuestionSummaryOpenText["samples"] = [];
|
||||
responses.forEach((response) => {
|
||||
const answer = response.data[question.id];
|
||||
let normalizedAnswer: string | null = null;
|
||||
if (typeof answer === "string" && answer) {
|
||||
normalizedAnswer = answer;
|
||||
} else if (Array.isArray(answer) && answer.length > 0) {
|
||||
normalizedAnswer = answer.filter((v) => v != null && v !== "").join(", ");
|
||||
}
|
||||
|
||||
if (normalizedAnswer) {
|
||||
if (answer && typeof answer === "string") {
|
||||
values.push({
|
||||
id: response.id,
|
||||
updatedAt: response.updatedAt,
|
||||
value: normalizedAnswer,
|
||||
value: answer,
|
||||
contact: response.contact,
|
||||
contactAttributes: response.contactAttributes,
|
||||
});
|
||||
|
||||
@@ -83,6 +83,7 @@ export const SurveyEditor = ({
|
||||
const [activeView, setActiveView] = useState<TSurveyEditorTabs>("questions");
|
||||
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
|
||||
const [localSurvey, setLocalSurvey] = useState<TSurvey | null>(() => structuredClone(survey));
|
||||
const [savedSurvey, setSavedSurvey] = useState<TSurvey>(survey);
|
||||
const [invalidQuestions, setInvalidQuestions] = useState<string[] | null>(null);
|
||||
const [selectedLanguageCode, setSelectedLanguageCode] = useState<string>("default");
|
||||
const surveyEditorRef = useRef(null);
|
||||
@@ -108,6 +109,7 @@ export const SurveyEditor = ({
|
||||
|
||||
const surveyClone = structuredClone(survey);
|
||||
setLocalSurvey(surveyClone);
|
||||
setSavedSurvey(surveyClone);
|
||||
|
||||
if (survey.questions.length > 0) {
|
||||
setActiveQuestionId(survey.questions[0].id);
|
||||
@@ -160,8 +162,9 @@ export const SurveyEditor = ({
|
||||
<SurveyMenuBar
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
localSurvey={localSurvey}
|
||||
survey={survey}
|
||||
survey={savedSurvey}
|
||||
environmentId={environment.id}
|
||||
setSavedSurvey={setSavedSurvey}
|
||||
activeId={activeView}
|
||||
setActiveId={setActiveView}
|
||||
setInvalidQuestions={setInvalidQuestions}
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { createSegmentAction } from "@/modules/ee/contacts/segments/actions";
|
||||
import { Alert, AlertButton, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { AlertDialog } from "@/modules/ui/components/alert-dialog";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { Project } from "@prisma/client";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { isEqual } from "lodash";
|
||||
import { ArrowLeftIcon, SettingsIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
import toast from "react-hot-toast";
|
||||
import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
@@ -23,11 +18,18 @@ import {
|
||||
ZSurveyEndScreenCard,
|
||||
ZSurveyRedirectUrlCard,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { createSegmentAction } from "@/modules/ee/contacts/segments/actions";
|
||||
import { Alert, AlertButton, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { AlertDialog } from "@/modules/ui/components/alert-dialog";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { updateSurveyAction } from "../actions";
|
||||
import { isSurveyValid } from "../lib/validation";
|
||||
|
||||
interface SurveyMenuBarProps {
|
||||
localSurvey: TSurvey;
|
||||
setSavedSurvey: (survey: TSurvey) => void;
|
||||
survey: TSurvey;
|
||||
setLocalSurvey: (survey: TSurvey) => void;
|
||||
environmentId: string;
|
||||
@@ -47,6 +49,7 @@ interface SurveyMenuBarProps {
|
||||
export const SurveyMenuBar = ({
|
||||
localSurvey,
|
||||
survey,
|
||||
setSavedSurvey,
|
||||
environmentId,
|
||||
setLocalSurvey,
|
||||
activeId,
|
||||
@@ -247,9 +250,12 @@ export const SurveyMenuBar = ({
|
||||
|
||||
setIsSurveySaving(false);
|
||||
if (updatedSurveyResponse?.data) {
|
||||
setLocalSurvey(updatedSurveyResponse.data);
|
||||
const updatedSurvey = updatedSurveyResponse.data;
|
||||
flushSync(() => {
|
||||
setLocalSurvey(updatedSurvey);
|
||||
setSavedSurvey(updatedSurvey);
|
||||
});
|
||||
toast.success(t("environments.surveys.edit.changes_saved"));
|
||||
router.refresh();
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(updatedSurveyResponse);
|
||||
toast.error(errorMessage);
|
||||
@@ -292,12 +298,18 @@ export const SurveyMenuBar = ({
|
||||
const segment = await handleSegmentUpdate();
|
||||
clearSurveyLocalStorage();
|
||||
|
||||
await updateSurveyAction({
|
||||
const publishedSurveyResponse = await updateSurveyAction({
|
||||
...localSurvey,
|
||||
status,
|
||||
segment,
|
||||
});
|
||||
setIsSurveyPublishing(false);
|
||||
if (publishedSurveyResponse?.data) {
|
||||
const publishedSurvey = publishedSurveyResponse.data;
|
||||
flushSync(() => {
|
||||
setSavedSurvey(publishedSurvey);
|
||||
});
|
||||
}
|
||||
router.push(`/environments/${environmentId}/surveys/${localSurvey.id}/summary?success=true`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
@@ -2340,6 +2340,50 @@
|
||||
"name": "My Action from Postman",
|
||||
"type": "code"
|
||||
},
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "Optional description of the action class",
|
||||
"type": "string"
|
||||
},
|
||||
"environmentId": {
|
||||
"description": "The environment ID where the action class will be created",
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"description": "Required when type is 'code'. A unique identifier for the action. Not needed for 'noCode' type.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the action class",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"noCodeConfig": {
|
||||
"description": "Configuration object required when type is 'noCode'. Defines the conditions for triggering the action. Not needed for 'code' type.",
|
||||
"example": {
|
||||
"elementSelector": {
|
||||
"cssSelector": ".button-class",
|
||||
"innerHtml": "Click me"
|
||||
},
|
||||
"type": "click",
|
||||
"urlFilters": [
|
||||
{
|
||||
"rule": "contains",
|
||||
"value": "https://www.google.com"
|
||||
}
|
||||
]
|
||||
},
|
||||
"nullable": true,
|
||||
"type": "object"
|
||||
},
|
||||
"type": {
|
||||
"description": "Type of action class",
|
||||
"enum": ["code", "noCode"],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["environmentId", "name", "type"],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
@@ -2417,8 +2461,11 @@
|
||||
"application/json": {
|
||||
"example": {
|
||||
"code": "bad_request",
|
||||
"details": {},
|
||||
"message": "Database error when creating an action for environment clurwouax000azffxt7n5unn3"
|
||||
"details": {
|
||||
"environmentId": "Required",
|
||||
"key": "Required"
|
||||
},
|
||||
"message": "Fields are missing or incorrectly formatted"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object"
|
||||
|
||||
Reference in New Issue
Block a user