mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-24 11:39:22 -05:00
Merge branch 'main' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor
This commit is contained in:
@@ -14,10 +14,6 @@ export const metadata = {
|
||||
|
||||
The Formbricks React Native SDK can be used for seamlessly integrating App Surveys into your React Native Apps. Here, w'll explore how to leverage the SDK for in app surveys. The SDK is [available on npm.](https://www.npmjs.com/package/@formbricks/react-native)
|
||||
|
||||
<Note>
|
||||
Our React Native SDK is now in public beta. We're happy to **offer you a free 3-month trial for our Scale plan** if you're providing feedback on your developer experience. Reach out on Discord and shoot Johannes a message to get the extended trial started.
|
||||
</Note>
|
||||
|
||||
### Install
|
||||
|
||||
<Col>
|
||||
|
||||
@@ -139,9 +139,9 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Zapier", href: "/developer-docs/integrations/zapier" },
|
||||
],
|
||||
},
|
||||
{ title: "SDK: React Native", href: "/developer-docs/react-native-in-app-surveys" },
|
||||
{ title: "SDK: Web Apps", href: "/developer-docs/app-survey-sdk" },
|
||||
{ title: "SDK: Public Websites", href: "/developer-docs/website-survey-sdk" },
|
||||
{ title: "SDK: React Native", href: "/developer-docs/react-native-in-app-surveys" },
|
||||
{ title: "SDK: Formbricks API", href: "/developer-docs/api-sdk" },
|
||||
{ title: "REST API", href: "/developer-docs/rest-api" },
|
||||
{ title: "Webhooks", href: "/developer-docs/webhooks" },
|
||||
|
||||
+1
@@ -56,6 +56,7 @@ export const AddActionModal = ({
|
||||
return (
|
||||
<ModalWithTabs
|
||||
label="Add action"
|
||||
description="Capture a new action to trigger a survey on."
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
tabs={tabs}
|
||||
|
||||
+2
-2
@@ -171,14 +171,14 @@ export const CreateNewActionTab = ({
|
||||
<div>
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(submitHandler)}>
|
||||
<div className="max-h-[400px] w-full space-y-4 overflow-y-auto">
|
||||
<div className="max-h-[500px] w-full space-y-4 overflow-y-auto pr-4">
|
||||
<div className="w-3/5">
|
||||
<FormField
|
||||
name={`type`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div>
|
||||
<Label className="font-semibold">Type</Label>
|
||||
<Label className="font-semibold">Action Type</Label>
|
||||
<TabToggle
|
||||
id="type"
|
||||
options={[
|
||||
|
||||
+1
-1
@@ -110,7 +110,7 @@ export const EditWelcomeCard = ({
|
||||
<div className="mt-3 flex w-full items-center justify-center">
|
||||
<FileInput
|
||||
id="welcome-card-image"
|
||||
allowedFileExtensions={["png", "jpeg", "jpg"]}
|
||||
allowedFileExtensions={["png", "jpeg", "jpg", "webp"]}
|
||||
environmentId={environmentId}
|
||||
onFileUpload={(url: string[]) => {
|
||||
updateSurvey({ fileUrl: url[0] });
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ export const UploadImageSurveyBg = ({
|
||||
<div className="flex w-full items-center justify-center">
|
||||
<FileInput
|
||||
id="survey-bg-file-input"
|
||||
allowedFileExtensions={["png", "jpeg", "jpg"]}
|
||||
allowedFileExtensions={["png", "jpeg", "jpg", "webp"]}
|
||||
environmentId={environmentId}
|
||||
onFileUpload={(url: string[]) => {
|
||||
if (url.length > 0) {
|
||||
|
||||
+1
-1
@@ -142,7 +142,7 @@ export const PictureSelectionForm = ({
|
||||
<div className="mt-3 flex w-full items-center justify-center">
|
||||
<FileInput
|
||||
id="choices-file-input"
|
||||
allowedFileExtensions={["png", "jpeg", "jpg"]}
|
||||
allowedFileExtensions={["png", "jpeg", "jpg", "webp"]}
|
||||
environmentId={environmentId}
|
||||
onFileUpload={handleFileInputChanges}
|
||||
fileUrl={question?.choices?.map((choice) => choice.imageUrl)}
|
||||
|
||||
+9
-1
@@ -179,7 +179,9 @@ export const SurveyMenuBar = ({
|
||||
(invalidLanguage: string) => getLanguageLabel(invalidLanguage) ?? invalidLanguage
|
||||
);
|
||||
|
||||
toast.error(`${currentError.message} ${invalidLanguageLabels.join(", ")}`);
|
||||
const messageSplit = currentError.message.split("-fLang-")[0];
|
||||
|
||||
toast.error(`${messageSplit} ${invalidLanguageLabels.join(", ")}`);
|
||||
} else {
|
||||
toast.error(currentError.message);
|
||||
}
|
||||
@@ -224,6 +226,12 @@ export const SurveyMenuBar = ({
|
||||
}
|
||||
});
|
||||
|
||||
if (localSurvey.type !== "link" && !localSurvey.triggers?.length) {
|
||||
toast.error("Please set a survey trigger");
|
||||
setIsSurveySaving(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const segment = await handleSegmentUpdate();
|
||||
const updatedSurveyResponse = await updateSurveyAction({ ...localSurvey, segment });
|
||||
|
||||
|
||||
+1
-1
@@ -369,7 +369,7 @@ export const TargetingCard = ({
|
||||
{isFormbricksCloud ? (
|
||||
<UpgradePlanNotice
|
||||
message="For advanced targeting, please"
|
||||
textForUrl="upgrade to the User Identification plan."
|
||||
textForUrl="upgrade to the Scale plan."
|
||||
url={`/environments/${environmentId}/settings/billing`}
|
||||
/>
|
||||
) : (
|
||||
|
||||
+2
-2
@@ -117,7 +117,7 @@ export const EditLogo = ({ product, environmentId, isViewer }: EditLogoProps) =>
|
||||
) : (
|
||||
<FileInput
|
||||
id="logo-input"
|
||||
allowedFileExtensions={["png", "jpeg", "jpg"]}
|
||||
allowedFileExtensions={["png", "jpeg", "jpg", "webp"]}
|
||||
environmentId={environmentId}
|
||||
onFileUpload={(files: string[]) => {
|
||||
setLogoUrl(files[0]);
|
||||
@@ -129,7 +129,7 @@ export const EditLogo = ({ product, environmentId, isViewer }: EditLogoProps) =>
|
||||
<Input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/jpeg, image/png"
|
||||
accept="image/jpeg, image/png, image/webp"
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
|
||||
+3
-3
@@ -34,9 +34,9 @@ export const EditProfileAvatarForm = ({ session, environmentId, imageUrl }: Edit
|
||||
.refine((files) => files.length === 1, "You must select a file.")
|
||||
.refine((files) => {
|
||||
const file = files[0];
|
||||
const allowedTypes = ["image/jpeg", "image/png"];
|
||||
const allowedTypes = ["image/jpeg", "image/png", "image/webp"];
|
||||
return allowedTypes.includes(file.type);
|
||||
}, "Invalid file type. Only JPEG and PNG are allowed.")
|
||||
}, "Invalid file type. Only JPEG, PNG, and WEBP files are allowed.")
|
||||
.refine((files) => {
|
||||
const file = files[0];
|
||||
const maxSize = 10 * 1024 * 1024;
|
||||
@@ -145,7 +145,7 @@ export const EditProfileAvatarForm = ({ session, environmentId, imageUrl }: Edit
|
||||
inputRef.current = e;
|
||||
}}
|
||||
className="hidden"
|
||||
accept="image/jpeg, image/png"
|
||||
accept="image/jpeg, image/png, image/webp"
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.files);
|
||||
form.handleSubmit(onSubmit)();
|
||||
|
||||
@@ -4,20 +4,33 @@ import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
export const replaceAttributeRecall = (survey: TSurvey, attributes: TAttributes): TSurvey => {
|
||||
const surveyTemp = structuredClone(survey);
|
||||
const languages = Object.keys(survey.questions[0].headline);
|
||||
const languages = surveyTemp.languages
|
||||
.map((surveyLanguage) => {
|
||||
if (surveyLanguage.default) {
|
||||
return "default";
|
||||
}
|
||||
|
||||
if (surveyLanguage.enabled) {
|
||||
return surveyLanguage.language.code;
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((language) => language !== null);
|
||||
|
||||
surveyTemp.questions.forEach((question) => {
|
||||
languages.forEach((language) => {
|
||||
if (question.headline[language].includes("recall:")) {
|
||||
if (question.headline[language]?.includes("recall:")) {
|
||||
question.headline[language] = parseRecallInfo(question.headline[language], attributes);
|
||||
}
|
||||
if (question.subheader && question.subheader[language].includes("recall:")) {
|
||||
if (question.subheader && question.subheader[language]?.includes("recall:")) {
|
||||
question.subheader[language] = parseRecallInfo(question.subheader[language], attributes);
|
||||
}
|
||||
});
|
||||
});
|
||||
if (surveyTemp.welcomeCard.enabled && surveyTemp.welcomeCard.headline) {
|
||||
languages.forEach((language) => {
|
||||
if (surveyTemp.welcomeCard.headline && surveyTemp.welcomeCard.headline[language].includes("recall:")) {
|
||||
if (surveyTemp.welcomeCard.headline && surveyTemp.welcomeCard.headline[language]?.includes("recall:")) {
|
||||
surveyTemp.welcomeCard.headline[language] = parseRecallInfo(
|
||||
surveyTemp.welcomeCard.headline[language],
|
||||
attributes
|
||||
@@ -28,9 +41,9 @@ export const replaceAttributeRecall = (survey: TSurvey, attributes: TAttributes)
|
||||
surveyTemp.endings.forEach((ending) => {
|
||||
if (ending.type === "endScreen") {
|
||||
languages.forEach((language) => {
|
||||
if (ending.headline && ending.headline[language].includes("recall:")) {
|
||||
if (ending.headline && ending.headline[language]?.includes("recall:")) {
|
||||
ending.headline[language] = parseRecallInfo(ending.headline[language], attributes);
|
||||
if (ending.subheader && ending.subheader[language].includes("recall:")) {
|
||||
if (ending.subheader && ending.subheader[language]?.includes("recall:")) {
|
||||
ending.subheader[language] = parseRecallInfo(ending.subheader[language], attributes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@formbricks/web",
|
||||
"version": "2.5.1",
|
||||
"version": "2.5.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"clean": "rimraf .turbo node_modules .next",
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ async function runMigration(): Promise<void> {
|
||||
console.log(`Data migration completed. Total time: ${((endTime - startTime) / 1000).toString()}s`);
|
||||
},
|
||||
{
|
||||
timeout: 180000, // 3 minutes
|
||||
timeout: 900000, // 15 minutes
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ export const MatrixQuestion = ({
|
||||
dir="auto"
|
||||
type="radio"
|
||||
tabIndex={-1}
|
||||
required={true}
|
||||
required={question.required}
|
||||
id={`${row}-${column}`}
|
||||
name={getLocalizedValue(row, languageCode)}
|
||||
value={getLocalizedValue(column, languageCode)}
|
||||
|
||||
@@ -16,6 +16,7 @@ export const ZAllowedFileExtension = z.enum([
|
||||
"png",
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"webp",
|
||||
"pdf",
|
||||
"doc",
|
||||
"docx",
|
||||
|
||||
+10
-10
@@ -1,30 +1,30 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZDisplay = z.object({
|
||||
id: z.string().cuid(),
|
||||
id: z.string().cuid2(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
personId: z.string().cuid().nullable(),
|
||||
surveyId: z.string().cuid(),
|
||||
responseId: z.string().cuid().nullable(),
|
||||
personId: z.string().cuid2().nullable(),
|
||||
surveyId: z.string().cuid2(),
|
||||
responseId: z.string().cuid2().nullable(),
|
||||
status: z.enum(["seen", "responded"]).nullable(),
|
||||
});
|
||||
|
||||
export type TDisplay = z.infer<typeof ZDisplay>;
|
||||
|
||||
export const ZDisplayCreateInput = z.object({
|
||||
environmentId: z.string().cuid(),
|
||||
surveyId: z.string().cuid(),
|
||||
environmentId: z.string().cuid2(),
|
||||
surveyId: z.string().cuid2(),
|
||||
userId: z.string().optional(),
|
||||
responseId: z.string().cuid().optional(),
|
||||
responseId: z.string().cuid2().optional(),
|
||||
});
|
||||
|
||||
export type TDisplayCreateInput = z.infer<typeof ZDisplayCreateInput>;
|
||||
|
||||
export const ZDisplayUpdateInput = z.object({
|
||||
environmentId: z.string().cuid(),
|
||||
environmentId: z.string().cuid2(),
|
||||
userId: z.string().optional(),
|
||||
responseId: z.string().cuid().optional(),
|
||||
responseId: z.string().cuid2().optional(),
|
||||
});
|
||||
|
||||
export type TDisplayUpdateInput = z.infer<typeof ZDisplayUpdateInput>;
|
||||
@@ -42,7 +42,7 @@ export const ZDisplayFilters = z.object({
|
||||
max: z.date().optional(),
|
||||
})
|
||||
.optional(),
|
||||
responseIds: z.array(z.string().cuid()).optional(),
|
||||
responseIds: z.array(z.string().cuid2()).optional(),
|
||||
});
|
||||
|
||||
export type TDisplayFilters = z.infer<typeof ZDisplayFilters>;
|
||||
|
||||
+11
-11
@@ -24,7 +24,7 @@ export type TSurveyWithTriggers = z.infer<typeof ZSurveyWithTriggers>;
|
||||
|
||||
export const ZJSWebsiteStateDisplay = z.object({
|
||||
createdAt: z.date(),
|
||||
surveyId: z.string().cuid(),
|
||||
surveyId: z.string().cuid2(),
|
||||
responded: z.boolean(),
|
||||
});
|
||||
|
||||
@@ -65,14 +65,14 @@ export const ZJsWebsiteState = z.object({
|
||||
export type TJsWebsiteState = z.infer<typeof ZJsWebsiteState>;
|
||||
|
||||
export const ZJsWebsiteSyncInput = z.object({
|
||||
environmentId: z.string().cuid(),
|
||||
environmentId: z.string().cuid2(),
|
||||
version: z.string().optional(),
|
||||
});
|
||||
|
||||
export type TJsWebsiteSyncInput = z.infer<typeof ZJsWebsiteSyncInput>;
|
||||
|
||||
export const ZJsWebsiteConfig = z.object({
|
||||
environmentId: z.string().cuid(),
|
||||
environmentId: z.string().cuid2(),
|
||||
apiHost: z.string(),
|
||||
state: ZJsWebsiteState,
|
||||
expiresAt: z.date(),
|
||||
@@ -82,7 +82,7 @@ export const ZJsWebsiteConfig = z.object({
|
||||
export type TJsWebsiteConfig = z.infer<typeof ZJsWebsiteConfig>;
|
||||
|
||||
export const ZJSAppConfig = z.object({
|
||||
environmentId: z.string().cuid(),
|
||||
environmentId: z.string().cuid2(),
|
||||
apiHost: z.string(),
|
||||
userId: z.string(),
|
||||
state: ZJsAppState,
|
||||
@@ -93,7 +93,7 @@ export const ZJSAppConfig = z.object({
|
||||
export type TJSAppConfig = z.infer<typeof ZJSAppConfig>;
|
||||
|
||||
export const ZJsWebsiteConfigUpdateInput = z.object({
|
||||
environmentId: z.string().cuid(),
|
||||
environmentId: z.string().cuid2(),
|
||||
apiHost: z.string(),
|
||||
state: ZJsWebsiteState,
|
||||
expiresAt: z.date(),
|
||||
@@ -103,7 +103,7 @@ export const ZJsWebsiteConfigUpdateInput = z.object({
|
||||
export type TJsWebsiteConfigUpdateInput = z.infer<typeof ZJsWebsiteConfigUpdateInput>;
|
||||
|
||||
export const ZJsAppConfigUpdateInput = z.object({
|
||||
environmentId: z.string().cuid(),
|
||||
environmentId: z.string().cuid2(),
|
||||
apiHost: z.string(),
|
||||
userId: z.string(),
|
||||
state: ZJsAppState,
|
||||
@@ -114,7 +114,7 @@ export const ZJsAppConfigUpdateInput = z.object({
|
||||
export type TJsAppConfigUpdateInput = z.infer<typeof ZJsAppConfigUpdateInput>;
|
||||
|
||||
export const ZJsWebsiteConfigInput = z.object({
|
||||
environmentId: z.string().cuid(),
|
||||
environmentId: z.string().cuid2(),
|
||||
apiHost: z.string(),
|
||||
errorHandler: z.function().args(z.any()).returns(z.void()).optional(),
|
||||
attributes: z.record(z.string()).optional(),
|
||||
@@ -123,7 +123,7 @@ export const ZJsWebsiteConfigInput = z.object({
|
||||
export type TJsWebsiteConfigInput = z.infer<typeof ZJsWebsiteConfigInput>;
|
||||
|
||||
export const ZJsAppConfigInput = z.object({
|
||||
environmentId: z.string().cuid(),
|
||||
environmentId: z.string().cuid2(),
|
||||
apiHost: z.string(),
|
||||
errorHandler: z.function().args(z.any()).returns(z.void()).optional(),
|
||||
userId: z.string(),
|
||||
@@ -133,7 +133,7 @@ export const ZJsAppConfigInput = z.object({
|
||||
export type TJsAppConfigInput = z.infer<typeof ZJsAppConfigInput>;
|
||||
|
||||
export const ZJsPeopleUserIdInput = z.object({
|
||||
environmentId: z.string().cuid(),
|
||||
environmentId: z.string().cuid2(),
|
||||
userId: z.string().min(1).max(255),
|
||||
version: z.string().optional(),
|
||||
});
|
||||
@@ -154,7 +154,7 @@ export const ZJsPeopleAttributeInput = z.object({
|
||||
export type TJsPeopleAttributeInput = z.infer<typeof ZJsPeopleAttributeInput>;
|
||||
|
||||
export const ZJsActionInput = z.object({
|
||||
environmentId: z.string().cuid(),
|
||||
environmentId: z.string().cuid2(),
|
||||
userId: z.string().optional(),
|
||||
name: z.string(),
|
||||
});
|
||||
@@ -166,7 +166,7 @@ export const ZJsWesbiteActionInput = ZJsActionInput.omit({ userId: true });
|
||||
export type TJsWesbiteActionInput = z.infer<typeof ZJsWesbiteActionInput>;
|
||||
|
||||
export const ZJsAppSyncParams = z.object({
|
||||
environmentId: z.string().cuid(),
|
||||
environmentId: z.string().cuid2(),
|
||||
apiHost: z.string(),
|
||||
userId: z.string(),
|
||||
attributes: ZAttributes.optional(),
|
||||
|
||||
@@ -5,7 +5,7 @@ export type TAccessType = z.infer<typeof ZAccessType>;
|
||||
|
||||
export const ZStorageRetrievalParams = z.object({
|
||||
fileName: z.string(),
|
||||
environmentId: z.string().cuid(),
|
||||
environmentId: z.string().cuid2(),
|
||||
accessType: ZAccessType,
|
||||
});
|
||||
|
||||
|
||||
@@ -63,6 +63,23 @@ export const validateQuestionLabels = (
|
||||
questionIndex: number,
|
||||
skipArticle = false
|
||||
): z.IssueData | null => {
|
||||
// fieldLabel should contain all the keys present in languages
|
||||
// even if one of the keys is an empty string, its okay but it shouldn't be undefined
|
||||
|
||||
for (const language of languages) {
|
||||
if (
|
||||
!language.default &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- could be undefined
|
||||
fieldLabel[language.language.code] === undefined
|
||||
) {
|
||||
return {
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `The ${field} in question ${String(questionIndex + 1)} is not present for the following languages: ${language.language.code}`,
|
||||
path: ["questions", questionIndex, field],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const invalidLanguageCodes = validateLabelForAllLanguages(fieldLabel, languages);
|
||||
const isDefaultOnly = invalidLanguageCodes.length === 1 && invalidLanguageCodes[0] === "default";
|
||||
|
||||
@@ -70,10 +87,14 @@ export const validateQuestionLabels = (
|
||||
const messageField = FIELD_TO_LABEL_MAP[field] ? FIELD_TO_LABEL_MAP[field] : field;
|
||||
const messageSuffix = isDefaultOnly ? " is missing" : " is missing for the following languages: ";
|
||||
|
||||
const message = isDefaultOnly
|
||||
? `${messagePrefix}${messageField} in question ${String(questionIndex + 1)}${messageSuffix}`
|
||||
: `${messagePrefix}${messageField} in question ${String(questionIndex + 1)}${messageSuffix} -fLang- ${invalidLanguageCodes.join()}`;
|
||||
|
||||
if (invalidLanguageCodes.length) {
|
||||
return {
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `${messagePrefix}${messageField} in question ${String(questionIndex + 1)}${messageSuffix}`,
|
||||
message,
|
||||
path: ["questions", questionIndex, field],
|
||||
params: isDefaultOnly ? undefined : { invalidLanguageCodes },
|
||||
};
|
||||
@@ -90,6 +111,27 @@ export const validateCardFieldsForAllLanguages = (
|
||||
endingCardIndex?: number,
|
||||
skipArticle = false
|
||||
): z.IssueData | null => {
|
||||
// fieldLabel should contain all the keys present in languages
|
||||
// even if one of the keys is an empty string, its okay but it shouldn't be undefined
|
||||
|
||||
const cardTypeLabel =
|
||||
cardType === "welcome" ? "Welcome card" : `Redirect to Url ${((endingCardIndex ?? -1) + 1).toString()}`;
|
||||
const path = cardType === "welcome" ? ["welcomeCard", field] : ["endings", endingCardIndex ?? -1, field];
|
||||
|
||||
for (const language of languages) {
|
||||
if (
|
||||
!language.default &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- could be undefined
|
||||
fieldLabel[language.language.code] === undefined
|
||||
) {
|
||||
return {
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `The ${field} in ${cardTypeLabel} is not present for the following languages: ${language.language.code}`,
|
||||
path,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const invalidLanguageCodes = validateLabelForAllLanguages(fieldLabel, languages);
|
||||
const isDefaultOnly = invalidLanguageCodes.length === 1 && invalidLanguageCodes[0] === "default";
|
||||
|
||||
@@ -97,15 +139,15 @@ export const validateCardFieldsForAllLanguages = (
|
||||
const messageField = FIELD_TO_LABEL_MAP[field] ? FIELD_TO_LABEL_MAP[field] : field;
|
||||
const messageSuffix = isDefaultOnly ? " is missing" : " is missing for the following languages: ";
|
||||
|
||||
const message = isDefaultOnly
|
||||
? `${messagePrefix}${messageField} on the ${cardTypeLabel}${messageSuffix}`
|
||||
: `${messagePrefix}${messageField} on the ${cardTypeLabel}${messageSuffix} -fLang- ${invalidLanguageCodes.join()}`;
|
||||
|
||||
if (invalidLanguageCodes.length) {
|
||||
return {
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `${messagePrefix}${messageField} on the ${
|
||||
cardType === "welcome"
|
||||
? "Welcome card"
|
||||
: `Redirect to Url ${((endingCardIndex ?? -1) + 1).toString()}`
|
||||
} ${messageSuffix}`,
|
||||
path: cardType === "welcome" ? ["welcomeCard", field] : ["endings", endingCardIndex ?? -1, field],
|
||||
message,
|
||||
path,
|
||||
params: isDefaultOnly ? undefined : { invalidLanguageCodes },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -428,7 +428,7 @@ export const QuestionFormInput = ({
|
||||
{showImageUploader && id === "headline" && (
|
||||
<FileInput
|
||||
id="question-image"
|
||||
allowedFileExtensions={["png", "jpeg", "jpg"]}
|
||||
allowedFileExtensions={["png", "jpeg", "jpg", "webp"]}
|
||||
environmentId={localSurvey.environmentId}
|
||||
onFileUpload={(url: string[] | undefined, fileType: "image" | "video") => {
|
||||
if (url) {
|
||||
|
||||
@@ -51,7 +51,7 @@ export const SingleResponseCardHeader = ({
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||
<TooltipContent avoidCollisions align="start" side="bottom">
|
||||
<TooltipContent avoidCollisions align="start" side="bottom" className="max-w-[75vw]">
|
||||
{tooltipContent}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -78,7 +78,10 @@ export const SingleResponseCardHeader = ({
|
||||
<div>
|
||||
<p className="py-1 font-bold text-slate-700">Person attributes:</p>
|
||||
{Object.keys(response.personAttributes).map((key) => (
|
||||
<p key={key}>
|
||||
<p
|
||||
key={key}
|
||||
className="truncate"
|
||||
title={`${key}: ${response.personAttributes && response.personAttributes[key]}`}>
|
||||
{key}:{" "}
|
||||
<span className="font-bold">{response.personAttributes && response.personAttributes[key]}</span>
|
||||
</p>
|
||||
@@ -92,18 +95,44 @@ export const SingleResponseCardHeader = ({
|
||||
<hr className="my-2 border-slate-200" />
|
||||
)}
|
||||
<p className="py-1 font-bold text-slate-700">Device info:</p>
|
||||
{response.meta.userAgent?.browser && <p>Browser: {response.meta.userAgent.browser}</p>}
|
||||
{response.meta.userAgent?.os && <p>OS: {response.meta.userAgent.os}</p>}
|
||||
{response.meta.userAgent?.browser && (
|
||||
<p className="truncate" title={`Browser: ${response.meta.userAgent.browser}`}>
|
||||
Browser: {response.meta.userAgent.browser}
|
||||
</p>
|
||||
)}
|
||||
{response.meta.userAgent?.os && (
|
||||
<p className="truncate" title={`OS: ${response.meta.userAgent.os}`}>
|
||||
OS: {response.meta.userAgent.os}
|
||||
</p>
|
||||
)}
|
||||
{response.meta.userAgent && (
|
||||
<p>
|
||||
<p
|
||||
className="truncate"
|
||||
title={`Device: ${response.meta.userAgent.device ? response.meta.userAgent.device : "PC / Generic device"}`}>
|
||||
Device:{" "}
|
||||
{response.meta.userAgent.device ? response.meta.userAgent.device : "PC / Generic device"}
|
||||
</p>
|
||||
)}
|
||||
{response.meta.url && <p>URL: {response.meta.url}</p>}
|
||||
{response.meta.action && <p>Action: {response.meta.action}</p>}
|
||||
{response.meta.source && <p>Source: {response.meta.source}</p>}
|
||||
{response.meta.country && <p>Country: {response.meta.country}</p>}
|
||||
{response.meta.url && (
|
||||
<p className="truncate" title={`URL: ${response.meta.url}`}>
|
||||
URL: {response.meta.url}
|
||||
</p>
|
||||
)}
|
||||
{response.meta.action && (
|
||||
<p className="truncate" title={`Action: ${response.meta.action}`}>
|
||||
Action: {response.meta.action}
|
||||
</p>
|
||||
)}
|
||||
{response.meta.source && (
|
||||
<p className="truncate" title={`Source: ${response.meta.source}`}>
|
||||
Source: {response.meta.source}
|
||||
</p>
|
||||
)}
|
||||
{response.meta.country && (
|
||||
<p className="truncate" title={`Country: ${response.meta.country}`}>
|
||||
Country: {response.meta.country}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { Globe, PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Control, FieldArrayWithId, UseFieldArrayRemove, useFieldArray } from "react-hook-form";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Control,
|
||||
FieldArrayWithId,
|
||||
UseFieldArrayRemove,
|
||||
UseFormReturn,
|
||||
useFieldArray,
|
||||
} from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { testURLmatch } from "@formbricks/lib/utils/url";
|
||||
import { TActionClassInput, TActionClassPageUrlRule } from "@formbricks/types/action-classes";
|
||||
import { Alert, AlertDescription, AlertTitle } from "../../../Alert";
|
||||
import { Button } from "../../../Button";
|
||||
import { FormControl, FormField, FormItem } from "../../../Form";
|
||||
import { Input } from "../../../Input";
|
||||
@@ -64,7 +68,7 @@ export const PageUrlSelector = ({ form }: PageUrlSelectorProps) => {
|
||||
name="noCodeConfig.urlFilters"
|
||||
render={() => (
|
||||
<div>
|
||||
<Label className="font-semibold">Filter</Label>
|
||||
<Label className="font-semibold">Page Filter</Label>
|
||||
<p className="text-sm font-normal text-slate-500">
|
||||
Limit the pages on which this action gets captured
|
||||
</p>
|
||||
@@ -83,7 +87,7 @@ export const PageUrlSelector = ({ form }: PageUrlSelectorProps) => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{filterType === "specific" ? (
|
||||
{filterType === "specific" && (
|
||||
<div className="mb-2 mt-4 w-full space-y-3 pe-2">
|
||||
<Label>URL</Label>
|
||||
<UrlInput control={form.control} fields={fields} removeUrlRule={removeUrlRule} />
|
||||
@@ -130,14 +134,6 @@ export const PageUrlSelector = ({ form }: PageUrlSelectorProps) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mr-2">
|
||||
<Alert className="my-2">
|
||||
<Globe className="h-4 w-4" />
|
||||
<AlertTitle>Visible on all pages</AlertTitle>
|
||||
<AlertDescription>This action will be captured on all pages of your website</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user