mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-21 23:29:48 -05:00
integrations code cleanup
This commit is contained in:
+21
-21
@@ -69,10 +69,10 @@ const NoBaseFoundError = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderQuestionSelection = ({
|
||||
const renderElementSelection = ({
|
||||
t,
|
||||
selectedSurvey,
|
||||
questions,
|
||||
elements,
|
||||
control,
|
||||
includeVariables,
|
||||
setIncludeVariables,
|
||||
@@ -85,7 +85,7 @@ const renderQuestionSelection = ({
|
||||
}: {
|
||||
t: TFunction;
|
||||
selectedSurvey: TSurvey;
|
||||
questions: TSurveyElement[];
|
||||
elements: TSurveyElement[];
|
||||
control: Control<IntegrationModalInputs>;
|
||||
includeVariables: boolean;
|
||||
setIncludeVariables: (value: boolean) => void;
|
||||
@@ -102,29 +102,29 @@ 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">
|
||||
{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">
|
||||
<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={field.value?.includes(question.id)}
|
||||
checked={field.value?.includes(element.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([...field.value, question.id])
|
||||
: field.onChange(field.value?.filter((value) => value !== question.id));
|
||||
? field.onChange([...(field.value || []), element.id])
|
||||
: field.onChange(field.value?.filter((value) => value !== element.id) || []);
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">
|
||||
{getTextContent(
|
||||
recallToHeadline(question.headline, selectedSurvey, false, "default")["default"]
|
||||
recallToHeadline(element.headline, selectedSurvey, false, "default")["default"]
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
@@ -199,7 +199,7 @@ export const AddIntegrationModal = ({
|
||||
};
|
||||
|
||||
const selectedSurvey = surveys.find((item) => item.id === survey);
|
||||
const questions = useMemo(
|
||||
const elements = useMemo(
|
||||
() => (selectedSurvey ? getElementsFromBlocks(selectedSurvey.blocks) : []),
|
||||
[selectedSurvey]
|
||||
);
|
||||
@@ -218,7 +218,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"));
|
||||
}
|
||||
|
||||
@@ -226,9 +226,9 @@ export const AddIntegrationModal = ({
|
||||
const integrationData: TIntegrationAirtableConfigData = {
|
||||
surveyId: selectedSurvey.id,
|
||||
surveyName: selectedSurvey.name,
|
||||
questionIds: data.questions,
|
||||
questions:
|
||||
data.questions.length === questions.length
|
||||
elementIds: data.elements,
|
||||
elements:
|
||||
data.elements.length === elements.length
|
||||
? t("common.all_questions")
|
||||
: t("common.selected_questions"),
|
||||
createdAt: new Date(),
|
||||
@@ -376,7 +376,7 @@ export const AddIntegrationModal = ({
|
||||
required
|
||||
onValueChange={(val) => {
|
||||
field.onChange(val);
|
||||
setValue("questions", []);
|
||||
setValue("elements", []);
|
||||
}}
|
||||
defaultValue={defaultData?.survey}>
|
||||
<SelectTrigger>
|
||||
@@ -402,10 +402,10 @@ export const AddIntegrationModal = ({
|
||||
|
||||
{survey &&
|
||||
selectedSurvey &&
|
||||
renderQuestionSelection({
|
||||
renderElementSelection({
|
||||
t,
|
||||
selectedSurvey,
|
||||
questions,
|
||||
elements: elements,
|
||||
control,
|
||||
includeVariables,
|
||||
setIncludeVariables,
|
||||
|
||||
+2
-2
@@ -110,7 +110,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,
|
||||
@@ -123,7 +123,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;
|
||||
|
||||
+15
-15
@@ -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,17 +86,17 @@ export const AddIntegrationModal = ({
|
||||
},
|
||||
};
|
||||
|
||||
const questions = useMemo(
|
||||
const surveyElements = useMemo(
|
||||
() => (selectedSurvey ? getElementsFromBlocks(selectedSurvey.blocks) : []),
|
||||
[selectedSurvey]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSurvey && !selectedIntegration) {
|
||||
const questionIds = questions.map((question) => question.id);
|
||||
setSelectedQuestions(questionIds);
|
||||
const elementIds = surveyElements.map((element) => element.id);
|
||||
setSelectedElements(elementIds);
|
||||
}
|
||||
}, [questions, selectedIntegration, selectedSurvey]);
|
||||
}, [surveyElements, selectedIntegration, selectedSurvey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedIntegration) {
|
||||
@@ -106,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);
|
||||
@@ -126,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);
|
||||
@@ -148,9 +148,9 @@ export const AddIntegrationModal = ({
|
||||
integrationData.spreadsheetName = spreadsheetName;
|
||||
integrationData.surveyId = selectedSurvey.id;
|
||||
integrationData.surveyName = selectedSurvey.name;
|
||||
integrationData.questionIds = selectedQuestions;
|
||||
integrationData.questions =
|
||||
selectedQuestions.length === questions.length
|
||||
integrationData.elementIds = selectedElements;
|
||||
integrationData.elements =
|
||||
selectedElements.length === surveyElements.length
|
||||
? t("common.all_questions")
|
||||
: t("common.selected_questions");
|
||||
integrationData.createdAt = new Date();
|
||||
@@ -181,7 +181,7 @@ export const AddIntegrationModal = ({
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (questionId: TSurveyQuestionId) => {
|
||||
setSelectedQuestions((prevValues) =>
|
||||
setSelectedElements((prevValues) =>
|
||||
prevValues.includes(questionId)
|
||||
? prevValues.filter((value) => value !== questionId)
|
||||
: [...prevValues, questionId]
|
||||
@@ -268,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">
|
||||
{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
|
||||
@@ -276,7 +276,7 @@ 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);
|
||||
}}
|
||||
|
||||
+1
-1
@@ -118,7 +118,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>
|
||||
);
|
||||
|
||||
+42
-41
@@ -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 {
|
||||
@@ -64,7 +65,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 +74,7 @@ export const AddIntegrationModal = ({
|
||||
>([
|
||||
{
|
||||
column: { id: "", name: "", type: "" },
|
||||
question: { id: "", name: "", type: "" },
|
||||
element: { id: "", name: "", type: "" },
|
||||
},
|
||||
]);
|
||||
const [isDeleting, setIsDeleting] = useState<boolean>(false);
|
||||
@@ -86,13 +87,13 @@ export const AddIntegrationModal = ({
|
||||
mapping: [
|
||||
{
|
||||
column: { id: "", name: "", type: "" },
|
||||
question: { id: "", name: "", type: "" },
|
||||
element: { id: "", name: "", type: "" },
|
||||
},
|
||||
],
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
const questions = useMemo(
|
||||
const elements = useMemo(
|
||||
() => (selectedSurvey ? getElementsFromBlocks(selectedSurvey.blocks) : []),
|
||||
[selectedSurvey]
|
||||
);
|
||||
@@ -124,12 +125,12 @@ export const AddIntegrationModal = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedDatabase?.id]);
|
||||
|
||||
const questionItems = useMemo(() => {
|
||||
const mappedQuestions = selectedSurvey
|
||||
? questions.map((q) => ({
|
||||
id: q.id,
|
||||
name: getTextContent(recallToHeadline(q.headline, selectedSurvey, false, "default")["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,
|
||||
}))
|
||||
: [];
|
||||
|
||||
@@ -137,31 +138,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 [...mappedQuestions, ...variables, ...hiddenFields, ...Metadata, ...createdAt];
|
||||
return [...mappedElements, ...variables, ...hiddenFields, ...Metadata, ...createdAt];
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedSurvey?.id]);
|
||||
|
||||
@@ -195,7 +196,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"));
|
||||
}
|
||||
|
||||
@@ -204,8 +205,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")
|
||||
@@ -266,23 +267,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: "" },
|
||||
},
|
||||
]);
|
||||
};
|
||||
@@ -293,7 +294,7 @@ export const AddIntegrationModal = ({
|
||||
});
|
||||
};
|
||||
|
||||
const ErrorMsg = ({ error, col, ques }) => {
|
||||
const ErrorMsg = ({ error, col, elem }) => {
|
||||
const showErrorMsg = useMemo(() => {
|
||||
switch (error?.type) {
|
||||
case ERRORS.UNSUPPORTED_TYPE:
|
||||
@@ -307,16 +308,16 @@ export const AddIntegrationModal = ({
|
||||
</>
|
||||
);
|
||||
case ERRORS.MAPPING:
|
||||
const question = getElementTypes(t).find((qt) => qt.id === ques.type);
|
||||
if (!question) return null;
|
||||
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: ques.name,
|
||||
question_label: question.label,
|
||||
que_name: elem.name,
|
||||
question_label: element.label,
|
||||
col_name: col.name,
|
||||
col_type: col.type,
|
||||
mapped_type: TYPE_MAPPING[question.id].join(" ,"),
|
||||
mapped_type: TYPE_MAPPING[element.id].join(" ,"),
|
||||
})}
|
||||
</>
|
||||
);
|
||||
@@ -347,15 +348,15 @@ export const AddIntegrationModal = ({
|
||||
key={idx}
|
||||
error={mapping[idx]?.error}
|
||||
col={mapping[idx].column}
|
||||
ques={mapping[idx].question}
|
||||
elem={mapping[idx].element}
|
||||
/>
|
||||
<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);
|
||||
@@ -367,7 +368,7 @@ export const AddIntegrationModal = ({
|
||||
error: {
|
||||
type: ERRORS.UNSUPPORTED_TYPE,
|
||||
},
|
||||
question: item,
|
||||
element: item,
|
||||
};
|
||||
return copy;
|
||||
}
|
||||
@@ -379,7 +380,7 @@ export const AddIntegrationModal = ({
|
||||
error: {
|
||||
type: ERRORS.MAPPING,
|
||||
},
|
||||
question: item,
|
||||
element: item,
|
||||
};
|
||||
return copy;
|
||||
}
|
||||
@@ -387,13 +388,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" />
|
||||
@@ -405,9 +406,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] = {
|
||||
@@ -420,7 +421,7 @@ export const AddIntegrationModal = ({
|
||||
return copy;
|
||||
}
|
||||
|
||||
if (!isValidQuesType) {
|
||||
if (!isValidElemType) {
|
||||
copy[idx] = {
|
||||
...copy[idx],
|
||||
error: {
|
||||
|
||||
+24
-24
@@ -13,7 +13,7 @@ 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";
|
||||
@@ -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,19 +73,19 @@ export const AddChannelMappingModal = ({
|
||||
},
|
||||
};
|
||||
|
||||
const questions = useMemo(
|
||||
const surveyElements = useMemo(
|
||||
() => (selectedSurvey ? getElementsFromBlocks(selectedSurvey.blocks) : []),
|
||||
[selectedSurvey]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSurvey) {
|
||||
const questionIds = questions.map((question) => question.id);
|
||||
const elementIds = surveyElements.map((element) => element.id);
|
||||
if (!selectedIntegration) {
|
||||
setSelectedQuestions(questionIds);
|
||||
setSelectedElements(elementIds);
|
||||
}
|
||||
}
|
||||
}, [questions, selectedIntegration, selectedSurvey]);
|
||||
}, [surveyElements, selectedIntegration, selectedSurvey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedIntegration) {
|
||||
@@ -98,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);
|
||||
@@ -117,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);
|
||||
@@ -126,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(),
|
||||
@@ -159,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]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -274,22 +274,22 @@ 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">
|
||||
{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(
|
||||
recallToHeadline(question.headline, selectedSurvey, false, "default")[
|
||||
recallToHeadline(element.headline, selectedSurvey, false, "default")[
|
||||
"default"
|
||||
]
|
||||
)}
|
||||
|
||||
+1
-1
@@ -134,7 +134,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>
|
||||
);
|
||||
|
||||
+2
-2
@@ -477,7 +477,7 @@ describe("getQuestionSummary", () => {
|
||||
responses,
|
||||
mockDropOff
|
||||
);
|
||||
const openTextSummary = summary.find((s: any) => s.question?.id === "q_open");
|
||||
const openTextSummary = summary.find((s: any) => s.element?.id === "q_open");
|
||||
expect(openTextSummary?.type).toBe(TSurveyElementTypeEnum.OpenText);
|
||||
expect(openTextSummary?.responseCount).toBe(1);
|
||||
// @ts-expect-error
|
||||
@@ -491,7 +491,7 @@ describe("getQuestionSummary", () => {
|
||||
responses,
|
||||
mockDropOff
|
||||
);
|
||||
const multiSingleSummary = summary.find((s: any) => s.question?.id === "q_multi_single");
|
||||
const multiSingleSummary = summary.find((s: any) => s.element?.id === "q_multi_single");
|
||||
expect(multiSingleSummary?.type).toBe(TSurveyElementTypeEnum.MultipleChoiceSingle);
|
||||
expect(multiSingleSummary?.responseCount).toBe(1);
|
||||
// @ts-expect-error
|
||||
|
||||
+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);
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ const mockAirtableIntegration: TIntegrationAirtable = {
|
||||
data: [
|
||||
{
|
||||
surveyId: surveyId,
|
||||
questionIds: [questionId1, questionId2],
|
||||
elementIds: [questionId1, questionId2],
|
||||
baseId: "base1",
|
||||
tableId: "table1",
|
||||
createdAt: new Date(),
|
||||
@@ -196,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,
|
||||
@@ -219,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,
|
||||
@@ -249,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" },
|
||||
},
|
||||
],
|
||||
@@ -351,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
|
||||
);
|
||||
});
|
||||
|
||||
@@ -395,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)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -44,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 (
|
||||
@@ -133,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,34 +245,36 @@ const handleSlackIntegration = async (
|
||||
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[] = [];
|
||||
|
||||
// Derive questions from blocks
|
||||
const surveyQuestions = getElementsFromBlocks(survey.blocks);
|
||||
const surveyElements = getElementsFromBlocks(survey.blocks);
|
||||
|
||||
for (const questionId of questionIds) {
|
||||
for (const elementId of elementIds) {
|
||||
//check for hidden field Ids
|
||||
if (survey.hiddenFields.fieldIds?.includes(questionId)) {
|
||||
responses.push(processResponseData(pipelineData.response.data[questionId]));
|
||||
questions.push(questionId);
|
||||
if (survey.hiddenFields.fieldIds?.includes(elementId)) {
|
||||
responses.push(processResponseData(pipelineData.response.data[elementId]));
|
||||
elements.push(elementId);
|
||||
continue;
|
||||
}
|
||||
const question = surveyQuestions.find((q) => q.id === questionId);
|
||||
if (!question) {
|
||||
const element = surveyElements.find((q) => q.id === elementId);
|
||||
if (!element) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const responseValue = pipelineData.response.data[questionId];
|
||||
const responseValue = pipelineData.response.data[elementId];
|
||||
|
||||
if (responseValue !== undefined) {
|
||||
let answer: typeof responseValue;
|
||||
if (question.type === TSurveyElementTypeEnum.PictureSelection) {
|
||||
if (element.type === TSurveyElementTypeEnum.PictureSelection) {
|
||||
const selectedChoiceIds = responseValue as string[];
|
||||
answer = question?.choices
|
||||
answer = element?.choices
|
||||
.filter((choice) => selectedChoiceIds.includes(choice.id))
|
||||
.map((choice) => choice.imageUrl)
|
||||
.join("\n");
|
||||
@@ -279,16 +294,19 @@ const extractResponses = async (
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
questions.push(
|
||||
elements.push(
|
||||
parseRecallInfo(
|
||||
getTextContent(getLocalizedValue(question?.headline, "default")),
|
||||
getTextContent(getLocalizedValue(element?.headline, "default")),
|
||||
integrationType === "slack" ? pipelineData.response.data : emptyResponseObject,
|
||||
integrationType === "slack" ? pipelineData.response.variables : {}
|
||||
) || ""
|
||||
);
|
||||
}
|
||||
|
||||
return [responses, questions];
|
||||
return {
|
||||
responses,
|
||||
elements,
|
||||
};
|
||||
};
|
||||
|
||||
const handleNotionIntegration = async (
|
||||
@@ -326,35 +344,34 @@ const buildNotionPayloadProperties = (
|
||||
const properties: any = {};
|
||||
const responses = data.response.data;
|
||||
|
||||
// Derive questions from blocks
|
||||
const surveyQuestions = getElementsFromBlocks(surveyData.blocks);
|
||||
const surveyElements = getElementsFromBlocks(surveyData.blocks);
|
||||
|
||||
const mappingQIds = mapping
|
||||
.filter((m) => m.question.type === TSurveyElementTypeEnum.PictureSelection)
|
||||
.map((m) => m.question.id);
|
||||
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 = surveyQuestions.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,
|
||||
};
|
||||
|
||||
@@ -480,11 +480,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,
|
||||
@@ -501,11 +501,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "Open Text",
|
||||
id: "openTextQ",
|
||||
questionType: TSurveyElementTypeEnum.OpenText,
|
||||
elementType: TSurveyElementTypeEnum.OpenText,
|
||||
},
|
||||
filterType: { filterComboBoxValue: "Filled out" },
|
||||
},
|
||||
@@ -522,11 +522,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "Address",
|
||||
id: "addressQ",
|
||||
questionType: TSurveyElementTypeEnum.Address,
|
||||
elementType: TSurveyElementTypeEnum.Address,
|
||||
},
|
||||
filterType: { filterComboBoxValue: "Skipped" },
|
||||
},
|
||||
@@ -543,11 +543,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "Contact Info",
|
||||
id: "contactQ",
|
||||
questionType: TSurveyElementTypeEnum.ContactInfo,
|
||||
elementType: TSurveyElementTypeEnum.ContactInfo,
|
||||
},
|
||||
filterType: { filterComboBoxValue: "Filled out" },
|
||||
},
|
||||
@@ -564,11 +564,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "Ranking",
|
||||
id: "rankingQ",
|
||||
questionType: TSurveyElementTypeEnum.Ranking,
|
||||
elementType: TSurveyElementTypeEnum.Ranking,
|
||||
},
|
||||
filterType: { filterComboBoxValue: "Filled out" },
|
||||
},
|
||||
@@ -585,11 +585,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "MC Single",
|
||||
id: "mcSingleQ",
|
||||
questionType: TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||
elementType: TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||
},
|
||||
filterType: { filterValue: "Includes either", filterComboBoxValue: ["Choice 1"] },
|
||||
},
|
||||
@@ -606,11 +606,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "MC Multi",
|
||||
id: "mcMultiQ",
|
||||
questionType: TSurveyElementTypeEnum.MultipleChoiceMulti,
|
||||
elementType: TSurveyElementTypeEnum.MultipleChoiceMulti,
|
||||
},
|
||||
filterType: { filterValue: "Includes all", filterComboBoxValue: ["Choice 1", "Choice 2"] },
|
||||
},
|
||||
@@ -627,11 +627,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "NPS",
|
||||
id: "npsQ",
|
||||
questionType: TSurveyElementTypeEnum.NPS,
|
||||
elementType: TSurveyElementTypeEnum.NPS,
|
||||
},
|
||||
filterType: { filterValue: "Is equal to", filterComboBoxValue: "7" },
|
||||
},
|
||||
@@ -648,11 +648,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "Rating",
|
||||
id: "ratingQ",
|
||||
questionType: TSurveyElementTypeEnum.Rating,
|
||||
elementType: TSurveyElementTypeEnum.Rating,
|
||||
},
|
||||
filterType: { filterValue: "Is less than", filterComboBoxValue: "4" },
|
||||
},
|
||||
@@ -669,11 +669,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "CTA",
|
||||
id: "ctaQ",
|
||||
questionType: TSurveyElementTypeEnum.CTA,
|
||||
elementType: TSurveyElementTypeEnum.CTA,
|
||||
},
|
||||
filterType: { filterComboBoxValue: "Clicked" },
|
||||
},
|
||||
@@ -690,11 +690,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "Consent",
|
||||
id: "consentQ",
|
||||
questionType: TSurveyElementTypeEnum.Consent,
|
||||
elementType: TSurveyElementTypeEnum.Consent,
|
||||
},
|
||||
filterType: { filterComboBoxValue: "Accepted" },
|
||||
},
|
||||
@@ -711,11 +711,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "Picture",
|
||||
id: "pictureQ",
|
||||
questionType: TSurveyElementTypeEnum.PictureSelection,
|
||||
elementType: TSurveyElementTypeEnum.PictureSelection,
|
||||
},
|
||||
filterType: { filterValue: "Includes either", filterComboBoxValue: ["Picture 1"] },
|
||||
},
|
||||
@@ -732,11 +732,11 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "Matrix",
|
||||
id: "matrixQ",
|
||||
questionType: TSurveyElementTypeEnum.Matrix,
|
||||
elementType: TSurveyElementTypeEnum.Matrix,
|
||||
},
|
||||
filterType: { filterValue: "Row 1", filterComboBoxValue: "Column 1" },
|
||||
},
|
||||
@@ -753,7 +753,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" },
|
||||
},
|
||||
],
|
||||
@@ -769,7 +769,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" },
|
||||
},
|
||||
],
|
||||
@@ -785,7 +785,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" },
|
||||
},
|
||||
],
|
||||
@@ -801,7 +801,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" },
|
||||
},
|
||||
],
|
||||
@@ -817,16 +817,16 @@ describe("surveys", () => {
|
||||
responseStatus: "complete",
|
||||
filter: [
|
||||
{
|
||||
questionType: {
|
||||
type: "Questions",
|
||||
elementType: {
|
||||
type: "Elements",
|
||||
label: "NPS",
|
||||
id: "npsQ",
|
||||
questionType: TSurveyElementTypeEnum.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" },
|
||||
},
|
||||
],
|
||||
@@ -845,7 +845,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" },
|
||||
},
|
||||
],
|
||||
@@ -873,7 +873,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 },
|
||||
},
|
||||
],
|
||||
@@ -889,7 +889,7 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: { type: "Meta", label: "url", id: "url" },
|
||||
elementType: { type: "Meta", label: "url", id: "url" },
|
||||
filterType: { filterValue: "Contains", filterComboBoxValue: "" },
|
||||
},
|
||||
],
|
||||
@@ -905,7 +905,7 @@ describe("surveys", () => {
|
||||
responseStatus: "all",
|
||||
filter: [
|
||||
{
|
||||
questionType: { type: "Meta", label: "url", id: "url" },
|
||||
elementType: { type: "Meta", label: "url", id: "url" },
|
||||
filterType: { filterValue: "Contains", filterComboBoxValue: " " },
|
||||
},
|
||||
],
|
||||
@@ -921,7 +921,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"] },
|
||||
},
|
||||
],
|
||||
@@ -937,11 +937,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"] },
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -427,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");
|
||||
});
|
||||
|
||||
@@ -298,12 +298,12 @@ describe("Response Processing", () => {
|
||||
const mapping = getElementResponseMapping(mockSurvey, mockResponse);
|
||||
expect(mapping).toHaveLength(2);
|
||||
expect(mapping[0]).toEqual({
|
||||
question: "Question 1",
|
||||
element: "Question 1",
|
||||
response: "Answer 1",
|
||||
type: TSurveyElementTypeEnum.OpenText,
|
||||
});
|
||||
expect(mapping[1]).toEqual({
|
||||
question: "Question 2",
|
||||
element: "Question 2",
|
||||
response: "Option 1; Option 2",
|
||||
type: TSurveyElementTypeEnum.MultipleChoiceMulti,
|
||||
});
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -10,7 +10,7 @@ const logicRules = getLogicRules(mockT as unknown as TFunction);
|
||||
|
||||
describe("getLogicRules", () => {
|
||||
test("should return correct structure for question rules", () => {
|
||||
expect(logicRules).toHaveProperty("question");
|
||||
expect(logicRules).toHaveProperty("element");
|
||||
expect(logicRules.element).toBeInstanceOf(Object);
|
||||
});
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ export type TIntegration = z.infer<typeof ZIntegration>;
|
||||
|
||||
export const ZIntegrationBaseSurveyData = z.object({
|
||||
createdAt: z.date(),
|
||||
questionIds: z.array(z.string()),
|
||||
questions: z.string(),
|
||||
elementIds: z.array(z.string()),
|
||||
elements: z.string(),
|
||||
surveyId: z.string(),
|
||||
surveyName: z.string(),
|
||||
});
|
||||
|
||||
@@ -33,10 +33,10 @@ export type TIntegrationNotionCredential = z.infer<typeof ZIntegrationNotionCred
|
||||
|
||||
export const ZIntegrationNotionConfigData = z
|
||||
.object({
|
||||
// question -> notion database column mapping
|
||||
// element -> notion database column mapping
|
||||
mapping: z.array(
|
||||
z.object({
|
||||
question: z.object({
|
||||
element: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
@@ -53,8 +53,8 @@ export const ZIntegrationNotionConfigData = z
|
||||
})
|
||||
.merge(
|
||||
ZIntegrationBaseSurveyData.omit({
|
||||
questionIds: true,
|
||||
questions: true,
|
||||
elementIds: true,
|
||||
elements: true,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -7,12 +7,14 @@ export const ZIntegrationBase = z.object({
|
||||
|
||||
export const ZIntegrationBaseSurveyData = z.object({
|
||||
createdAt: z.date(),
|
||||
questionIds: z.array(z.string()),
|
||||
// questionIds: z.array(z.string()),
|
||||
elementIds: z.array(z.string()),
|
||||
includeVariables: z.boolean().optional(),
|
||||
includeHiddenFields: z.boolean().optional(),
|
||||
includeMetadata: z.boolean().optional(),
|
||||
includeCreatedAt: z.boolean().optional(),
|
||||
questions: z.string(),
|
||||
// questions: z.string(),
|
||||
elements: z.string(),
|
||||
surveyId: z.string(),
|
||||
surveyName: z.string(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user