feat: Hidden fields and metadata for integrations (#2752)

Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
This commit is contained in:
Dhruwang Jariwala
2024-06-13 16:44:43 +05:30
committed by GitHub
parent afe01a61ae
commit 543d85eb28
12 changed files with 307 additions and 115 deletions
@@ -2,6 +2,7 @@
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions"; import { authOptions } from "@formbricks/lib/authOptions";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { canUserAccessIntegration } from "@formbricks/lib/integration/auth"; import { canUserAccessIntegration } from "@formbricks/lib/integration/auth";
import { createOrUpdateIntegration, deleteIntegration } from "@formbricks/lib/integration/service"; import { createOrUpdateIntegration, deleteIntegration } from "@formbricks/lib/integration/service";
import { AuthorizationError } from "@formbricks/types/errors"; import { AuthorizationError } from "@formbricks/types/errors";
@@ -11,6 +12,12 @@ export const createOrUpdateIntegrationAction = async (
environmentId: string, environmentId: string,
integrationData: TIntegrationInput integrationData: TIntegrationInput
) => { ) => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authenticated");
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
return await createOrUpdateIntegration(environmentId, integrationData); return await createOrUpdateIntegration(environmentId, integrationData);
}; };
@@ -20,6 +20,7 @@ import {
TIntegrationAirtableTables, TIntegrationAirtableTables,
} from "@formbricks/types/integration/airtable"; } from "@formbricks/types/integration/airtable";
import { TSurvey } from "@formbricks/types/surveys"; import { TSurvey } from "@formbricks/types/surveys";
import { AdditionalIntegrationSettings } from "@formbricks/ui/AdditionalIntegrationSettings";
import { Alert, AlertDescription, AlertTitle } from "@formbricks/ui/Alert"; import { Alert, AlertDescription, AlertTitle } from "@formbricks/ui/Alert";
import { Button } from "@formbricks/ui/Button"; import { Button } from "@formbricks/ui/Button";
import { Checkbox } from "@formbricks/ui/Checkbox"; import { Checkbox } from "@formbricks/ui/Checkbox";
@@ -46,6 +47,8 @@ export type IntegrationModalInputs = {
table: string; table: string;
survey: string; survey: string;
questions: string[]; questions: string[];
includeHiddenFields: boolean;
includeMetadata: boolean;
}; };
const NoBaseFoundError = () => { const NoBaseFoundError = () => {
@@ -72,12 +75,24 @@ export const AddIntegrationModal = ({
const [tables, setTables] = useState<TIntegrationAirtableTables["tables"]>([]); const [tables, setTables] = useState<TIntegrationAirtableTables["tables"]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { handleSubmit, control, watch, setValue, reset } = useForm<IntegrationModalInputs>(); const { handleSubmit, control, watch, setValue, reset } = useForm<IntegrationModalInputs>();
const [includeHiddenFields, setIncludeHiddenFields] = useState(false);
const [includeMetadata, setIncludeMetadata] = useState(false);
const airtableIntegrationData: TIntegrationAirtableInput = {
type: "airtable",
config: {
key: airtableIntegration?.config?.key,
data: airtableIntegration.config.data ?? [],
email: airtableIntegration?.config?.email,
},
};
useEffect(() => { useEffect(() => {
if (isEditMode) { if (isEditMode) {
const { index: _index, ...rest } = defaultData; const { index: _index, ...rest } = defaultData;
reset(rest); reset(rest);
fetchTable(defaultData.base); fetchTable(defaultData.base);
setIncludeHiddenFields(defaultData.includeHiddenFields);
setIncludeMetadata(defaultData.includeMetadata);
} else { } else {
reset(); reset();
} }
@@ -104,15 +119,6 @@ export const AddIntegrationModal = ({
throw new Error("Please select at least one question"); throw new Error("Please select at least one question");
} }
const airtableIntegrationData: TIntegrationAirtableInput = {
type: "airtable",
config: {
key: airtableIntegration?.config?.key,
data: airtableIntegration.config.data ?? [],
email: airtableIntegration?.config?.email,
},
};
const currentTable = tables.find((item) => item.id === data.table); const currentTable = tables.find((item) => item.id === data.table);
const integrationData: TIntegrationAirtableConfigData = { const integrationData: TIntegrationAirtableConfigData = {
surveyId: selectedSurvey.id, surveyId: selectedSurvey.id,
@@ -124,6 +130,8 @@ export const AddIntegrationModal = ({
baseId: data.base, baseId: data.base,
tableId: data.table, tableId: data.table,
tableName: currentTable?.name ?? "", tableName: currentTable?.name ?? "",
includeHiddenFields,
includeMetadata,
}; };
if (isEditMode) { if (isEditMode) {
@@ -165,10 +173,10 @@ export const AddIntegrationModal = ({
const handleDelete = async (index: number) => { const handleDelete = async (index: number) => {
try { try {
const integrationCopy = { ...airtableIntegration }; const integrationData = structuredClone(airtableIntegrationData);
integrationCopy.config.data.splice(index, 1); integrationData.config.data.splice(index, 1);
await createOrUpdateIntegrationAction(environmentId, integrationCopy); await createOrUpdateIntegrationAction(environmentId, integrationData);
handleClose(); handleClose();
router.refresh(); router.refresh();
@@ -280,42 +288,52 @@ export const AddIntegrationModal = ({
) : null} ) : null}
{survey && selectedSurvey && ( {survey && selectedSurvey && (
<div> <div className="space-y-4">
<Label htmlFor="Surveys">Questions</Label> <div>
<div className="mt-1 rounded-lg border border-slate-200"> <Label htmlFor="Surveys">Questions</Label>
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900"> <div className="mt-1 rounded-lg border border-slate-200">
{replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions.map( <div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
(question) => ( {replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions.map(
<Controller (question) => (
key={question.id} <Controller
control={control} key={question.id}
name={"questions"} control={control}
render={({ field }) => ( name={"questions"}
<div className="my-1 flex items-center space-x-2"> render={({ field }) => (
<label htmlFor={question.id} className="flex cursor-pointer items-center"> <div className="my-1 flex items-center space-x-2">
<Checkbox <label htmlFor={question.id} className="flex cursor-pointer items-center">
type="button" <Checkbox
id={question.id} type="button"
value={question.id} id={question.id}
className="bg-white" value={question.id}
checked={field.value?.includes(question.id)} className="bg-white"
onCheckedChange={(checked) => { checked={field.value?.includes(question.id)}
return checked onCheckedChange={(checked) => {
? field.onChange([...field.value, question.id]) return checked
: field.onChange(field.value?.filter((value) => value !== question.id)); ? field.onChange([...field.value, question.id])
}} : field.onChange(
/> field.value?.filter((value) => value !== question.id)
<span className="ml-2"> );
{getLocalizedValue(question.headline, "default")} }}
</span> />
</label> <span className="ml-2">
</div> {getLocalizedValue(question.headline, "default")}
)} </span>
/> </label>
) </div>
)} )}
/>
)
)}
</div>
</div> </div>
</div> </div>
<AdditionalIntegrationSettings
includeHiddenFields={includeHiddenFields}
includeMetadata={includeMetadata}
setIncludeHiddenFields={setIncludeHiddenFields}
setIncludeMetadata={setIncludeMetadata}
/>
</div> </div>
)} )}
@@ -112,6 +112,8 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
questions: data.questionIds, questions: data.questionIds,
survey: data.surveyId, survey: data.surveyId,
table: data.tableId, table: data.tableId,
includeHiddenFields: !!data.includeHiddenFields,
includeMetadata: !!data.includeMetadata,
index, index,
}); });
setIsModalOpen(true); setIsModalOpen(true);
@@ -17,6 +17,9 @@ export async function getSpreadsheetNameByIdAction(
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId); const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
if (!isAuthorized) throw new AuthorizationError("Not authorized"); if (!isAuthorized) throw new AuthorizationError("Not authorized");
const integrationData = structuredClone(googleSheetIntegration);
return await getSpreadsheetNameById(googleSheetIntegration, spreadsheetId); integrationData.config.data.forEach((data) => {
data.createdAt = new Date(data.createdAt);
});
return await getSpreadsheetNameById(integrationData, spreadsheetId);
} }
@@ -19,6 +19,7 @@ import {
TIntegrationGoogleSheetsInput, TIntegrationGoogleSheetsInput,
} from "@formbricks/types/integration/googleSheet"; } from "@formbricks/types/integration/googleSheet";
import { TSurvey } from "@formbricks/types/surveys"; import { TSurvey } from "@formbricks/types/surveys";
import { AdditionalIntegrationSettings } from "@formbricks/ui/AdditionalIntegrationSettings";
import { Button } from "@formbricks/ui/Button"; import { Button } from "@formbricks/ui/Button";
import { Checkbox } from "@formbricks/ui/Checkbox"; import { Checkbox } from "@formbricks/ui/Checkbox";
import { DropdownSelector } from "@formbricks/ui/DropdownSelector"; import { DropdownSelector } from "@formbricks/ui/DropdownSelector";
@@ -45,7 +46,7 @@ export const AddIntegrationModal = ({
selectedIntegration, selectedIntegration,
attributeClasses, attributeClasses,
}: AddIntegrationModalProps) => { }: AddIntegrationModalProps) => {
const integrationData = { const integrationData: TIntegrationGoogleSheetsConfigData = {
spreadsheetId: "", spreadsheetId: "",
spreadsheetName: "", spreadsheetName: "",
surveyId: "", surveyId: "",
@@ -61,6 +62,8 @@ export const AddIntegrationModal = ({
const [spreadsheetUrl, setSpreadsheetUrl] = useState(""); const [spreadsheetUrl, setSpreadsheetUrl] = useState("");
const [isDeleting, setIsDeleting] = useState<boolean>(false); const [isDeleting, setIsDeleting] = useState<boolean>(false);
const existingIntegrationData = googleSheetIntegration?.config?.data; const existingIntegrationData = googleSheetIntegration?.config?.data;
const [includeHiddenFields, setIncludeHiddenFields] = useState(false);
const [includeMetadata, setIncludeMetadata] = useState(false);
const googleSheetIntegrationData: TIntegrationGoogleSheetsInput = { const googleSheetIntegrationData: TIntegrationGoogleSheetsInput = {
type: "googleSheets", type: "googleSheets",
config: { config: {
@@ -71,7 +74,7 @@ export const AddIntegrationModal = ({
}; };
useEffect(() => { useEffect(() => {
if (selectedSurvey) { if (selectedSurvey && !selectedIntegration) {
const questionIds = selectedSurvey.questions.map((question) => question.id); const questionIds = selectedSurvey.questions.map((question) => question.id);
setSelectedQuestions(questionIds); setSelectedQuestions(questionIds);
} }
@@ -86,6 +89,8 @@ export const AddIntegrationModal = ({
})! })!
); );
setSelectedQuestions(selectedIntegration.questionIds); setSelectedQuestions(selectedIntegration.questionIds);
setIncludeHiddenFields(!!selectedIntegration.includeHiddenFields);
setIncludeMetadata(!!selectedIntegration.includeMetadata);
return; return;
} else { } else {
setSpreadsheetUrl(""); setSpreadsheetUrl("");
@@ -95,7 +100,7 @@ export const AddIntegrationModal = ({
const linkSheet = async () => { const linkSheet = async () => {
try { try {
if (isValidGoogleSheetsUrl(spreadsheetUrl)) { if (!isValidGoogleSheetsUrl(spreadsheetUrl)) {
throw new Error("Please enter a valid spreadsheet url"); throw new Error("Please enter a valid spreadsheet url");
} }
if (!selectedSurvey) { if (!selectedSurvey) {
@@ -110,6 +115,7 @@ export const AddIntegrationModal = ({
environmentId, environmentId,
spreadsheetId spreadsheetId
); );
setIsLinkingSheet(true); setIsLinkingSheet(true);
integrationData.spreadsheetId = spreadsheetId; integrationData.spreadsheetId = spreadsheetId;
integrationData.spreadsheetName = spreadsheetName; integrationData.spreadsheetName = spreadsheetName;
@@ -121,6 +127,8 @@ export const AddIntegrationModal = ({
? "All questions" ? "All questions"
: "Selected questions"; : "Selected questions";
integrationData.createdAt = new Date(); integrationData.createdAt = new Date();
integrationData.includeHiddenFields = includeHiddenFields;
integrationData.includeMetadata = includeMetadata;
if (selectedIntegration) { if (selectedIntegration) {
// update action // update action
googleSheetIntegrationData.config!.data[selectedIntegration.index] = integrationData; googleSheetIntegrationData.config!.data[selectedIntegration.index] = integrationData;
@@ -148,12 +156,16 @@ export const AddIntegrationModal = ({
}; };
const setOpenWithStates = (isOpen: boolean) => { const setOpenWithStates = (isOpen: boolean) => {
resetForm();
setOpen(isOpen); setOpen(isOpen);
}; };
const resetForm = () => { const resetForm = () => {
setSpreadsheetUrl("");
setIsLinkingSheet(false); setIsLinkingSheet(false);
setSelectedSurvey(null); setSelectedSurvey(null);
setIncludeHiddenFields(false);
setIncludeMetadata(false);
}; };
const deleteLink = async () => { const deleteLink = async () => {
@@ -214,33 +226,41 @@ export const AddIntegrationModal = ({
</div> </div>
</div> </div>
{selectedSurvey && ( {selectedSurvey && (
<div> <div className="space-y-4">
<Label htmlFor="Surveys">Questions</Label> <div>
<div className="mt-1 rounded-lg border border-slate-200"> <Label htmlFor="Surveys">Questions</Label>
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900"> <div className="mt-1 rounded-lg border border-slate-200">
{replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions.map( <div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
(question) => ( {replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions.map(
<div key={question.id} className="my-1 flex items-center space-x-2"> (question) => (
<label htmlFor={question.id} className="flex cursor-pointer items-center"> <div key={question.id} className="my-1 flex items-center space-x-2">
<Checkbox <label htmlFor={question.id} className="flex cursor-pointer items-center">
type="button" <Checkbox
id={question.id} type="button"
value={question.id} id={question.id}
className="bg-white" value={question.id}
checked={selectedQuestions.includes(question.id)} className="bg-white"
onCheckedChange={() => { checked={selectedQuestions.includes(question.id)}
handleCheckboxChange(question.id); onCheckedChange={() => {
}} handleCheckboxChange(question.id);
/> }}
<span className="ml-2 w-[30rem] truncate"> />
{getLocalizedValue(question.headline, "default")} <span className="ml-2 w-[30rem] truncate">
</span> {getLocalizedValue(question.headline, "default")}
</label> </span>
</div> </label>
) </div>
)} )
)}
</div>
</div> </div>
</div> </div>
<AdditionalIntegrationSettings
includeHiddenFields={includeHiddenFields}
includeMetadata={includeMetadata}
setIncludeHiddenFields={setIncludeHiddenFields}
setIncludeMetadata={setIncludeMetadata}
/>
</div> </div>
)} )}
</div> </div>
@@ -14,7 +14,5 @@ export const constructGoogleSheetsUrl = (spreadsheetId: string): string => {
}; };
export const isValidGoogleSheetsUrl = (url: string): boolean => { export const isValidGoogleSheetsUrl = (url: string): boolean => {
// Regular expression to match Google Sheets URL format return url.startsWith("https://docs.google.com/spreadsheets/d/");
const googleSheetsUrlRegex = /^https:\/\/docs\.google\.com\/spreadsheets\/d\/[a-zA-Z0-9-_]+\/?$/;
return googleSheetsUrlRegex.test(url);
}; };
@@ -121,11 +121,19 @@ export const AddIntegrationModal = ({
const hiddenFields = selectedSurvey?.hiddenFields.enabled const hiddenFields = selectedSurvey?.hiddenFields.enabled
? selectedSurvey?.hiddenFields.fieldIds?.map((fId) => ({ ? selectedSurvey?.hiddenFields.fieldIds?.map((fId) => ({
id: fId, id: fId,
name: fId, name: `Hidden field : ${fId}`,
type: TSurveyQuestionTypeEnum.OpenText, type: TSurveyQuestionTypeEnum.OpenText,
})) || [] })) || []
: []; : [];
return [...questions, ...hiddenFields]; const Metadata = [
{
id: "metadata",
name: `Metadata`,
type: TSurveyQuestionTypeEnum.OpenText,
},
];
return [...questions, ...hiddenFields, ...Metadata];
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedSurvey?.id]); }, [selectedSurvey?.id]);
@@ -14,6 +14,7 @@ import {
TIntegrationSlackInput, TIntegrationSlackInput,
} from "@formbricks/types/integration/slack"; } from "@formbricks/types/integration/slack";
import { TSurvey } from "@formbricks/types/surveys"; import { TSurvey } from "@formbricks/types/surveys";
import { AdditionalIntegrationSettings } from "@formbricks/ui/AdditionalIntegrationSettings";
import { Button } from "@formbricks/ui/Button"; import { Button } from "@formbricks/ui/Button";
import { Checkbox } from "@formbricks/ui/Checkbox"; import { Checkbox } from "@formbricks/ui/Checkbox";
import { DropdownSelector } from "@formbricks/ui/DropdownSelector"; import { DropdownSelector } from "@formbricks/ui/DropdownSelector";
@@ -48,6 +49,8 @@ export const AddChannelMappingModal = ({
const [selectedSurvey, setSelectedSurvey] = useState<TSurvey | null>(null); const [selectedSurvey, setSelectedSurvey] = useState<TSurvey | null>(null);
const [selectedChannel, setSelectedChannel] = useState<TIntegrationItem | null>(null); const [selectedChannel, setSelectedChannel] = useState<TIntegrationItem | null>(null);
const [isDeleting, setIsDeleting] = useState<boolean>(false); const [isDeleting, setIsDeleting] = useState<boolean>(false);
const [includeHiddenFields, setIncludeHiddenFields] = useState(false);
const [includeMetadata, setIncludeMetadata] = useState(false);
const existingIntegrationData = slackIntegration?.config?.data; const existingIntegrationData = slackIntegration?.config?.data;
const slackIntegrationData: TIntegrationSlackInput = { const slackIntegrationData: TIntegrationSlackInput = {
type: "slack", type: "slack",
@@ -78,6 +81,8 @@ export const AddChannelMappingModal = ({
})! })!
); );
setSelectedQuestions(selectedIntegration.questionIds); setSelectedQuestions(selectedIntegration.questionIds);
setIncludeHiddenFields(!!selectedIntegration.includeHiddenFields);
setIncludeMetadata(!!selectedIntegration.includeMetadata);
return; return;
} }
resetForm(); resetForm();
@@ -107,6 +112,8 @@ export const AddChannelMappingModal = ({
? "All questions" ? "All questions"
: "Selected questions", : "Selected questions",
createdAt: new Date(), createdAt: new Date(),
includeHiddenFields,
includeMetadata,
}; };
if (selectedIntegration) { if (selectedIntegration) {
// update action // update action
@@ -222,30 +229,40 @@ export const AddChannelMappingModal = ({
</div> </div>
{selectedSurvey && ( {selectedSurvey && (
<div> <div>
<Label htmlFor="Surveys">Questions</Label> <div>
<div className="mt-1 rounded-lg border border-slate-200"> <Label htmlFor="Surveys">Questions</Label>
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900"> <div className="mt-1 rounded-lg border border-slate-200">
{replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions?.map( <div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
(question) => ( {replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions?.map(
<div key={question.id} className="my-1 flex items-center space-x-2"> (question) => (
<label htmlFor={question.id} className="flex cursor-pointer items-center"> <div key={question.id} className="my-1 flex items-center space-x-2">
<Checkbox <label htmlFor={question.id} className="flex cursor-pointer items-center">
type="button" <Checkbox
id={question.id} type="button"
value={question.id} id={question.id}
className="bg-white" value={question.id}
checked={selectedQuestions.includes(question.id)} className="bg-white"
onCheckedChange={() => { checked={selectedQuestions.includes(question.id)}
handleCheckboxChange(question.id); onCheckedChange={() => {
}} handleCheckboxChange(question.id);
/> }}
<span className="ml-2">{getLocalizedValue(question.headline, "default")}</span> />
</label> <span className="ml-2">
</div> {getLocalizedValue(question.headline, "default")}
) </span>
)} </label>
</div>
)
)}
</div>
</div> </div>
</div> </div>
<AdditionalIntegrationSettings
includeHiddenFields={includeHiddenFields}
includeMetadata={includeMetadata}
setIncludeHiddenFields={setIncludeHiddenFields}
setIncludeMetadata={setIncludeMetadata}
/>
</div> </div>
)} )}
</div> </div>
@@ -10,8 +10,42 @@ import { TIntegrationGoogleSheets } from "@formbricks/types/integration/googleSh
import { TIntegrationNotion, TIntegrationNotionConfigData } from "@formbricks/types/integration/notion"; import { TIntegrationNotion, TIntegrationNotionConfigData } from "@formbricks/types/integration/notion";
import { TIntegrationSlack } from "@formbricks/types/integration/slack"; import { TIntegrationSlack } from "@formbricks/types/integration/slack";
import { TPipelineInput } from "@formbricks/types/pipelines"; import { TPipelineInput } from "@formbricks/types/pipelines";
import { TResponseMeta } from "@formbricks/types/responses";
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys"; import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys";
const convertMetaObjectToString = (metadata: TResponseMeta): string => {
let result: string[] = [];
if (metadata.source) result.push(`Source: ${metadata.source}`);
if (metadata.url) result.push(`URL: ${metadata.url}`);
if (metadata.userAgent?.browser) result.push(`Browser: ${metadata.userAgent.browser}`);
if (metadata.userAgent?.os) result.push(`OS: ${metadata.userAgent.os}`);
if (metadata.userAgent?.device) result.push(`Device: ${metadata.userAgent.device}`);
if (metadata.country) result.push(`Country: ${metadata.country}`);
if (metadata.action) result.push(`Action: ${metadata.action}`);
// Join all the elements in the result array with a newline for formatting
return result.join("\n");
};
const processDataForIntegration = async (
data: TPipelineInput,
survey: TSurvey,
includeMetadata: boolean,
includeHiddenFields: boolean,
questionIds: string[]
): Promise<string[][]> => {
const ids =
includeHiddenFields && survey.hiddenFields.fieldIds
? [...questionIds, ...survey.hiddenFields.fieldIds]
: questionIds;
const values = await extractResponses(data, ids, survey);
if (includeMetadata) {
values[0].push(convertMetaObjectToString(data.response.meta));
values[1].push("Metadata");
}
return values;
};
export const handleIntegrations = async ( export const handleIntegrations = async (
integrations: TIntegration[], integrations: TIntegration[],
data: TPipelineInput, data: TPipelineInput,
@@ -43,8 +77,13 @@ const handleAirtableIntegration = async (
if (integration.config.data.length > 0) { if (integration.config.data.length > 0) {
for (const element of integration.config.data) { for (const element of integration.config.data) {
if (element.surveyId === data.surveyId) { if (element.surveyId === data.surveyId) {
const values = await extractResponses(data, element.questionIds as string[], survey); const values = await processDataForIntegration(
data,
survey,
!!element.includeMetadata,
!!element.includeHiddenFields,
element.questionIds
);
await airtableWriteData(integration.config.key, element, values); await airtableWriteData(integration.config.key, element, values);
} }
} }
@@ -59,11 +98,18 @@ const handleGoogleSheetsIntegration = async (
if (integration.config.data.length > 0) { if (integration.config.data.length > 0) {
for (const element of integration.config.data) { for (const element of integration.config.data) {
if (element.surveyId === data.surveyId) { if (element.surveyId === data.surveyId) {
const values = await extractResponses(data, element.questionIds, survey); const values = await processDataForIntegration(
data,
survey,
!!element.includeMetadata,
!!element.includeHiddenFields,
element.questionIds
);
const integrationData = structuredClone(integration); const integrationData = structuredClone(integration);
integrationData.config.data.forEach((data) => { integrationData.config.data.forEach((data) => {
data.createdAt = new Date(data.createdAt); data.createdAt = new Date(data.createdAt);
}); });
await writeData(integrationData, element.spreadsheetId, values); await writeData(integrationData, element.spreadsheetId, values);
} }
} }
@@ -78,7 +124,13 @@ const handleSlackIntegration = async (
if (integration.config.data.length > 0) { if (integration.config.data.length > 0) {
for (const element of integration.config.data) { for (const element of integration.config.data) {
if (element.surveyId === data.surveyId) { if (element.surveyId === data.surveyId) {
const values = await extractResponses(data, element.questionIds as string[], survey); const values = await processDataForIntegration(
data,
survey,
!!element.includeMetadata,
!!element.includeHiddenFields,
element.questionIds
);
await writeDataToSlack(integration.config.key, element.channelId, values, survey?.name); await writeDataToSlack(integration.config.key, element.channelId, values, survey?.name);
} }
} }
@@ -86,7 +138,7 @@ const handleSlackIntegration = async (
}; };
const extractResponses = async ( const extractResponses = async (
data: TPipelineInput, pipelineData: TPipelineInput,
questionIds: string[], questionIds: string[],
survey: TSurvey survey: TSurvey
): Promise<string[][]> => { ): Promise<string[][]> => {
@@ -94,12 +146,18 @@ const extractResponses = async (
const questions: string[] = []; const questions: string[] = [];
for (const questionId of questionIds) { 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); const question = survey?.questions.find((q) => q.id === questionId);
if (!question) { if (!question) {
continue; continue;
} }
const responseValue = data.response.data[questionId]; const responseValue = pipelineData.response.data[questionId];
if (responseValue !== undefined) { if (responseValue !== undefined) {
let answer: typeof responseValue; let answer: typeof responseValue;
@@ -162,11 +220,16 @@ const buildNotionPayloadProperties = (
}); });
mapping.forEach((map) => { mapping.forEach((map) => {
const value = responses[map.question.id]; if (map.question.id === "metadata") {
properties[map.column.name] = {
properties[map.column.name] = { [map.column.type]: getValue(map.column.type, convertMetaObjectToString(data.response.meta)),
[map.column.type]: getValue(map.column.type, processResponseData(value)), };
}; } else {
const value = responses[map.question.id];
properties[map.column.name] = {
[map.column.type]: getValue(map.column.type, processResponseData(value)),
};
}
}); });
return properties; return properties;
@@ -10,6 +10,8 @@ export const ZIntegrationBase = z.object({
export const ZIntegrationBaseSurveyData = z.object({ export const ZIntegrationBaseSurveyData = z.object({
createdAt: z.date(), createdAt: z.date(),
questionIds: z.array(z.string()), questionIds: z.array(z.string()),
includeHiddenFields: z.boolean().optional(),
includeMetadata: z.boolean().optional(),
questions: z.string(), questions: z.string(),
surveyId: z.string(), surveyId: z.string(),
surveyName: z.string(), surveyName: z.string(),
@@ -0,0 +1,54 @@
import { Checkbox } from "../Checkbox";
import { Label } from "../Label";
interface AdditionalIntegrationSettingsProps {
includeHiddenFields: boolean;
includeMetadata: boolean;
setIncludeHiddenFields: (includeHiddenFields: boolean) => void;
setIncludeMetadata: (includeHiddenFields: boolean) => void;
}
export const AdditionalIntegrationSettings = ({
includeHiddenFields,
includeMetadata,
setIncludeHiddenFields,
setIncludeMetadata,
}: AdditionalIntegrationSettingsProps) => {
return (
<div className="mt-4">
<Label htmlFor="Surveys">Additional Setings</Label>
<div className="text-sm">
<div className="my-1 flex items-center space-x-2">
<label htmlFor={"includeHiddenFields"} className="flex cursor-pointer items-center">
<Checkbox
type="button"
id={"includeHiddenFields"}
value={"includeHiddenFields"}
className="bg-white"
checked={includeHiddenFields}
onCheckedChange={() => {
setIncludeHiddenFields(!includeHiddenFields);
}}
/>
<span className="ml-2 w-[30rem] truncate">Include Hidden Fields</span>
</label>
</div>
<div className="my-1 flex items-center space-x-2">
<label htmlFor={"includeMetadata"} className="flex cursor-pointer items-center">
<Checkbox
type="button"
id={"includeMetadata"}
value={"includeMetadata"}
className="bg-white"
checked={includeMetadata}
onCheckedChange={() => {
setIncludeMetadata(!includeMetadata);
}}
/>
<span className="ml-2 w-[30rem] truncate">Include Metadata (Browser, Country, etc.)</span>
</label>
</div>
</div>
</div>
);
};
+1 -1
View File
@@ -53,7 +53,7 @@ export const DropdownSelector = ({
className="z-50 max-h-64 min-w-[220px] max-w-[90%] overflow-auto rounded-md bg-white text-sm text-slate-800 shadow-md" className="z-50 max-h-64 min-w-[220px] max-w-[90%] overflow-auto rounded-md bg-white text-sm text-slate-800 shadow-md"
align="start"> align="start">
{items {items
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name?.localeCompare(b.name))
.map((item) => ( .map((item) => (
<DropdownMenuItem <DropdownMenuItem
key={item.id} key={item.id}