mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-08 06:41:45 -05:00
feat: Hidden fields and metadata for integrations (#2752)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
afe01a61ae
commit
543d85eb28
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+64
-46
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
+2
@@ -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);
|
||||||
|
|||||||
+5
-2
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-28
@@ -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>
|
||||||
|
|||||||
+1
-3
@@ -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);
|
|
||||||
};
|
};
|
||||||
|
|||||||
+10
-2
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
+39
-22
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user