moves the integrations code over to the blocks schema

This commit is contained in:
pandeymangg
2025-11-11 11:16:08 +05:30
parent 3b5fe4cb94
commit 335ec02361
6 changed files with 86 additions and 31 deletions

View File

@@ -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 { getQuestionsFromBlocks } 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";
@@ -71,6 +72,7 @@ const NoBaseFoundError = () => {
const renderQuestionSelection = ({
t,
selectedSurvey,
questions,
control,
includeVariables,
setIncludeVariables,
@@ -83,6 +85,7 @@ const renderQuestionSelection = ({
}: {
t: TFunction;
selectedSurvey: TSurvey;
questions: TSurveyElement[];
control: Control<IntegrationModalInputs>;
includeVariables: boolean;
setIncludeVariables: (value: boolean) => void;
@@ -99,7 +102,7 @@ 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) => (
{questions.map((question) => (
<Controller
key={question.id}
control={control}
@@ -120,7 +123,9 @@ const renderQuestionSelection = ({
}}
/>
<span className="ml-2">
{getTextContent(getLocalizedValue(question.headline, "default"))}
{getTextContent(
recallToHeadline(question.headline, selectedSurvey, false, "default")["default"]
)}
</span>
</label>
</div>
@@ -194,6 +199,11 @@ export const AddIntegrationModal = ({
};
const selectedSurvey = surveys.find((item) => item.id === survey);
const questions = useMemo(
() => (selectedSurvey ? getQuestionsFromBlocks(selectedSurvey.blocks) : []),
[selectedSurvey]
);
const submitHandler = async (data: IntegrationModalInputs) => {
try {
if (!data.base || data.base === "") {
@@ -218,7 +228,7 @@ export const AddIntegrationModal = ({
surveyName: selectedSurvey.name,
questionIds: data.questions,
questions:
data.questions.length === selectedSurvey.questions.length
data.questions.length === questions.length
? t("common.all_questions")
: t("common.selected_questions"),
createdAt: new Date(),
@@ -395,6 +405,7 @@ export const AddIntegrationModal = ({
renderQuestionSelection({
t,
selectedSurvey,
questions,
control,
includeVariables,
setIncludeVariables,

View File

@@ -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 { getQuestionsFromBlocks } 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";
@@ -86,12 +86,17 @@ export const AddIntegrationModal = ({
},
};
const questions = useMemo(
() => (selectedSurvey ? getQuestionsFromBlocks(selectedSurvey.blocks) : []),
[selectedSurvey]
);
useEffect(() => {
if (selectedSurvey && !selectedIntegration) {
const questionIds = selectedSurvey.questions.map((question) => question.id);
const questionIds = questions.map((question) => question.id);
setSelectedQuestions(questionIds);
}
}, [selectedIntegration, selectedSurvey]);
}, [questions, selectedIntegration, selectedSurvey]);
useEffect(() => {
if (selectedIntegration) {
@@ -145,7 +150,7 @@ export const AddIntegrationModal = ({
integrationData.surveyName = selectedSurvey.name;
integrationData.questionIds = selectedQuestions;
integrationData.questions =
selectedQuestions.length === selectedSurvey?.questions.length
selectedQuestions.length === questions.length
? t("common.all_questions")
: t("common.selected_questions");
integrationData.createdAt = new Date();
@@ -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) => (
{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">
<Checkbox
@@ -277,7 +282,11 @@ export const AddIntegrationModal = ({
}}
/>
<span className="ml-2 w-[30rem] truncate">
{getTextContent(getLocalizedValue(question.headline, "default"))}
{getTextContent(
recallToHeadline(question.headline, selectedSurvey, false, "default")[
"default"
]
)}
</span>
</label>
</div>

View File

@@ -13,6 +13,7 @@ import {
TIntegrationNotionDatabase,
} from "@formbricks/types/integration/notion";
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { getTextContent } from "@formbricks/types/surveys/validation";
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
import {
ERRORS,
@@ -20,9 +21,9 @@ 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 { recallToHeadline } from "@/lib/utils/recall";
import { getQuestionsFromBlocks } from "@/modules/survey/lib/client-utils";
import { getQuestionTypes } from "@/modules/survey/lib/questions";
import { Button } from "@/modules/ui/components/button";
import {
@@ -91,6 +92,11 @@ export const AddIntegrationModal = ({
createdAt: new Date(),
};
const questions = useMemo(
() => (selectedSurvey ? getQuestionsFromBlocks(selectedSurvey.blocks) : []),
[selectedSurvey]
);
const notionIntegrationData: TIntegrationInput = {
type: "notion",
config: {
@@ -119,10 +125,10 @@ export const AddIntegrationModal = ({
}, [selectedDatabase?.id]);
const questionItems = useMemo(() => {
const questions = selectedSurvey
? replaceHeadlineRecall(selectedSurvey, "default")?.questions.map((q) => ({
const mappedQuestions = selectedSurvey
? questions.map((q) => ({
id: q.id,
name: getLocalizedValue(q.headline, "default"),
name: getTextContent(recallToHeadline(q.headline, selectedSurvey, false, "default")["default"]),
type: q.type,
}))
: [];
@@ -155,7 +161,7 @@ export const AddIntegrationModal = ({
},
];
return [...questions, ...variables, ...hiddenFields, ...Metadata, ...createdAt];
return [...mappedQuestions, ...variables, ...hiddenFields, ...Metadata, ...createdAt];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedSurvey?.id]);

View File

@@ -17,8 +17,8 @@ import { TSurvey, TSurveyQuestionId } 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 { getQuestionsFromBlocks } 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";
@@ -73,14 +73,19 @@ export const AddChannelMappingModal = ({
},
};
const questions = useMemo(
() => (selectedSurvey ? getQuestionsFromBlocks(selectedSurvey.blocks) : []),
[selectedSurvey]
);
useEffect(() => {
if (selectedSurvey) {
const questionIds = selectedSurvey.questions.map((question) => question.id);
const questionIds = questions.map((question) => question.id);
if (!selectedIntegration) {
setSelectedQuestions(questionIds);
}
}
}, [selectedIntegration, selectedSurvey]);
}, [questions, selectedIntegration, selectedSurvey]);
useEffect(() => {
if (selectedIntegration) {
@@ -269,7 +274,7 @@ 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) => (
{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">
<Checkbox
@@ -283,7 +288,11 @@ export const AddChannelMappingModal = ({
}}
/>
<span className="ml-2">
{getTextContent(getLocalizedValue(question.headline, "default"))}
{getTextContent(
recallToHeadline(question.headline, selectedSurvey, false, "default")[
"default"
]
)}
</span>
</label>
</div>

View File

@@ -6,7 +6,8 @@ import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-s
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 { 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 { getQuestionsFromBlocks } from "@/lib/survey/utils";
import { getFormattedDateTimeString } from "@/lib/utils/datetime";
import { parseRecallInfo } from "@/lib/utils/recall";
import { truncateText } from "@/lib/utils/strings";
@@ -236,6 +238,9 @@ const extractResponses = async (
const responses: string[] = [];
const questions: string[] = [];
// Derive questions from blocks
const surveyQuestions = getQuestionsFromBlocks(survey.blocks);
for (const questionId of questionIds) {
//check for hidden field Ids
if (survey.hiddenFields.fieldIds?.includes(questionId)) {
@@ -243,7 +248,7 @@ const extractResponses = async (
questions.push(questionId);
continue;
}
const question = survey?.questions.find((q) => q.id === questionId);
const question = surveyQuestions.find((q) => q.id === questionId);
if (!question) {
continue;
}
@@ -252,7 +257,7 @@ const extractResponses = async (
if (responseValue !== undefined) {
let answer: typeof responseValue;
if (question.type === TSurveyQuestionTypeEnum.PictureSelection) {
if (question.type === TSurveyElementTypeEnum.PictureSelection) {
const selectedChoiceIds = responseValue as string[];
answer = question?.choices
.filter((choice) => selectedChoiceIds.includes(choice.id))
@@ -321,14 +326,17 @@ const buildNotionPayloadProperties = (
const properties: any = {};
const responses = data.response.data;
// Derive questions from blocks
const surveyQuestions = getQuestionsFromBlocks(surveyData.blocks);
const mappingQIds = mapping
.filter((m) => m.question.type === TSurveyQuestionTypeEnum.PictureSelection)
.filter((m) => m.question.type === TSurveyElementTypeEnum.PictureSelection)
.map((m) => m.question.id);
Object.keys(responses).forEach((resp) => {
if (mappingQIds.find((qId) => qId === resp)) {
const selectedChoiceIds = responses[resp] as string[];
const pictureQuestion = surveyData.questions.find((q) => q.id === resp);
const pictureQuestion = surveyQuestions.find((q) => q.id === resp);
responses[resp] = (pictureQuestion as any)?.choices
.filter((choice) => selectedChoiceIds.includes(choice.id))

View File

@@ -1,5 +1,17 @@
"use client";
import { TSurveyBlock } from "@formbricks/types/surveys/blocks";
import { TSurveyElement } from "@formbricks/types/surveys/elements";
export const copySurveyLink = (surveyUrl: string, singleUseId?: string): string => {
return singleUseId ? `${surveyUrl}?suId=${singleUseId}` : surveyUrl;
};
/**
* Derives a flat array of elements from the survey's blocks structure.
* This is the client-side equivalent of the server-side getQuestionsFromBlocks.
* @param blocks - Array of survey blocks
* @returns An array of TSurveyElement (pure elements without block-level properties)
*/
export const getQuestionsFromBlocks = (blocks: TSurveyBlock[]): TSurveyElement[] =>
blocks.flatMap((block) => block.elements);