@@ -250,19 +250,15 @@ export const MultiLanguageCard: FC = ({
/>
) : (
<>
- {projectLanguages.length <= 1 && (
-
- {projectLanguages.length === 0
- ? t("environments.surveys.edit.no_languages_found_add_first_one_to_get_started")
- : t(
- "environments.surveys.edit.you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations"
- )}
+ {projectLanguages.length === 0 && (
+
+ {t("environments.surveys.edit.no_languages_found_add_first_one_to_get_started")}
)}
- {projectLanguages.length > 1 && (
+ {projectLanguages.length > 0 && (
{isMultiLanguageAllowed && !isMultiLanguageActivated ? (
-
+
{t("environments.surveys.edit.switch_multi_language_on_to_get_started")}
) : null}
@@ -276,7 +272,7 @@ export const MultiLanguageCard: FC
= ({
setConfirmationModalInfo={setConfirmationModalInfo}
locale={locale}
/>
- {defaultLanguage ? (
+ {defaultLanguage && projectLanguages.length > 1 ? (
field.onChange(password)}
value={field.value}
- className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
+ className="block w-full rounded-md border-slate-300 shadow-sm focus:border-brand-dark focus:ring-brand-dark sm:text-sm"
/>
{error?.message && {error.message}}
diff --git a/apps/web/modules/ee/two-factor-auth/components/disable-two-factor-modal.tsx b/apps/web/modules/ee/two-factor-auth/components/disable-two-factor-modal.tsx
index 72d7c773b4..5ece8bd615 100644
--- a/apps/web/modules/ee/two-factor-auth/components/disable-two-factor-modal.tsx
+++ b/apps/web/modules/ee/two-factor-auth/components/disable-two-factor-modal.tsx
@@ -109,7 +109,7 @@ export const DisableTwoFactorModal = ({ open, setOpen }: DisableTwoFactorModalPr
required
onChange={(password) => field.onChange(password)}
value={field.value}
- className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
+ className="block w-full rounded-md border-slate-300 shadow-sm focus:border-brand-dark focus:ring-brand-dark sm:text-sm"
/>
{error?.message && {error.message}}
diff --git a/apps/web/modules/ee/two-factor-auth/components/two-factor-backup.tsx b/apps/web/modules/ee/two-factor-auth/components/two-factor-backup.tsx
index d3e6b0b72d..ec549435e7 100644
--- a/apps/web/modules/ee/two-factor-auth/components/two-factor-backup.tsx
+++ b/apps/web/modules/ee/two-factor-auth/components/two-factor-backup.tsx
@@ -35,7 +35,7 @@ export const TwoFactorBackup = ({ form }: TwoFactorBackupProps) => {
id="totpBackup"
required
placeholder="XXXXX-XXXXX"
- className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
+ className="block w-full rounded-md border-slate-300 shadow-sm focus:border-brand-dark focus:ring-brand-dark sm:text-sm"
value={field.value}
onChange={(e) => field.onChange(e.target.value)}
/>
diff --git a/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.tsx b/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.tsx
index 4f2ceb97c9..9ac826d3ec 100644
--- a/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.tsx
+++ b/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.tsx
@@ -206,7 +206,7 @@ export const EmailCustomizationSettings = ({
{t("environments.settings.general.logo_in_email_header")}
-
+
{logoUrl && (
@@ -276,7 +276,7 @@ export const EmailCustomizationSettings = ({
-
+
+
{t("common.only_owners_managers_and_manage_access_members_can_perform_this_action")}
diff --git a/apps/web/modules/integrations/webhooks/components/webhook-detail-modal.tsx b/apps/web/modules/integrations/webhooks/components/webhook-detail-modal.tsx
index 514e117748..74b5b358d0 100644
--- a/apps/web/modules/integrations/webhooks/components/webhook-detail-modal.tsx
+++ b/apps/web/modules/integrations/webhooks/components/webhook-detail-modal.tsx
@@ -70,7 +70,7 @@ export const WebhookModal = ({ open, setOpen, webhook, surveys, isReadOnly }: We
type="button"
className={`mr-4 px-1 pb-3 focus:outline-none ${
activeTab === index
- ? "border-brand-dark border-b-2 font-semibold text-slate-900"
+ ? "border-b-2 border-brand-dark font-semibold text-slate-900"
: "text-slate-500 hover:text-slate-700"
}`}
onClick={() => handleTabClick(index)}>
diff --git a/apps/web/modules/organization/settings/api-keys/components/edit-api-keys.tsx b/apps/web/modules/organization/settings/api-keys/components/edit-api-keys.tsx
index f735e1b9de..b40b99e05d 100644
--- a/apps/web/modules/organization/settings/api-keys/components/edit-api-keys.tsx
+++ b/apps/web/modules/organization/settings/api-keys/components/edit-api-keys.tsx
@@ -134,7 +134,7 @@ export const EditAPIKeys = ({ organizationId, apiKeys, locale, isReadOnly, proje
return (
-
{apiKey}
+
{apiKey}
{apiKeysLocal?.length === 0 ? (
-
+
{t("environments.workspace.api_keys.no_api_keys_yet")}
) : (
diff --git a/apps/web/modules/organization/settings/api-keys/loading.tsx b/apps/web/modules/organization/settings/api-keys/loading.tsx
index c19854bdbf..b12b9c9fb9 100644
--- a/apps/web/modules/organization/settings/api-keys/loading.tsx
+++ b/apps/web/modules/organization/settings/api-keys/loading.tsx
@@ -10,7 +10,7 @@ const LoadingCard = () => {
return (
-
+
{t("common.loading")}
diff --git a/apps/web/modules/organization/settings/api-keys/types/api-keys.ts b/apps/web/modules/organization/settings/api-keys/types/api-keys.ts
index 81de781f2a..f6c9b7f748 100644
--- a/apps/web/modules/organization/settings/api-keys/types/api-keys.ts
+++ b/apps/web/modules/organization/settings/api-keys/types/api-keys.ts
@@ -50,8 +50,10 @@ export const TApiKeyEnvironmentPermission = z.object({
export type TApiKeyEnvironmentPermission = z.infer;
-export interface TApiKeyWithEnvironmentPermission
- extends Pick {
+export interface TApiKeyWithEnvironmentPermission extends Pick<
+ ApiKey,
+ "id" | "label" | "createdAt" | "organizationAccess"
+> {
apiKeyEnvironments: TApiKeyEnvironmentPermission[];
}
diff --git a/apps/web/modules/organization/settings/teams/components/edit-memberships/member-actions.tsx b/apps/web/modules/organization/settings/teams/components/edit-memberships/member-actions.tsx
index 9f40b78134..bfdc75071f 100644
--- a/apps/web/modules/organization/settings/teams/components/edit-memberships/member-actions.tsx
+++ b/apps/web/modules/organization/settings/teams/components/edit-memberships/member-actions.tsx
@@ -168,7 +168,7 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
{
- const inviteUserActionResult = await inviteUserAction({
- organizationId: organization.id,
- email: email.toLowerCase(),
- name,
- role,
- teamIds,
- });
- return {
- email,
- success: Boolean(inviteUserActionResult?.data),
- };
- })
- );
- let failedInvites: string[] = [];
- let successInvites: string[] = [];
- invitePromises.forEach((invite) => {
+ const inviteResults: { email: string; success: boolean }[] = [];
+ for (const { name, email, role, teamIds } of data) {
+ const inviteUserActionResult = await inviteUserAction({
+ organizationId: organization.id,
+ email: email.toLowerCase(),
+ name,
+ role,
+ teamIds,
+ });
+ inviteResults.push({
+ email,
+ success: Boolean(inviteUserActionResult?.data),
+ });
+ }
+ const failedInvites: string[] = [];
+ const successInvites: string[] = [];
+ inviteResults.forEach((invite) => {
if (!invite.success) {
failedInvites.push(invite.email);
} else {
diff --git a/apps/web/modules/organization/settings/teams/types/invites.ts b/apps/web/modules/organization/settings/teams/types/invites.ts
index 3434a2c06c..728c668c38 100644
--- a/apps/web/modules/organization/settings/teams/types/invites.ts
+++ b/apps/web/modules/organization/settings/teams/types/invites.ts
@@ -3,8 +3,10 @@ import { z } from "zod";
import { ZInvite } from "@formbricks/database/zod/invites";
import { ZUserName } from "@formbricks/types/user";
-export interface TInvite
- extends Omit {}
+export interface TInvite extends Omit<
+ Invite,
+ "deprecatedRole" | "organizationId" | "creatorId" | "acceptorId" | "teamIds"
+> {}
export interface InviteWithCreator extends Pick {
creator: {
diff --git a/apps/web/modules/projects/settings/(setup)/components/ActionActivityTab.tsx b/apps/web/modules/projects/settings/(setup)/components/ActionActivityTab.tsx
index dc2ff668b0..f462c65bce 100644
--- a/apps/web/modules/projects/settings/(setup)/components/ActionActivityTab.tsx
+++ b/apps/web/modules/projects/settings/(setup)/components/ActionActivityTab.tsx
@@ -151,7 +151,7 @@ export const ActionActivityTab = ({
{ACTION_TYPE_ICON_LOOKUP[actionClass.type]}
-
{actionClass.type}
+
{actionClass.type}
diff --git a/apps/web/modules/projects/settings/general/components/custom-scripts-form.tsx b/apps/web/modules/projects/settings/general/components/custom-scripts-form.tsx
index 60da3212e2..3e93d2d554 100644
--- a/apps/web/modules/projects/settings/general/components/custom-scripts-form.tsx
+++ b/apps/web/modules/projects/settings/general/components/custom-scripts-form.tsx
@@ -90,7 +90,7 @@ export const CustomScriptsForm: React.FC
= ({ project, i
rows={8}
placeholder={t("environments.workspace.general.custom_scripts_placeholder")}
className={cn(
- "focus:border-brand-dark flex w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 font-mono text-xs text-slate-800 placeholder:text-slate-400 focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50",
+ "flex w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 font-mono text-xs text-slate-800 placeholder:text-slate-400 focus:border-brand-dark focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
isReadOnly && "bg-slate-50"
)}
{...field}
diff --git a/apps/web/modules/projects/settings/general/components/delete-project-render.tsx b/apps/web/modules/projects/settings/general/components/delete-project-render.tsx
index 8c230db35b..28809e6292 100644
--- a/apps/web/modules/projects/settings/general/components/delete-project-render.tsx
+++ b/apps/web/modules/projects/settings/general/components/delete-project-render.tsx
@@ -89,7 +89,7 @@ export const DeleteProjectRender = ({
)}
- {!IS_FORMBRICKS_CLOUD && !IS_DEVELOPMENT && (
-
- )}
);
diff --git a/apps/web/modules/projects/settings/lib/tag.test.ts b/apps/web/modules/projects/settings/lib/tag.test.ts
index b06931f7ea..28e2675f4e 100644
--- a/apps/web/modules/projects/settings/lib/tag.test.ts
+++ b/apps/web/modules/projects/settings/lib/tag.test.ts
@@ -152,13 +152,13 @@ describe("tag lib", () => {
.mockResolvedValueOnce(baseTag as any)
.mockResolvedValueOnce(newTag as any);
vi.mocked(prisma.response.findMany).mockResolvedValueOnce([{ id: "resp1" }] as any);
- vi.mocked(prisma.$transaction).mockResolvedValueOnce(undefined).mockResolvedValueOnce(undefined);
+ vi.mocked(prisma.$transaction).mockResolvedValueOnce(undefined);
const result = await mergeTags(baseTag.id, newTag.id);
expect(result).toEqual(ok(newTag));
expect(prisma.tag.findUnique).toHaveBeenCalledWith({ where: { id: baseTag.id } });
expect(prisma.tag.findUnique).toHaveBeenCalledWith({ where: { id: newTag.id } });
expect(prisma.response.findMany).toHaveBeenCalled();
- expect(prisma.$transaction).toHaveBeenCalledTimes(2);
+ expect(prisma.$transaction).toHaveBeenCalledTimes(1);
});
test("merges tags with no responses with both tags", async () => {
vi.mocked(prisma.tag.findUnique)
@@ -195,6 +195,20 @@ describe("tag lib", () => {
});
}
});
+ test("returns error when merging a tag into itself", async () => {
+ const result = await mergeTags(baseTag.id, baseTag.id);
+ expect(result.ok).toBe(false);
+
+ if (!result.ok) {
+ expect(result.error).toStrictEqual({
+ code: "merge_same_tag",
+ message: "Cannot merge a tag into itself",
+ });
+ }
+
+ expect(prisma.tag.findUnique).not.toHaveBeenCalled();
+ expect(prisma.$transaction).not.toHaveBeenCalled();
+ });
test("throws on prisma error", async () => {
vi.mocked(prisma.tag.findUnique).mockRejectedValueOnce(new Error("fail"));
const result = await mergeTags(baseTag.id, newTag.id);
diff --git a/apps/web/modules/projects/settings/lib/tag.ts b/apps/web/modules/projects/settings/lib/tag.ts
index 73505fe78f..c143574e31 100644
--- a/apps/web/modules/projects/settings/lib/tag.ts
+++ b/apps/web/modules/projects/settings/lib/tag.ts
@@ -72,6 +72,13 @@ export const mergeTags = async (
): Promise }>> => {
validateInputs([originalTagId, ZId], [newTagId, ZId]);
+ if (originalTagId === newTagId) {
+ return err({
+ code: TagError.MERGE_SAME_TAG,
+ message: "Cannot merge a tag into itself",
+ });
+ }
+
try {
let originalTag: TTag | null;
@@ -103,90 +110,35 @@ export const mergeTags = async (
});
}
- // finds all the responses that have both the tags
- let responsesWithBothTags = await prisma.response.findMany({
+ // Find responses that have both tags to avoid unique constraint violations during merge
+ const responsesWithBothTags = await prisma.response.findMany({
where: {
- AND: [
- {
- tags: {
- some: {
- tagId: {
- in: [originalTagId],
- },
- },
- },
- },
- {
- tags: {
- some: {
- tagId: {
- in: [newTagId],
- },
- },
- },
- },
- ],
+ AND: [{ tags: { some: { tagId: originalTagId } } }, { tags: { some: { tagId: newTagId } } }],
},
+ select: { id: true },
});
- if (!!responsesWithBothTags?.length) {
- await Promise.all(
- responsesWithBothTags.map(async (response) => {
- await prisma.$transaction([
- prisma.tagsOnResponses.deleteMany({
- where: {
- responseId: response.id,
- tagId: {
- in: [originalTagId, newTagId],
- },
- },
- }),
-
- prisma.tagsOnResponses.create({
- data: {
- responseId: response.id,
- tagId: newTagId,
- },
- }),
- ]);
- })
- );
-
- await prisma.$transaction([
- prisma.tagsOnResponses.updateMany({
- where: {
- tagId: originalTagId,
- },
- data: {
- tagId: newTagId,
- },
- }),
-
- prisma.tag.delete({
- where: {
- id: originalTagId,
- },
- }),
- ]);
-
- return ok(newTag);
- }
+ const conflictResponseIds = responsesWithBothTags.map((r) => r.id);
await prisma.$transaction([
+ // Remove originalTag from responses that already have newTag (prevents unique constraint violation)
+ ...(conflictResponseIds.length > 0
+ ? [
+ prisma.tagsOnResponses.deleteMany({
+ where: {
+ responseId: { in: conflictResponseIds },
+ tagId: originalTagId,
+ },
+ }),
+ ]
+ : []),
+ // Move all remaining originalTag associations to newTag
prisma.tagsOnResponses.updateMany({
- where: {
- tagId: originalTagId,
- },
- data: {
- tagId: newTagId,
- },
- }),
-
- prisma.tag.delete({
- where: {
- id: originalTagId,
- },
+ where: { tagId: originalTagId },
+ data: { tagId: newTagId },
}),
+ // Delete the original tag
+ prisma.tag.delete({ where: { id: originalTagId } }),
]);
return ok(newTag);
diff --git a/apps/web/modules/projects/settings/look/components/theme-styling.tsx b/apps/web/modules/projects/settings/look/components/theme-styling.tsx
index 9bcea700a9..717b7e73f3 100644
--- a/apps/web/modules/projects/settings/look/components/theme-styling.tsx
+++ b/apps/web/modules/projects/settings/look/components/theme-styling.tsx
@@ -203,7 +203,9 @@ export const ThemeStyling = ({
field.onChange(color)}
containerClass="w-full"
/>
@@ -214,8 +216,8 @@ export const ThemeStyling = ({
@@ -159,7 +159,7 @@ export const ProjectLookSettingsLoading = () => {
title="Formbricks Signature"
description="We love your support but understand if you toggle it off.">
-
+
diff --git a/apps/web/modules/projects/settings/tags/components/merge-tags-combobox.tsx b/apps/web/modules/projects/settings/tags/components/merge-tags-combobox.tsx
index 97501c08f3..010a3420d8 100644
--- a/apps/web/modules/projects/settings/tags/components/merge-tags-combobox.tsx
+++ b/apps/web/modules/projects/settings/tags/components/merge-tags-combobox.tsx
@@ -34,7 +34,7 @@ export const MergeTagsCombobox = ({ tags, onSelect }: MergeTagsComboboxProps) =>
+ className="font-medium text-slate-900 focus:border-transparent focus:shadow-transparent focus:outline-transparent focus:ring-0 focus:ring-transparent">
{t("environments.workspace.tags.merge")}
@@ -43,7 +43,7 @@ export const MergeTagsCombobox = ({ tags, onSelect }: MergeTagsComboboxProps) =>
diff --git a/apps/web/modules/projects/settings/tags/components/single-tag.tsx b/apps/web/modules/projects/settings/tags/components/single-tag.tsx
index cb7f2dc2a9..77196e4f28 100644
--- a/apps/web/modules/projects/settings/tags/components/single-tag.tsx
+++ b/apps/web/modules/projects/settings/tags/components/single-tag.tsx
@@ -125,12 +125,12 @@ export const SingleTag: React.FC = ({
-
+
{tagCountLoading ?
:
{tagCount}
}
{!isReadOnly && (
-
+
{isMergingTags ? (
@@ -152,7 +152,7 @@ export const SingleTag: React.FC
= ({
setOpenDeleteTagDialog(true)}>
{t("common.delete")}
diff --git a/apps/web/modules/projects/settings/types/tag.ts b/apps/web/modules/projects/settings/types/tag.ts
index 9dfdc5baaf..6ea1b10afa 100644
--- a/apps/web/modules/projects/settings/types/tag.ts
+++ b/apps/web/modules/projects/settings/types/tag.ts
@@ -1,5 +1,6 @@
export enum TagError {
TAG_NOT_FOUND = "tag_not_found",
TAG_NAME_ALREADY_EXISTS = "tag_name_already_exists",
+ MERGE_SAME_TAG = "merge_same_tag",
UNEXPECTED_ERROR = "unexpected_error",
}
diff --git a/apps/web/modules/survey/components/element-form-input/index.tsx b/apps/web/modules/survey/components/element-form-input/index.tsx
index 6498c487f7..ce2aecb598 100644
--- a/apps/web/modules/survey/components/element-form-input/index.tsx
+++ b/apps/web/modules/survey/components/element-form-input/index.tsx
@@ -391,7 +391,7 @@ export const ElementFormInput = ({
return (
{label && (
-
+
{id === "headline" && currentElement && updateElement && (
@@ -521,7 +521,7 @@ export const ElementFormInput = ({
return (
{label && (
-
+
)}
@@ -568,7 +568,7 @@ export const ElementFormInput = ({
1 ? "pr-24" : ""
}`}
dir="auto"
diff --git a/apps/web/modules/survey/components/template-list/components/start-from-scratch-template.tsx b/apps/web/modules/survey/components/template-list/components/start-from-scratch-template.tsx
index bc8fce54ee..9bf0cbe51a 100644
--- a/apps/web/modules/survey/components/template-list/components/start-from-scratch-template.tsx
+++ b/apps/web/modules/survey/components/template-list/components/start-from-scratch-template.tsx
@@ -51,7 +51,7 @@ export const StartFromScratchTemplate = ({
const cardContent = (
<>
-
+
{customSurvey.name}
{customSurvey.description}
{showCreateSurveyButton && (
diff --git a/apps/web/modules/survey/editor/components/add-action-modal.tsx b/apps/web/modules/survey/editor/components/add-action-modal.tsx
index f88af0a333..b174160e07 100644
--- a/apps/web/modules/survey/editor/components/add-action-modal.tsx
+++ b/apps/web/modules/survey/editor/components/add-action-modal.tsx
@@ -93,7 +93,7 @@ export const AddActionModal = ({
key={tab.title}
className={`mr-4 px-1 pb-3 focus:outline-none ${
activeTab === index
- ? "border-brand-dark border-b-2 font-semibold text-slate-900"
+ ? "border-b-2 border-brand-dark font-semibold text-slate-900"
: "text-slate-500 hover:text-slate-700"
}`}
onClick={() => handleTabClick(index)}>
diff --git a/apps/web/modules/survey/editor/components/add-element-button.tsx b/apps/web/modules/survey/editor/components/add-element-button.tsx
index b6f929e399..7af00f440d 100644
--- a/apps/web/modules/survey/editor/components/add-element-button.tsx
+++ b/apps/web/modules/survey/editor/components/add-element-button.tsx
@@ -38,7 +38,7 @@ export const AddElementButton = ({ addElement, project, isCxMode }: AddElementBu
)}>
-
+
@@ -67,7 +67,7 @@ export const AddElementButton = ({ addElement, project, isCxMode }: AddElementBu
onMouseEnter={() => setHoveredElementId(elementType.id)}
onMouseLeave={() => setHoveredElementId(null)}>
-
+
{elementType.label}
diff --git a/apps/web/modules/survey/editor/components/bulk-edit-options-modal.tsx b/apps/web/modules/survey/editor/components/bulk-edit-options-modal.tsx
index 1625167910..832579e0b0 100644
--- a/apps/web/modules/survey/editor/components/bulk-edit-options-modal.tsx
+++ b/apps/web/modules/survey/editor/components/bulk-edit-options-modal.tsx
@@ -177,7 +177,7 @@ export const BulkEditOptionsModal = ({
}
}}
rows={15}
- className="focus:border-brand w-full rounded-md border border-slate-300 bg-white p-3 font-mono text-sm focus:outline-none"
+ className="w-full rounded-md border border-slate-300 bg-white p-3 font-mono text-sm focus:border-brand focus:outline-none"
placeholder={t("environments.surveys.edit.bulk_edit_description")}
/>
{validationError &&
{validationError}
}
diff --git a/apps/web/modules/survey/editor/components/cta-element-form.tsx b/apps/web/modules/survey/editor/components/cta-element-form.tsx
index 6efd8be4f5..b4269e07e4 100644
--- a/apps/web/modules/survey/editor/components/cta-element-form.tsx
+++ b/apps/web/modules/survey/editor/components/cta-element-form.tsx
@@ -111,7 +111,7 @@ export const CTAElementForm = ({
description={t("environments.surveys.edit.button_external_description")}
childBorder
customContainerClass="p-0 mt-4">
-
+
{/* The highlight container is absolutely positioned behind the input */}
{highlightedJSX}
diff --git a/apps/web/modules/survey/editor/components/file-upload-element-form.tsx b/apps/web/modules/survey/editor/components/file-upload-element-form.tsx
index 55c6016d9b..705a613430 100644
--- a/apps/web/modules/survey/editor/components/file-upload-element-form.tsx
+++ b/apps/web/modules/survey/editor/components/file-upload-element-form.tsx
@@ -172,7 +172,7 @@ export const FileUploadElementForm = ({
updateElement(elementIdx, { maxSizeInMB: Number.parseInt(e.target.value, 10) });
}}
- className="mr-2 ml-2 inline w-20 bg-white text-center text-sm"
+ className="ml-2 mr-2 inline w-20 bg-white text-center text-sm"
/>
MB
diff --git a/apps/web/modules/survey/editor/components/form-styling-settings.tsx b/apps/web/modules/survey/editor/components/form-styling-settings.tsx
index 7e4577d101..0a73ed4d89 100644
--- a/apps/web/modules/survey/editor/components/form-styling-settings.tsx
+++ b/apps/web/modules/survey/editor/components/form-styling-settings.tsx
@@ -67,7 +67,7 @@ export const FormStylingSettings = ({
- {t("environments.surveys.edit.form_styling")}
+ {t("environments.surveys.edit.survey_styling")}
{t("environments.surveys.edit.style_the_question_texts_descriptions_and_input_fields")}
@@ -125,6 +125,9 @@ export const FormStylingSettings = ({
description={t(
"environments.workspace.look.advanced_styling_field_headline_weight_description"
)}
+ step={100}
+ min={100}
+ max={900}
/>
@@ -292,6 +301,9 @@ export const FormStylingSettings = ({
description={t(
"environments.workspace.look.advanced_styling_field_button_font_weight_description"
)}
+ step={100}
+ min={100}
+ max={900}
/>
+
@@ -191,7 +191,7 @@ export const HiddenFieldsCard = ({
);
})
) : (
-
+
{t("environments.surveys.edit.no_hidden_fields_yet_add_first_one_below")}
)}
diff --git a/apps/web/modules/survey/editor/components/how-to-send-card.tsx b/apps/web/modules/survey/editor/components/how-to-send-card.tsx
index a35aa9cb7f..0450caa3f9 100644
--- a/apps/web/modules/survey/editor/components/how-to-send-card.tsx
+++ b/apps/web/modules/survey/editor/components/how-to-send-card.tsx
@@ -106,7 +106,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
className="h-full w-full cursor-pointer"
id="howToSendCardTrigger">
-
+
diff --git a/apps/web/modules/survey/editor/components/logo-settings-card.tsx b/apps/web/modules/survey/editor/components/logo-settings-card.tsx
index 2a8b680c1a..19fdc31f6e 100644
--- a/apps/web/modules/survey/editor/components/logo-settings-card.tsx
+++ b/apps/web/modules/survey/editor/components/logo-settings-card.tsx
@@ -124,7 +124,7 @@ export const LogoSettingsCard = ({
disabled && "cursor-not-allowed opacity-60 hover:bg-white"
)}>
-
+
{option.name}
@@ -273,7 +273,7 @@ export const RecontactOptionsCard = ({ localSurvey, setLocalSurvey }: RecontactO
{option.name}
diff --git a/apps/web/modules/survey/editor/components/redirect-url-form.tsx b/apps/web/modules/survey/editor/components/redirect-url-form.tsx
index e140d22123..968d4d0022 100644
--- a/apps/web/modules/survey/editor/components/redirect-url-form.tsx
+++ b/apps/web/modules/survey/editor/components/redirect-url-form.tsx
@@ -45,7 +45,7 @@ export const RedirectUrlForm = ({ localSurvey, endingCard, updateSurvey }: Redir
{/* The highlight container is absolutely positioned behind the input */}
{highlightedJSX}
diff --git a/apps/web/modules/survey/editor/components/response-options-card.tsx b/apps/web/modules/survey/editor/components/response-options-card.tsx
index 0ea5d0f948..63a4504a3b 100644
--- a/apps/web/modules/survey/editor/components/response-options-card.tsx
+++ b/apps/web/modules/survey/editor/components/response-options-card.tsx
@@ -205,7 +205,7 @@ export const ResponseOptionsCard = ({
)}>
-
+
{t("environments.surveys.edit.completed_responses")}
@@ -310,7 +310,7 @@ export const ResponseOptionsCard = ({
handleClosedSurveyMessageChange({ heading: e.target.value })}
diff --git a/apps/web/modules/survey/editor/components/styling-view.tsx b/apps/web/modules/survey/editor/components/styling-view.tsx
index 03ab831657..8e83b79294 100644
--- a/apps/web/modules/survey/editor/components/styling-view.tsx
+++ b/apps/web/modules/survey/editor/components/styling-view.tsx
@@ -9,7 +9,12 @@ import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { TProjectStyling } from "@formbricks/types/project";
import { TSurvey, TSurveyStyling } from "@formbricks/types/surveys/types";
-import { STYLE_DEFAULTS, deriveNewFieldsFromLegacy, getSuggestedColors } from "@/lib/styling/constants";
+import {
+ COLOR_DEFAULTS,
+ STYLE_DEFAULTS,
+ deriveNewFieldsFromLegacy,
+ getSuggestedColors,
+} from "@/lib/styling/constants";
import { FormStylingSettings } from "@/modules/survey/editor/components/form-styling-settings";
import { LogoSettingsCard } from "@/modules/survey/editor/components/logo-settings-card";
import { AlertDialog } from "@/modules/ui/components/alert-dialog";
@@ -92,7 +97,8 @@ export const StylingView = ({
const [confirmSuggestColorsOpen, setConfirmSuggestColorsOpen] = useState(false);
const handleSuggestColors = () => {
- const currentBrandColor = form.getValues().brandColor?.light ?? STYLE_DEFAULTS.brandColor?.light;
+ const currentBrandColor =
+ form.getValues().brandColor?.light ?? STYLE_DEFAULTS.brandColor?.light ?? COLOR_DEFAULTS.brandColor;
const suggested = getSuggestedColors(currentBrandColor);
for (const [key, value] of Object.entries(suggested)) {
@@ -235,7 +241,7 @@ export const StylingView = ({
field.onChange(color)}
containerClass="w-full"
/>
@@ -245,8 +251,8 @@ export const StylingView = ({
/>
setConfirmSuggestColorsOpen(true)}>
{t("environments.workspace.look.suggest_colors")}
diff --git a/apps/web/modules/survey/editor/components/survey-menu-bar.tsx b/apps/web/modules/survey/editor/components/survey-menu-bar.tsx
index 9333f351c4..df8dbf6a26 100644
--- a/apps/web/modules/survey/editor/components/survey-menu-bar.tsx
+++ b/apps/web/modules/survey/editor/components/survey-menu-bar.tsx
@@ -475,7 +475,7 @@ export const SurveyMenuBar = ({
/>
-
+
{!isStorageConfigured && (
diff --git a/apps/web/modules/survey/editor/lib/survey.ts b/apps/web/modules/survey/editor/lib/survey.ts
index 438fe233ea..743aaa563e 100644
--- a/apps/web/modules/survey/editor/lib/survey.ts
+++ b/apps/web/modules/survey/editor/lib/survey.ts
@@ -222,6 +222,7 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise =>
newFollowUps.length > 0
? {
data: newFollowUps.map((followUp) => ({
+ id: followUp.id,
name: followUp.name,
trigger: followUp.trigger,
action: followUp.action,
diff --git a/apps/web/modules/survey/follow-ups/components/follow-up-item.tsx b/apps/web/modules/survey/follow-ups/components/follow-up-item.tsx
index 68f495e26f..246576eea1 100644
--- a/apps/web/modules/survey/follow-ups/components/follow-up-item.tsx
+++ b/apps/web/modules/survey/follow-ups/components/follow-up-item.tsx
@@ -155,7 +155,7 @@ export const FollowUpItem = ({
-
+
{isPreview && (
-
+
Survey Preview 👀
diff --git a/apps/web/modules/survey/link/components/verify-email.tsx b/apps/web/modules/survey/link/components/verify-email.tsx
index 63dc953e7b..ff18a3a60d 100644
--- a/apps/web/modules/survey/link/components/verify-email.tsx
+++ b/apps/web/modules/survey/link/components/verify-email.tsx
@@ -188,7 +188,7 @@ export const VerifyEmail = ({
{!emailSent && showPreviewQuestions && (
{t("s.question_preview")}
-
+
{questions.map((question, index) => (
{
}));
// Re-import the function to use the updated mock
- const { getBasicSurveyMetadata: getBasicSurveyMetadataWithCloudMock } = await import(
- "./metadata-utils"
- );
+ const { getBasicSurveyMetadata: getBasicSurveyMetadataWithCloudMock } =
+ await import("./metadata-utils");
const mockSurvey = {
id: mockSurveyId,
diff --git a/apps/web/modules/survey/list/components/copy-survey-form.tsx b/apps/web/modules/survey/list/components/copy-survey-form.tsx
index 13cbc5f6e6..b371590348 100644
--- a/apps/web/modules/survey/list/components/copy-survey-form.tsx
+++ b/apps/web/modules/survey/list/components/copy-survey-form.tsx
@@ -128,10 +128,6 @@ export const CopySurveyForm = ({ defaultProjects, survey, onCancel, setOpen }: C
: project?.environments[1];
return {
- operation: copySurveyToOtherEnvironmentAction({
- surveyId: survey.id,
- targetEnvironmentId: environmentId,
- }),
projectName: project?.name ?? "Unknown Project",
environmentType: environment?.type ?? "unknown",
environmentId,
@@ -139,7 +135,14 @@ export const CopySurveyForm = ({ defaultProjects, survey, onCancel, setOpen }: C
});
});
- const results = await Promise.all(copyOperationsWithMetadata.map((item) => item.operation));
+ const results: Awaited>[] = [];
+ for (const item of copyOperationsWithMetadata) {
+ const result = await copySurveyToOtherEnvironmentAction({
+ surveyId: survey.id,
+ targetEnvironmentId: item.environmentId,
+ });
+ results.push(result);
+ }
let successCount = 0;
let errorCount = 0;
diff --git a/apps/web/modules/survey/list/components/sort-option.tsx b/apps/web/modules/survey/list/components/sort-option.tsx
index e6fb896e76..d11d95c190 100644
--- a/apps/web/modules/survey/list/components/sort-option.tsx
+++ b/apps/web/modules/survey/list/components/sort-option.tsx
@@ -19,7 +19,7 @@ export const SortOption = ({ option, sortBy, handleSortChange }: SortOptionProps
}}>
+ className={`h-4 w-4 rounded-full border ${sortBy === option.value ? "border-slate-900 bg-brand-dark outline outline-brand-dark" : "border-white"}`}>
{option.label}
diff --git a/apps/web/modules/survey/list/components/survey-dropdown-menu.tsx b/apps/web/modules/survey/list/components/survey-dropdown-menu.tsx
index 614546e380..2d8fef2ecb 100644
--- a/apps/web/modules/survey/list/components/survey-dropdown-menu.tsx
+++ b/apps/web/modules/survey/list/components/survey-dropdown-menu.tsx
@@ -246,7 +246,7 @@ export const SurveyDropDownMenu = ({
{!isSurveyCreationDeletionDisabled && (
handleDeleteSurvey(survey.id)}
diff --git a/apps/web/modules/survey/list/components/survey-filter-dropdown.tsx b/apps/web/modules/survey/list/components/survey-filter-dropdown.tsx
index ffbb263bfd..500ba1299d 100644
--- a/apps/web/modules/survey/list/components/survey-filter-dropdown.tsx
+++ b/apps/web/modules/survey/list/components/survey-filter-dropdown.tsx
@@ -52,7 +52,7 @@ export const SurveyFilterDropdown = ({
diff --git a/apps/web/modules/survey/templates/components/template-container.tsx b/apps/web/modules/survey/templates/components/template-container.tsx
index 42cbdd63c3..352bfdc7fe 100644
--- a/apps/web/modules/survey/templates/components/template-container.tsx
+++ b/apps/web/modules/survey/templates/components/template-container.tsx
@@ -39,7 +39,7 @@ export const TemplateContainerWithPreview = ({
{isTemplatePage && }
-
+
{isTemplatePage
? t("environments.surveys.templates.create_a_new_survey")
diff --git a/apps/web/modules/ui/components/button/index.tsx b/apps/web/modules/ui/components/button/index.tsx
index beaa0fb43f..72d29d2cf0 100644
--- a/apps/web/modules/ui/components/button/index.tsx
+++ b/apps/web/modules/ui/components/button/index.tsx
@@ -36,8 +36,7 @@ const buttonVariants = cva(
);
export interface ButtonProps
- extends React.ButtonHTMLAttributes,
- VariantProps {
+ extends React.ButtonHTMLAttributes, VariantProps {
asChild?: boolean;
loading?: boolean;
}
diff --git a/apps/web/modules/ui/components/card-styling-settings/index.tsx b/apps/web/modules/ui/components/card-styling-settings/index.tsx
index 6a87b19e88..efbdfdfd94 100644
--- a/apps/web/modules/ui/components/card-styling-settings/index.tsx
+++ b/apps/web/modules/ui/components/card-styling-settings/index.tsx
@@ -133,58 +133,6 @@ export const CardStylingSettings = ({
)}
/>
- {(!surveyType || isAppSurvey) && (
-
-
-
(
-
-
-
- {
- if (!checked) {
- field.onChange(null);
- return;
- }
-
- field.onChange({
- light: STYLE_DEFAULTS.highlightBorderColor?.light,
- });
- }}
- />
-
-
-
- {t("environments.surveys.edit.add_highlight_border")}
-
-
-
- {!!field.value && (
-
-
- field.onChange({
- ...field.value,
- light: color,
- })
- }
- containerClass="my-0"
- />
-
- )}
-
- )}
- />
-
-
- )}
-
- {/* Progress Bar Section (Moved from Advanced) */}
+ {/* Highlight Border Section */}
+
+
+
+
(
+
+
+
+ {
+ if (!checked) {
+ field.onChange(null);
+ return;
+ }
+ field.onChange({
+ light: STYLE_DEFAULTS.highlightBorderColor?.light,
+ });
+ }}
+ />
+
+
+
+ {t("environments.surveys.edit.add_highlight_border")}
+
+
+ {t("environments.surveys.edit.add_highlight_border_description")}
+
+
+
+
+ {!!field.value && (
+
+
+ field.onChange({
+ ...field.value,
+ light: color,
+ })
+ }
+ containerClass="w-1/2"
+ />
+
+ )}
+
+ )}
+ />
+
+
+
+ {/* Progress Bar Section */}
diff --git a/apps/web/modules/ui/components/client-logo/index.tsx b/apps/web/modules/ui/components/client-logo/index.tsx
index b78c513658..35134abcb4 100644
--- a/apps/web/modules/ui/components/client-logo/index.tsx
+++ b/apps/web/modules/ui/components/client-logo/index.tsx
@@ -46,7 +46,7 @@ export const ClientLogo = ({
target="_blank">
)}
@@ -69,7 +69,7 @@ export const ClientLogo = ({
e.preventDefault();
}
}}
- className="rounded-md border border-dashed border-slate-400 bg-slate-200 px-6 py-3 text-xs whitespace-nowrap text-slate-900 opacity-50 backdrop-blur-sm hover:cursor-pointer hover:border-slate-600"
+ className="whitespace-nowrap rounded-md border border-dashed border-slate-400 bg-slate-200 px-6 py-3 text-xs text-slate-900 opacity-50 backdrop-blur-sm hover:cursor-pointer hover:border-slate-600"
target="_blank">
{t("common.add_logo")}
diff --git a/apps/web/modules/ui/components/color-picker/components/popover-picker.tsx b/apps/web/modules/ui/components/color-picker/components/popover-picker.tsx
index 2c7fbae031..71ce0b6f6e 100644
--- a/apps/web/modules/ui/components/color-picker/components/popover-picker.tsx
+++ b/apps/web/modules/ui/components/color-picker/components/popover-picker.tsx
@@ -1,6 +1,6 @@
-import { useCallback, useRef, useState } from "react";
+import { useState } from "react";
import { HexColorPicker } from "react-colorful";
-import { useClickOutside } from "@/lib/utils/hooks/useClickOutside";
+import { Popover, PopoverContent, PopoverTrigger } from "@/modules/ui/components/popover";
interface PopoverPickerProps {
color: string;
@@ -9,33 +9,27 @@ interface PopoverPickerProps {
}
export const PopoverPicker = ({ color, onChange, disabled = false }: PopoverPickerProps) => {
- const popover = useRef(null);
- const [isOpen, toggle] = useState(false);
-
- const close = useCallback(() => toggle(false), []);
- useClickOutside(popover, close);
+ const [isOpen, setIsOpen] = useState(false);
return (
-
-
{
- e.preventDefault();
- if (!disabled) {
- toggle(!isOpen);
- }
- }}
- />
-
- {isOpen && (
-
- )}
-
+
+
+ {
+ e.preventDefault();
+ if (!disabled) {
+ setIsOpen((prev) => !prev);
+ }
+ }}
+ />
+
+
+
+
+
);
};
diff --git a/apps/web/modules/ui/components/color-picker/index.tsx b/apps/web/modules/ui/components/color-picker/index.tsx
index 6d00ac001a..7dcaafac2e 100644
--- a/apps/web/modules/ui/components/color-picker/index.tsx
+++ b/apps/web/modules/ui/components/color-picker/index.tsx
@@ -13,18 +13,16 @@ interface ColorPickerProps {
export const ColorPicker = ({ color, onChange, containerClass, disabled = false }: ColorPickerProps) => {
return (
-
diff --git a/apps/web/modules/ui/components/command/index.tsx b/apps/web/modules/ui/components/command/index.tsx
index 53080c68c1..188b8630b2 100644
--- a/apps/web/modules/ui/components/command/index.tsx
+++ b/apps/web/modules/ui/components/command/index.tsx
@@ -115,7 +115,7 @@ function CommandItem({ className, ...props }: React.ComponentProps
{quotaError && isSubmitted && (
-
{quotaError}
+
{quotaError}
)}
);
diff --git a/apps/web/modules/ui/components/data-table/components/data-table-header.tsx b/apps/web/modules/ui/components/data-table/components/data-table-header.tsx
index 94463c7be2..7a8af483c4 100644
--- a/apps/web/modules/ui/components/data-table/components/data-table-header.tsx
+++ b/apps/web/modules/ui/components/data-table/components/data-table-header.tsx
@@ -68,7 +68,7 @@ export const DataTableHeader =
({
onTouchStart={header.getResizeHandler()}
data-testid="column-resize-handle"
className={cn(
- "absolute top-0 right-0 hidden h-full w-1 cursor-col-resize bg-slate-500",
+ "absolute right-0 top-0 hidden h-full w-1 cursor-col-resize bg-slate-500",
header.column.getIsResizing() ? "bg-black" : "bg-slate-500",
!header.column.getCanResize() ? "hidden" : "group-hover:block"
)}>
diff --git a/apps/web/modules/ui/components/data-table/components/selected-row-settings.tsx b/apps/web/modules/ui/components/data-table/components/selected-row-settings.tsx
index a60537110b..fb8279150b 100644
--- a/apps/web/modules/ui/components/data-table/components/selected-row-settings.tsx
+++ b/apps/web/modules/ui/components/data-table/components/selected-row-settings.tsx
@@ -66,10 +66,14 @@ export const SelectedRowSettings = ({
setIsDeleting(true);
const rowsToBeDeleted = table.getFilteredSelectedRowModel().rows.map((row) => row.id);
- if (type === "response") {
- await Promise.all(rowsToBeDeleted.map((rowId) => deleteAction(rowId, { decrementQuotas })));
- } else {
- await Promise.all(rowsToBeDeleted.map((rowId) => deleteAction(rowId)));
+ const CHUNK_SIZE = 5;
+ for (let i = 0; i < rowsToBeDeleted.length; i += CHUNK_SIZE) {
+ const chunk = rowsToBeDeleted.slice(i, i + CHUNK_SIZE);
+ if (type === "response") {
+ await Promise.all(chunk.map((rowId) => deleteAction(rowId, { decrementQuotas })));
+ } else {
+ await Promise.all(chunk.map((rowId) => deleteAction(rowId)));
+ }
}
// Update the row list UI
@@ -141,7 +145,7 @@ export const SelectedRowSettings = ({
return (
<>
-
+
{`${selectedRowCount} ${selectedTypeLabel} ${t("common.selected")}`}
-
{`${t("common.delete")} ${deleteWhat}`}
+
{t("common.delete_what", { deleteWhat })}
{t("environments.workspace.general.this_action_cannot_be_undone")}
diff --git a/apps/web/modules/ui/components/dialog/index.tsx b/apps/web/modules/ui/components/dialog/index.tsx
index 5e904c8d18..b039f8af24 100644
--- a/apps/web/modules/ui/components/dialog/index.tsx
+++ b/apps/web/modules/ui/components/dialog/index.tsx
@@ -83,7 +83,7 @@ const DialogContent = React.forwardRef<
{...props}>
{children}
{!hideCloseButton && (
-
+
Close
@@ -105,7 +105,7 @@ const DialogHeader = ({ className, ...props }: DialogHeaderProps) => (
svg]:text-primary [&>svg]:absolute [&>svg]:size-4 [&>svg~*]:min-h-4 [&>svg~*]:items-center [&>svg~*]:pl-6 sm:[&>svg~*]:flex",
+ "[&>svg]:absolute [&>svg]:size-4 [&>svg]:text-primary [&>svg~*]:min-h-4 [&>svg~*]:items-center [&>svg~*]:pl-6 sm:[&>svg~*]:flex",
className
)}
{...props}
@@ -150,7 +150,7 @@ const DialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
));
diff --git a/apps/web/modules/ui/components/editor/components/link-editor.tsx b/apps/web/modules/ui/components/editor/components/link-editor.tsx
index c7500f27d3..578c1f9a05 100644
--- a/apps/web/modules/ui/components/editor/components/link-editor.tsx
+++ b/apps/web/modules/ui/components/editor/components/link-editor.tsx
@@ -123,7 +123,7 @@ const LinkEditorContent = ({ editor, open, setOpen }: LinkEditorProps) => {
+
{body}
);
diff --git a/apps/web/modules/ui/components/input-combo-box/index.tsx b/apps/web/modules/ui/components/input-combo-box/index.tsx
index 957c0246de..84733d5f59 100644
--- a/apps/web/modules/ui/components/input-combo-box/index.tsx
+++ b/apps/web/modules/ui/components/input-combo-box/index.tsx
@@ -226,7 +226,7 @@ export const InputCombobox: React.FC
= ({
tabIndex={0}
aria-controls="options"
aria-expanded={open}
- className={cn("flex w-full cursor-pointer items-center justify-end bg-white pr-2 h-10", {
+ className={cn("flex h-10 w-full cursor-pointer items-center justify-end bg-white pr-2", {
"w-10 justify-center pr-0": withInput && inputType !== "dropdown",
"pointer-events-none": isClearing,
})}>
diff --git a/apps/web/modules/ui/components/input/index.tsx b/apps/web/modules/ui/components/input/index.tsx
index 28e43e6688..bcbda90b70 100644
--- a/apps/web/modules/ui/components/input/index.tsx
+++ b/apps/web/modules/ui/components/input/index.tsx
@@ -1,8 +1,10 @@
import * as React from "react";
import { cn } from "@/lib/cn";
-export interface InputProps
- extends Omit, "crossOrigin" | "dangerouslySetInnerHTML"> {
+export interface InputProps extends Omit<
+ React.InputHTMLAttributes,
+ "crossOrigin" | "dangerouslySetInnerHTML"
+> {
crossOrigin?: "" | "anonymous" | "use-credentials" | undefined;
dangerouslySetInnerHTML?: {
__html: string;
@@ -14,7 +16,7 @@ const Input = React.forwardRef(({ className, isInv
return (
= ({
{connected === true ? (
-
+
) : (
diff --git a/apps/web/modules/ui/components/limits-reached-banner/index.tsx b/apps/web/modules/ui/components/limits-reached-banner/index.tsx
index 5d5c7356f3..d4d7d8096e 100644
--- a/apps/web/modules/ui/components/limits-reached-banner/index.tsx
+++ b/apps/web/modules/ui/components/limits-reached-banner/index.tsx
@@ -39,7 +39,7 @@ export const LimitsReachedBanner = ({
-
+
{t("common.limits_reached")}
diff --git a/apps/web/modules/ui/components/modal-with-tabs/index.tsx b/apps/web/modules/ui/components/modal-with-tabs/index.tsx
index 5bc12067c5..803f304ee5 100644
--- a/apps/web/modules/ui/components/modal-with-tabs/index.tsx
+++ b/apps/web/modules/ui/components/modal-with-tabs/index.tsx
@@ -59,7 +59,7 @@ export const ModalWithTabs = ({
key={index}
className={`mr-4 px-1 pb-3 focus:outline-none ${
activeTab === index
- ? "border-brand-dark border-b-2 font-semibold text-slate-900"
+ ? "border-b-2 border-brand-dark font-semibold text-slate-900"
: "text-slate-500 hover:text-slate-700"
}`}
onClick={() => handleTabClick(index)}>
diff --git a/apps/web/modules/ui/components/multi-select/badge.tsx b/apps/web/modules/ui/components/multi-select/badge.tsx
index 02149b51de..f49e9f8630 100644
--- a/apps/web/modules/ui/components/multi-select/badge.tsx
+++ b/apps/web/modules/ui/components/multi-select/badge.tsx
@@ -20,8 +20,7 @@ const badgeVariants = cva(
);
export interface BadgeProps
- extends React.HTMLAttributes
,
- VariantProps {}
+ extends React.HTMLAttributes, VariantProps {}
function Badge({ className, variant, ...props }: BadgeProps) {
return ;
diff --git a/apps/web/modules/ui/components/pending-downgrade-banner/index.tsx b/apps/web/modules/ui/components/pending-downgrade-banner/index.tsx
index 6aa49239d1..94baf1dcc3 100644
--- a/apps/web/modules/ui/components/pending-downgrade-banner/index.tsx
+++ b/apps/web/modules/ui/components/pending-downgrade-banner/index.tsx
@@ -76,7 +76,7 @@ export const PendingDowngradeBanner = ({
-
+
diff --git a/apps/web/modules/ui/components/progress-bar/index.tsx b/apps/web/modules/ui/components/progress-bar/index.tsx
index a16e95d794..067903c40e 100644
--- a/apps/web/modules/ui/components/progress-bar/index.tsx
+++ b/apps/web/modules/ui/components/progress-bar/index.tsx
@@ -44,7 +44,7 @@ export const HalfCircle: React.FC = ({ value }: { value: number
diff --git a/apps/web/modules/ui/components/select/index.tsx b/apps/web/modules/ui/components/select/index.tsx
index 8cc3e44c33..c7ddbac0b6 100644
--- a/apps/web/modules/ui/components/select/index.tsx
+++ b/apps/web/modules/ui/components/select/index.tsx
@@ -18,7 +18,7 @@ const SelectTrigger = React.forwardRef<
@@ -62,7 +62,7 @@ const SelectLabel: React.ComponentType = React
>(({ className, ...props }, ref) => (
));
@@ -75,7 +75,7 @@ const SelectItem: React.ComponentType = React.f
diff --git a/apps/web/modules/ui/components/sheet/index.tsx b/apps/web/modules/ui/components/sheet/index.tsx
index 8de3e49a67..ed99d3e61d 100644
--- a/apps/web/modules/ui/components/sheet/index.tsx
+++ b/apps/web/modules/ui/components/sheet/index.tsx
@@ -49,15 +49,14 @@ const sheetVariants = cva(
);
interface SheetContentProps
- extends React.ComponentPropsWithoutRef,
- VariantProps {}
+ extends React.ComponentPropsWithoutRef, VariantProps {}
const SheetContent = React.forwardRef, SheetContentProps>(
({ side = "right", className, children, ...props }, ref) => (
-
+
Close
diff --git a/apps/web/modules/ui/components/slider/index.tsx b/apps/web/modules/ui/components/slider/index.tsx
index 28faf7ebb0..3a5a921e80 100644
--- a/apps/web/modules/ui/components/slider/index.tsx
+++ b/apps/web/modules/ui/components/slider/index.tsx
@@ -15,7 +15,7 @@ export const Slider: React.ForwardRefExoticComponent<
-
+
));
Slider.displayName = SliderPrimitive.Root.displayName;
diff --git a/apps/web/modules/ui/components/styling-fields/components/dimension-input.tsx b/apps/web/modules/ui/components/styling-fields/components/dimension-input.tsx
index 649c647566..747778334c 100644
--- a/apps/web/modules/ui/components/styling-fields/components/dimension-input.tsx
+++ b/apps/web/modules/ui/components/styling-fields/components/dimension-input.tsx
@@ -32,14 +32,15 @@ export const DimensionInput = ({ form, name, label, description, placeholder }:
else if (value.endsWith("rem")) unit = "rem";
else if (value.endsWith("em")) unit = "em";
}
- const numericValue = typeof value === "string" ? Number.parseFloat(value) : value;
+ const parsed = typeof value === "string" ? Number.parseFloat(value) : value;
+ const numericValue = typeof parsed === "number" && Number.isNaN(parsed) ? null : parsed;
return (
{label}
{description && {description}}
-
+
(
diff --git a/apps/web/modules/ui/components/styling-tabs/index.tsx b/apps/web/modules/ui/components/styling-tabs/index.tsx
index a7e3983910..0588e729f9 100644
--- a/apps/web/modules/ui/components/styling-tabs/index.tsx
+++ b/apps/web/modules/ui/components/styling-tabs/index.tsx
@@ -61,7 +61,7 @@ export const StylingTabs =
({
className={cn(
"flex flex-1 cursor-pointer items-center justify-center gap-4 rounded-md py-2 text-center text-sm",
selectedOption === option.value ? "bg-slate-100" : "bg-white",
- "focus:ring-brand-dark focus:ring-opacity-50 focus:ring-2 focus:outline-none",
+ "focus:outline-none focus:ring-2 focus:ring-brand-dark focus:ring-opacity-50",
selectedOption === option.value ? activeTabClassName : inactiveTabClassName
)}>
{status === "inProgress" && (
-
+
)}
@@ -45,7 +45,7 @@ export const SurveyStatusIndicator = ({ status, tooltip }: SurveyStatusIndicator
<>
{t("common.gathering_responses")}
-
+
>
@@ -74,7 +74,7 @@ export const SurveyStatusIndicator = ({ status, tooltip }: SurveyStatusIndicator
{status === "inProgress" && (
-
+
)}
diff --git a/apps/web/modules/ui/components/tab-bar/index.tsx b/apps/web/modules/ui/components/tab-bar/index.tsx
index d8209f8090..f8759a4364 100644
--- a/apps/web/modules/ui/components/tab-bar/index.tsx
+++ b/apps/web/modules/ui/components/tab-bar/index.tsx
@@ -31,7 +31,7 @@ export const TabBar: React.FC = ({
onClick={() => setActiveId(tab.id)}
className={cn(
tab.id === activeId
- ? `border-brand-dark border-b-2 font-semibold text-slate-900 ${activeTabClassName}`
+ ? `border-b-2 border-brand-dark font-semibold text-slate-900 ${activeTabClassName}`
: "text-slate-500 hover:text-slate-700",
"flex h-full items-center px-3 text-sm font-medium"
)}
diff --git a/apps/web/modules/ui/components/tab-nav/index.tsx b/apps/web/modules/ui/components/tab-nav/index.tsx
index 237255ae27..8f8b3b430a 100644
--- a/apps/web/modules/ui/components/tab-nav/index.tsx
+++ b/apps/web/modules/ui/components/tab-nav/index.tsx
@@ -32,7 +32,7 @@ const Nav: React.FC = ({ tabs, activeId, setActiveId, activeTabClassNa
disabled
? "cursor-not-allowed text-slate-400"
: tab.id === activeId
- ? `border-brand-dark text-primary border-b-2 font-semibold ${activeTabClassName}`
+ ? `border-b-2 border-brand-dark font-semibold text-primary ${activeTabClassName}`
: "text-slate-500 hover:text-slate-700"
)}
aria-current={tab.id === activeId ? "page" : undefined}>
diff --git a/apps/web/modules/ui/components/tab-toggle/index.tsx b/apps/web/modules/ui/components/tab-toggle/index.tsx
index a65efe6650..a01f295e37 100644
--- a/apps/web/modules/ui/components/tab-toggle/index.tsx
+++ b/apps/web/modules/ui/components/tab-toggle/index.tsx
@@ -39,7 +39,7 @@ export const TabToggle = ({
className={cn(
"flex-1 cursor-pointer rounded-md py-2 text-center text-sm text-slate-800",
selectedOption === option.value && "bg-white",
- "focus:ring-brand-dark focus:outline-none focus:ring-2 focus:ring-opacity-50",
+ "focus:outline-none focus:ring-2 focus:ring-brand-dark focus:ring-opacity-50",
disabled && "cursor-not-allowed opacity-50"
)}>
,
- VariantProps {}
+ extends React.ComponentProps, VariantProps {}
interface TabsTriggerProps
- extends React.ComponentProps,
- VariantProps {
+ extends React.ComponentProps, VariantProps {
readonly icon?: React.ReactNode;
readonly showIcon?: boolean;
}
diff --git a/apps/web/modules/ui/components/tags-combobox/index.tsx b/apps/web/modules/ui/components/tags-combobox/index.tsx
index 983ac74c68..5d74894814 100644
--- a/apps/web/modules/ui/components/tags-combobox/index.tsx
+++ b/apps/web/modules/ui/components/tags-combobox/index.tsx
@@ -86,7 +86,7 @@ export const TagsCombobox = ({
? t("environments.workspace.tags.add_tag")
: t("environments.workspace.tags.search_tags")
}
- className="border-b border-none border-transparent shadow-none ring-offset-transparent outline-0 focus:border-none focus:border-transparent focus:shadow-none focus:ring-offset-transparent focus:outline-0"
+ className="border-b border-none border-transparent shadow-none outline-0 ring-offset-transparent focus:border-none focus:border-transparent focus:shadow-none focus:outline-0 focus:ring-offset-transparent"
value={searchValue}
onValueChange={(search) => setSearchValue(search)}
onKeyDown={(e) => {
diff --git a/apps/web/modules/ui/components/theme-styling-preview-survey/index.tsx b/apps/web/modules/ui/components/theme-styling-preview-survey/index.tsx
index e2d7a8a45d..49eeedbe62 100644
--- a/apps/web/modules/ui/components/theme-styling-preview-survey/index.tsx
+++ b/apps/web/modules/ui/components/theme-styling-preview-survey/index.tsx
@@ -92,7 +92,7 @@ export const ThemeStylingPreviewSurvey = ({
},
shrink: {
width: ["83.33%"],
- height: ["660px"],
+ height: ["700px"],
},
};
@@ -148,7 +148,6 @@ export const ThemeStylingPreviewSurvey = ({
}
className={cn(
"relative z-10 flex w-5/6 flex-col rounded-lg border border-slate-300 shadow-xl",
- "h-[660px] max-h-[95%]",
isAppSurvey ? "bg-slate-200" : "overflow-y-auto bg-white"
)}>
diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs
index ecbc3081ac..b7a4a85d60 100644
--- a/apps/web/next.config.mjs
+++ b/apps/web/next.config.mjs
@@ -14,7 +14,8 @@ const nextConfig = {
basePath: process.env.BASE_PATH || undefined,
output: "standalone",
poweredByHeader: false,
- productionBrowserSourceMaps: true,
+ // Enable source maps only when uploading to Sentry (CI/production); skip for faster local builds
+ productionBrowserSourceMaps: !!process.env.SENTRY_AUTH_TOKEN,
serverExternalPackages: [
"@aws-sdk",
"@opentelemetry/api",
diff --git a/apps/web/package.json b/apps/web/package.json
index 5852cf4fad..1936eace3c 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -1,7 +1,7 @@
{
"name": "@formbricks/web",
"version": "0.0.0",
- "packageManager": "pnpm@10.28.2",
+ "packageManager": "pnpm@10.30.3",
"private": true,
"scripts": {
"clean": "rimraf .turbo node_modules .next coverage",
@@ -19,9 +19,6 @@
"i18n:generate": "npx lingo.dev@latest run && npx lingo.dev@latest lockfile --force"
},
"dependencies": {
- "@aws-sdk/client-s3": "3.971.0",
- "@aws-sdk/s3-presigned-post": "3.971.0",
- "@aws-sdk/s3-request-presigner": "3.971.0",
"@boxyhq/saml-jackson": "1.52.2",
"@dnd-kit/core": "6.3.1",
"@dnd-kit/modifiers": "9.0.0",
@@ -36,106 +33,98 @@
"@formbricks/storage": "workspace:*",
"@formbricks/surveys": "workspace:*",
"@formbricks/types": "workspace:*",
- "@hookform/resolvers": "5.0.1",
+ "@hookform/resolvers": "5.2.2",
"@json2csv/node": "7.0.6",
- "@lexical/code": "0.36.2",
- "@lexical/link": "0.36.2",
- "@lexical/list": "0.36.2",
- "@lexical/markdown": "0.36.2",
- "@lexical/react": "0.36.2",
- "@lexical/rich-text": "0.36.2",
- "@lexical/table": "0.36.2",
- "@opentelemetry/auto-instrumentations-node": "0.69.0",
- "@opentelemetry/exporter-metrics-otlp-http": "0.211.0",
- "@opentelemetry/exporter-prometheus": "0.211.0",
- "@opentelemetry/exporter-trace-otlp-http": "0.211.0",
- "@opentelemetry/resources": "2.5.0",
- "@opentelemetry/sdk-metrics": "2.5.0",
- "@opentelemetry/sdk-node": "0.211.0",
- "@opentelemetry/sdk-trace-base": "2.5.0",
- "@opentelemetry/semantic-conventions": "1.38.0",
- "@paralleldrive/cuid2": "2.2.2",
- "@prisma/client": "6.14.0",
- "@prisma/instrumentation": "6.14.0",
- "@radix-ui/react-accordion": "1.2.10",
- "@radix-ui/react-checkbox": "1.3.1",
- "@radix-ui/react-collapsible": "1.1.10",
- "@radix-ui/react-dialog": "1.1.13",
- "@radix-ui/react-dropdown-menu": "2.1.14",
- "@radix-ui/react-label": "2.1.6",
- "@radix-ui/react-popover": "1.1.13",
- "@radix-ui/react-radio-group": "1.3.6",
- "@radix-ui/react-select": "2.2.4",
- "@radix-ui/react-separator": "1.1.6",
- "@radix-ui/react-slider": "1.3.4",
- "@radix-ui/react-slot": "1.2.2",
- "@radix-ui/react-switch": "1.2.4",
- "@radix-ui/react-tabs": "1.1.11",
- "@radix-ui/react-toggle": "1.1.8",
- "@radix-ui/react-toggle-group": "1.1.9",
- "@radix-ui/react-tooltip": "1.2.6",
- "@sentry/nextjs": "10.5.0",
- "@t3-oss/env-nextjs": "0.13.4",
- "@tailwindcss/forms": "0.5.10",
- "@tailwindcss/typography": "0.5.16",
+ "@lexical/code": "0.41.0",
+ "@lexical/link": "0.41.0",
+ "@lexical/list": "0.41.0",
+ "@lexical/markdown": "0.41.0",
+ "@lexical/react": "0.41.0",
+ "@lexical/rich-text": "0.41.0",
+ "@lexical/table": "0.41.0",
+ "@opentelemetry/auto-instrumentations-node": "0.70.1",
+ "@opentelemetry/exporter-metrics-otlp-http": "0.212.0",
+ "@opentelemetry/exporter-prometheus": "0.212.0",
+ "@opentelemetry/exporter-trace-otlp-http": "0.212.0",
+ "@opentelemetry/resources": "2.5.1",
+ "@opentelemetry/sdk-metrics": "2.5.1",
+ "@opentelemetry/sdk-node": "0.212.0",
+ "@opentelemetry/sdk-trace-base": "2.5.1",
+ "@opentelemetry/semantic-conventions": "1.40.0",
+ "@paralleldrive/cuid2": "2.3.1",
+ "@prisma/client": "6.19.2",
+ "@prisma/instrumentation": "6.19.2",
+ "@radix-ui/react-checkbox": "1.3.3",
+ "@radix-ui/react-collapsible": "1.1.12",
+ "@radix-ui/react-dialog": "1.1.15",
+ "@radix-ui/react-dropdown-menu": "2.1.16",
+ "@radix-ui/react-label": "2.1.8",
+ "@radix-ui/react-popover": "1.1.15",
+ "@radix-ui/react-radio-group": "1.3.8",
+ "@radix-ui/react-select": "2.2.6",
+ "@radix-ui/react-separator": "1.1.8",
+ "@radix-ui/react-slider": "1.3.6",
+ "@radix-ui/react-slot": "1.2.4",
+ "@radix-ui/react-switch": "1.2.6",
+ "@radix-ui/react-tabs": "1.1.13",
+ "@radix-ui/react-tooltip": "1.2.8",
+ "@sentry/nextjs": "10.42.0",
+ "@t3-oss/env-nextjs": "0.13.10",
+ "@tailwindcss/forms": "0.5.11",
+ "@tailwindcss/typography": "0.5.19",
"@tanstack/react-table": "8.21.3",
"@ungap/structured-clone": "1.3.0",
- "@vercel/functions": "2.2.8",
- "@vercel/og": "0.8.5",
- "bcryptjs": "3.0.2",
- "boring-avatars": "2.0.1",
+ "bcryptjs": "3.0.3",
+ "boring-avatars": "2.0.4",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"cmdk": "1.1.1",
"csv-parse": "5.6.0",
"date-fns": "4.1.0",
"file-loader": "6.2.0",
- "framer-motion": "12.10.0",
+ "framer-motion": "12.34.4",
"googleapis": "148.0.0",
"heic-convert": "2.1.0",
"https-proxy-agent": "7.0.6",
- "i18next": "25.5.2",
- "i18next-icu": "2.4.0",
+ "i18next": "25.8.13",
+ "i18next-icu": "2.4.3",
"i18next-resources-to-backend": "1.2.1",
- "jiti": "2.4.2",
- "jsonwebtoken": "9.0.2",
- "lexical": "0.36.2",
+ "jiti": "2.6.1",
+ "jsonwebtoken": "9.0.3",
+ "lexical": "0.41.0",
"lodash": "4.17.23",
- "lucide-react": "0.507.0",
- "markdown-it": "14.1.0",
- "mime-types": "3.0.1",
+ "lucide-react": "0.576.0",
+ "markdown-it": "14.1.1",
"next": "16.1.6",
- "next-auth": "4.24.12",
+ "next-auth": "4.24.13",
"next-safe-action": "7.10.8",
"node-fetch": "3.3.2",
- "nodemailer": "7.0.11",
+ "nodemailer": "7.0.13",
"otplib": "12.0.1",
- "papaparse": "5.5.2",
+ "papaparse": "5.5.3",
"prismjs": "1.30.0",
"qr-code-styling": "1.9.2",
"qrcode": "1.5.4",
- "react": "19.2.3",
+ "react": "19.2.4",
"react-calendar": "5.1.0",
"react-colorful": "5.6.1",
"react-confetti": "6.4.0",
- "react-day-picker": "9.6.7",
- "react-dom": "19.2.3",
- "react-hook-form": "7.56.2",
- "react-hot-toast": "2.5.2",
- "react-i18next": "15.7.3",
- "react-turnstile": "1.1.4",
+ "react-day-picker": "9.14.0",
+ "react-dom": "19.2.4",
+ "react-hook-form": "7.71.2",
+ "react-hot-toast": "2.6.0",
+ "react-i18next": "15.7.4",
+ "react-turnstile": "1.1.5",
"react-use": "17.6.0",
- "redis": "4.7.0",
- "sanitize-html": "2.17.0",
+ "sanitize-html": "2.17.1",
"server-only": "0.0.1",
- "sharp": "0.34.1",
+ "sharp": "0.34.5",
"stripe": "16.12.0",
- "superjson": "2.2.2",
- "tailwind-merge": "3.2.0",
- "tailwindcss": "3.4.17",
- "ua-parser-js": "2.0.3",
+ "tailwind-merge": "3.5.0",
+ "tailwindcss": "3.4.19",
+ "ua-parser-js": "2.0.9",
"uuid": "11.1.0",
- "webpack": "5.99.8",
+ "webpack": "5.105.3",
"xlsx": "file:vendor/xlsx-0.20.3.tgz",
"zod": "3.25.76",
"zod-openapi": "4.2.4"
@@ -143,31 +132,26 @@
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*",
- "@testing-library/jest-dom": "6.6.3",
- "@testing-library/react": "16.3.0",
- "@types/bcryptjs": "2.4.6",
+ "@testing-library/jest-dom": "6.9.1",
+ "@testing-library/react": "16.3.2",
"@types/heic-convert": "2.1.0",
- "@types/jsonwebtoken": "9.0.9",
- "@types/lodash": "4.17.16",
+ "@types/jsonwebtoken": "9.0.10",
+ "@types/lodash": "4.17.24",
"@types/markdown-it": "14.1.2",
- "@types/mime-types": "2.1.4",
- "@types/nodemailer": "7.0.2",
- "@types/papaparse": "5.3.15",
- "@types/qrcode": "1.5.5",
+ "@types/nodemailer": "7.0.11",
+ "@types/papaparse": "5.5.2",
+ "@types/qrcode": "1.5.6",
"@types/sanitize-html": "2.16.0",
- "@types/testing-library__react": "10.2.0",
"@types/ungap__structured-clone": "1.2.0",
- "@vitest/coverage-v8": "3.1.3",
- "autoprefixer": "10.4.21",
- "cross-env": "10.0.0",
- "dotenv": "16.5.0",
- "esbuild": "0.25.12",
- "postcss": "8.5.3",
+ "@vitest/coverage-v8": "3.2.4",
+ "autoprefixer": "10.4.27",
+ "cross-env": "10.1.0",
+ "dotenv": "16.6.1",
+ "postcss": "8.5.8",
"resize-observer-polyfill": "1.5.1",
- "ts-node": "10.9.2",
"vite": "6.4.1",
"vite-tsconfig-paths": "5.1.4",
- "vitest": "3.1.3",
+ "vitest": "3.2.4",
"vitest-mock-extended": "3.1.0"
}
}
diff --git a/apps/web/playwright/survey-styling.spec.ts b/apps/web/playwright/survey-styling.spec.ts
index 1d0c422171..796d84b104 100644
--- a/apps/web/playwright/survey-styling.spec.ts
+++ b/apps/web/playwright/survey-styling.spec.ts
@@ -50,8 +50,8 @@ test.describe("Survey Styling", async () => {
await addCustomStyles.click();
}
- // --- Form styling ---
- await openAccordion(page, "Form styling");
+ // --- Survey styling ---
+ await openAccordion(page, "Survey styling");
// 1. Headlines & Descriptions
await openAccordion(page, "Headlines & Descriptions");
@@ -132,6 +132,7 @@ test.describe("Survey Styling", async () => {
await openAccordion(page, "Options (Radio/Checkbox)");
await setColor(page, "Background", "dddddd");
await setColor(page, "Label Color", "111111");
+ await setColor(page, "Border Color", "999999");
// Note: Border Radius is reused, but we can set it here to be sure
await setDimension(page, "Border Radius", "6");
await setDimension(page, "Padding X", "12");
@@ -142,6 +143,7 @@ test.describe("Survey Styling", async () => {
css = await page.evaluate(() => document.getElementById("formbricks__css__custom")?.innerHTML);
expect(css).toContain("--fb-option-bg-color: #dddddd");
expect(css).toContain("--fb-option-label-color: #111111");
+ expect(css).toContain("--fb-option-border-color: #999999");
expect(css).toContain("--fb-option-border-radius: 6px");
expect(css).toContain("--fb-option-padding-x: 12px");
expect(css).toContain("--fb-option-padding-y: 8px");
@@ -182,7 +184,7 @@ test.describe("Survey Styling", async () => {
}
// Set some non-color properties BEFORE suggesting colors, so we can verify they aren't overwritten
- await openAccordion(page, "Form styling");
+ await openAccordion(page, "Survey styling");
await openAccordion(page, "Inputs");
await setDimension(page, "Border Radius", "12");
await setDimension(page, "Padding Y", "20");
@@ -341,7 +343,7 @@ test.describe("Survey Styling", async () => {
}
// Apply Overrides
- await openAccordion(page, "Form styling");
+ await openAccordion(page, "Survey styling");
await openAccordion(page, "Headlines & Descriptions");
// Override Headline Color (Blue)
diff --git a/apps/web/playwright/survey.spec.ts b/apps/web/playwright/survey.spec.ts
index 68e553c307..e14e1938a6 100644
--- a/apps/web/playwright/survey.spec.ts
+++ b/apps/web/playwright/survey.spec.ts
@@ -191,16 +191,16 @@ test.describe("Survey Create & Submit Response without logic", async () => {
page.getByRole("rowheader", { name: surveys.createAndSubmit.matrix.rows[2] })
).toBeVisible();
await expect(
- page.getByRole("cell", { name: surveys.createWithLogicAndSubmit.matrix.columns[0], exact: true })
+ page.getByRole("columnheader", { name: surveys.createAndSubmit.matrix.columns[0], exact: true })
).toBeVisible();
await expect(
- page.getByRole("cell", { name: surveys.createWithLogicAndSubmit.matrix.columns[1], exact: true })
+ page.getByRole("columnheader", { name: surveys.createAndSubmit.matrix.columns[1], exact: true })
).toBeVisible();
await expect(
- page.getByRole("cell", { name: surveys.createWithLogicAndSubmit.matrix.columns[2], exact: true })
+ page.getByRole("columnheader", { name: surveys.createAndSubmit.matrix.columns[2], exact: true })
).toBeVisible();
await expect(
- page.getByRole("cell", { name: surveys.createWithLogicAndSubmit.matrix.columns[3], exact: true })
+ page.getByRole("columnheader", { name: surveys.createAndSubmit.matrix.columns[3], exact: true })
).toBeVisible();
await expect(page.locator("#questionCard-9").getByRole("button", { name: "Next" })).toBeVisible();
await expect(page.locator("#questionCard-9").getByRole("button", { name: "Back" })).toBeVisible();
@@ -895,16 +895,28 @@ test.describe("Testing Survey with advanced logic", async () => {
page.getByRole("rowheader", { name: surveys.createWithLogicAndSubmit.matrix.rows[2] })
).toBeVisible();
await expect(
- page.getByRole("cell", { name: surveys.createWithLogicAndSubmit.matrix.columns[0], exact: true })
+ page.getByRole("columnheader", {
+ name: surveys.createWithLogicAndSubmit.matrix.columns[0],
+ exact: true,
+ })
).toBeVisible();
await expect(
- page.getByRole("cell", { name: surveys.createWithLogicAndSubmit.matrix.columns[1], exact: true })
+ page.getByRole("columnheader", {
+ name: surveys.createWithLogicAndSubmit.matrix.columns[1],
+ exact: true,
+ })
).toBeVisible();
await expect(
- page.getByRole("cell", { name: surveys.createWithLogicAndSubmit.matrix.columns[2], exact: true })
+ page.getByRole("columnheader", {
+ name: surveys.createWithLogicAndSubmit.matrix.columns[2],
+ exact: true,
+ })
).toBeVisible();
await expect(
- page.getByRole("cell", { name: surveys.createWithLogicAndSubmit.matrix.columns[3], exact: true })
+ page.getByRole("columnheader", {
+ name: surveys.createWithLogicAndSubmit.matrix.columns[3],
+ exact: true,
+ })
).toBeVisible();
await expect(page.locator("#questionCard-7").getByRole("button", { name: "Next" })).toBeVisible();
await expect(page.locator("#questionCard-7").getByRole("button", { name: "Back" })).toBeVisible();
@@ -995,15 +1007,15 @@ test.describe("Testing Survey with advanced logic", async () => {
const updatedUrl = currentUrl.replace("summary?share=true", "responses");
await page.goto(updatedUrl);
- await page.waitForSelector("table#response-table");
-
- await expect(page.getByRole("cell", { name: "score" })).toBeVisible();
+ const responseTable = page.locator("table#response-table");
+ await expect(responseTable).toBeVisible();
+ await expect(responseTable.getByRole("columnheader", { name: /^score$/i })).toBeVisible({
+ timeout: 15000,
+ });
await page.waitForLoadState("networkidle");
await page.waitForTimeout(5000);
- await page.pause();
-
// Look for any cell containing "32" or a score-related value
const scoreCell = page.getByRole("cell").filter({ hasText: /^32/ });
await expect(scoreCell).toBeVisible({
diff --git a/apps/web/scripts/docker/read-secrets.sh b/apps/web/scripts/docker/read-secrets.sh
index 6a994a4fc8..f25de5f8a7 100644
--- a/apps/web/scripts/docker/read-secrets.sh
+++ b/apps/web/scripts/docker/read-secrets.sh
@@ -1,29 +1,39 @@
#!/bin/sh
set -eu
+
+# Build-time fallbacks used only when Docker secrets are unavailable (for example
+# in forked PR validations where repository secrets are not exposed).
+DEFAULT_DATABASE_URL="postgresql://test:test@localhost:5432/formbricks"
+DEFAULT_ENCRYPTION_KEY="0123456789abcdef0123456789abcdef"
+DEFAULT_REDIS_URL="redis://localhost:6379"
+
if [ -f "/run/secrets/database_url" ]; then
IFS= read -r DATABASE_URL < /run/secrets/database_url || true
- DATABASE_URL=${DATABASE_URL%$'\n'}
- export DATABASE_URL
-else
- echo "DATABASE_URL secret not found. Build will fail because it is required by the application."
fi
+if [ -z "${DATABASE_URL:-}" ]; then
+ DATABASE_URL="${DEFAULT_DATABASE_URL}"
+ echo "⚠️ DATABASE_URL secret not found or empty. Using build-time fallback value."
+fi
+export DATABASE_URL
if [ -f "/run/secrets/encryption_key" ]; then
IFS= read -r ENCRYPTION_KEY < /run/secrets/encryption_key || true
- ENCRYPTION_KEY=${ENCRYPTION_KEY%$'\n'}
- export ENCRYPTION_KEY
-else
- echo "ENCRYPTION_KEY secret not found. Build will fail because it is required by the application."
fi
+if [ -z "${ENCRYPTION_KEY:-}" ]; then
+ ENCRYPTION_KEY="${DEFAULT_ENCRYPTION_KEY}"
+ echo "⚠️ ENCRYPTION_KEY secret not found or empty. Using build-time fallback value."
+fi
+export ENCRYPTION_KEY
if [ -f "/run/secrets/redis_url" ]; then
IFS= read -r REDIS_URL < /run/secrets/redis_url || true
- REDIS_URL=${REDIS_URL%$'\n'}
- export REDIS_URL
-else
- echo "REDIS_URL secret not found. Build will fail because it is required by the application."
fi
+if [ -z "${REDIS_URL:-}" ]; then
+ REDIS_URL="${DEFAULT_REDIS_URL}"
+ echo "⚠️ REDIS_URL secret not found or empty. Using build-time fallback value."
+fi
+export REDIS_URL
if [ -f "/run/secrets/sentry_auth_token" ]; then
# Only upload sourcemaps on amd64 platform to avoid duplicate uploads
@@ -51,4 +61,4 @@ echo " REDIS_URL: $([ -n "${REDIS_URL:-}" ] && printf '[SET]' || printf '[NOT S
echo " SENTRY_AUTH_TOKEN: $([ -n "${SENTRY_AUTH_TOKEN:-}" ] && printf '[SET]' || printf '[NOT SET]')"
echo " TARGETARCH: $([ -n "${TARGETARCH:-}" ] && printf '%s' "${TARGETARCH}" || printf '[NOT SET]')"
-exec "$@"
\ No newline at end of file
+exec "$@"
diff --git a/docs/development/technical-handbook/database-model.mdx b/docs/development/technical-handbook/database-model.mdx
index 64fd0eaac7..fe559c2a73 100644
--- a/docs/development/technical-handbook/database-model.mdx
+++ b/docs/development/technical-handbook/database-model.mdx
@@ -88,7 +88,7 @@ erDiagram
- Records user answers to surveys
- Links to contact information when available
- - Supports tagging and notes for analysis
+ - Supports tagging for analysis
3. **ActionClass**
- Defines triggering points for surveys
diff --git a/docs/images/xm-and-surveys/core-features/styling-theme/form-css-styling.webp b/docs/images/xm-and-surveys/core-features/styling-theme/form-css-styling.webp
new file mode 100644
index 0000000000..8a1bec036a
Binary files /dev/null and b/docs/images/xm-and-surveys/core-features/styling-theme/form-css-styling.webp differ
diff --git a/docs/xm-and-surveys/core-features/styling-theme.mdx b/docs/xm-and-surveys/core-features/styling-theme.mdx
index 3df460223f..77ef83e9ab 100644
--- a/docs/xm-and-surveys/core-features/styling-theme.mdx
+++ b/docs/xm-and-surveys/core-features/styling-theme.mdx
@@ -1,6 +1,6 @@
---
title: "Styling Theme"
-description: "Keep the survey styling consistent over all surveys with a Styling Theme. Customize the colors, fonts, and other styling options to match your brand's aesthetic."
+description: "Keep the survey styling consistent over all surveys with a Styling Theme. Customize colors, fonts, buttons, inputs, and more to match your brand."
icon: "palette"
---
@@ -10,48 +10,114 @@ icon: "palette"
uploads](/self-hosting/configuration/file-uploads) before using these features.
-Keep the survey styling consistent over all surveys with a Styling Theme. Customize the colors, fonts, and other styling options to match your brand's aesthetic.
+Keep the survey styling consistent over all surveys with a Styling Theme. Customize colors, fonts, buttons, inputs, and other styling options to match your brand's aesthetic.
## Configuration
In the left side bar, you find the `Configuration` page. On this page you find the `Look & Feel` section:
-
-/>
+
-## **Styling Options**
+## Survey styling
-1. **Form Styling:** Customize the survey card using the following settings
+The Survey styling section gives you granular control over every text and input element in your survey. Expand the **Survey styling** panel to find collapsible sub-sections for Headlines & Descriptions, Inputs, Buttons, and Options.
-
+### Headlines & Descriptions
-- **Brand Color**: Sets the primary color tone of the survey.
-- **Text Color**: This is a single color scheme that will be used across to display all the text on your survey. Ensures all text is readable against the background.
-- **Input Color:** Alters the border color of input fields.
-- **Input Border Color**: This is the color of the border of the form input field.
+Fine-tune how question headlines, descriptions, and upper labels appear:
-2. **Card Styling:** Adjust the look of the survey card
+| Property | Description |
+|---|---|
+| **Headline Color** | Color of the question headline text |
+| **Description Color** | Color of the question description text |
+| **Headline Font Size** | Font size for headlines (in `px` or any CSS unit) |
+| **Description Font Size** | Font size for descriptions |
+| **Headline Font Weight** | Numeric font weight for headlines (e.g. `400`, `600`, `700`) |
+| **Description Font Weight** | Numeric font weight for descriptions |
+| **Upper Label Color** | Color of the small labels above input fields |
+| **Upper Label Font Size** | Font size for upper labels |
+| **Upper Label Font Weight** | Numeric font weight for upper labels |
-
+### Inputs
-- **Roundness**: Adjusts the corner roundness of the survey card and its components (including input boxes, buttons).
-- **Card Background Color**: Sets the card's main background color.
-- **Card Border Color**: Changes the border color of the card
+Control the appearance of text inputs, textareas, and other form fields:
-- **Hide Progress Bar**: Optionally remove the progress bar to simplify the survey experience
-- **Add Highlight Border**: Adds a distinct border for emphasis.
+| Property | Description |
+|---|---|
+| **Input Color** | Background color of input fields |
+| **Input Border Color** | Border color of input fields |
+| **Input Text Color** | Color of text typed into inputs |
+| **Border Radius** | Corner roundness of input fields (in `px` or any CSS unit) |
+| **Height** | Height of input fields |
+| **Font Size** | Font size of text inside inputs |
+| **Padding X** | Horizontal padding inside inputs |
+| **Padding Y** | Vertical padding inside inputs |
+| **Placeholder Opacity** | Opacity of placeholder text (`0` to `1`) |
+| **Shadow** | CSS box-shadow value for inputs (e.g. `0 1px 2px rgba(0,0,0,0.1)`) |
-3. **Background Styling**: Customize the survey background with static colors, animations, or images (upload your own or get from Unsplash)
+### Buttons
-
This is only available for Link Surveys
+Customize the submit and navigation buttons:
-
+| Property | Description |
+|---|---|
+| **Button Background** | Background color of buttons |
+| **Button Text** | Text color of buttons |
+| **Border Radius** | Corner roundness of buttons |
+| **Height** | Height of buttons |
+| **Font Size** | Font size of button text |
+| **Font Weight** | Numeric font weight for button text |
+| **Padding X** | Horizontal padding inside buttons |
+| **Padding Y** | Vertical padding inside buttons |
-- **Color**: Pick any color for the background
-- **Animation**: Add dynamic animations to enhance user experience..
-- **Upload**: Use a custom uploaded image for a personalized touch. Images must be 5 MB or less.
-- Image: Choose from Unsplash's extensive gallery. Note that these images will have a link and mention of the author & Unsplash on the bottom right to give them the credit for their awesome work!
-- **Background Overlay**: Adjust the background's opacity
+### Options
+
+Style the select options in single-select, multi-select, and similar question types:
+
+| Property | Description |
+|---|---|
+| **Option Background** | Background color of option items |
+| **Option Label Color** | Text color of option labels |
+| **Border Radius** | Corner roundness of option items |
+| **Padding X** | Horizontal padding inside options |
+| **Padding Y** | Vertical padding inside options |
+| **Font Size** | Font size of option text |
+
+## Card Styling
+
+Adjust the look of the survey card container:
+
+| Property | Description |
+|---|---|
+| **Roundness** | Corner roundness of the survey card (in `px` or any CSS unit) |
+| **Card Background Color** | Background color of the survey card |
+| **Card Border Color** | Border color of the survey card |
+| **Add Highlight Border** | Adds a distinct colored border for emphasis (app surveys only) |
+| **Card Arrangement** | Layout mode for stacking cards: **Simple**, **Straight**, or **Casual** |
+
+### Progress Bar
+
+When the progress bar is visible (toggle **Hide Progress Bar** to control it), you can customize:
+
+| Property | Description |
+|---|---|
+| **Track Background** | Background color of the progress track |
+| **Indicator Background** | Fill color of the progress indicator |
+| **Track Height** | Height of the progress bar track |
+
+## Background Styling
+
+Customize the survey background with static colors, animations, or images (upload your own or pick from Unsplash).
+
+
Background styling is only available for Link Surveys.
+
+| Property | Description |
+|---|---|
+| **Color** | Pick any color for the background |
+| **Animation** | Add dynamic animations to enhance user experience |
+| **Upload** | Use a custom uploaded image (5 MB max) |
+| **Image** | Choose from Unsplash's gallery (attribution shown automatically) |
+| **Background Overlay** | Adjust the background's opacity / brightness |
## Add Brand Logo
@@ -59,30 +125,44 @@ Customize your survey with your brand's logo.
Brand logos are only visible on Link Survey pages.
-1. In the Look & Feel page itself in Project settings, scroll down to see the Logo Upload box.
+
+
+ In the **Look & Feel** page in Project settings, scroll down to the **Logo Upload** box.
-
+ 
+
-2. Upload your logo. Logos must be 5 MB or less.
+
+ Upload your logo. Logos must be 5 MB or less.
-
+ 
+
-3. Add a background color: If you’ve uploaded a transparent image and want to add background to it, enable this toggle and select the color of your choice.
+
+ If you've uploaded a transparent image and want to add a background to it, enable the toggle and select a color.
-
+ 
+
-4. Remember to save your changes!
+
+ Remember to save your changes!
-
+ 
+
+
-
The logo settings apply across all Link Surveys pages.
+
The logo settings apply across all Link Survey pages.
## Overwrite Styling Theme
-You can allow to overwrite the styling theme for individual surveys to create unique styles for each survey:
+You can allow overwriting the styling theme for individual surveys to create unique styles per survey:
-
+
-In the survey editor, a tab called `Styling` will appear. Here you can overwrite the default styling theme.
+In the survey editor, a **Styling** tab will appear where you can overwrite the default styling theme. See the [Custom Styling](/xm-and-surveys/surveys/general-features/overwrite-styling) guide for details.
----
+## CSS Variables Reference
+
+Under the hood, every styling property maps to a CSS variable prefixed with `--fb-`. For App & Website Surveys, you can override these directly in your global CSS file (e.g., `globals.css`) by targeting the `#fbjs` selector.
+
+See the full [CSS Variables Reference](/xm-and-surveys/surveys/general-features/overwrite-styling#overwrite-css-styles-for-app--website-surveys) in the Custom Styling guide.
diff --git a/docs/xm-and-surveys/surveys/general-features/overwrite-styling.mdx b/docs/xm-and-surveys/surveys/general-features/overwrite-styling.mdx
index 331766395c..f255e828bd 100644
--- a/docs/xm-and-surveys/surveys/general-features/overwrite-styling.mdx
+++ b/docs/xm-and-surveys/surveys/general-features/overwrite-styling.mdx
@@ -76,12 +76,4 @@ Windows XP
Who's a Good Boy?
"Things you've likely said to your dog."
-
-
-### Fixes & Improvements:
-- **Numbered list formatting**: Fixed numbering issues.
-- **Consistent image alt text**: Updated descriptions for clarity.
-- **CSS syntax correction**: Removed unnecessary text before code block.
-- **Grammar & punctuation fixes**: Ensured clarity and smooth readability.
-
-This should now display correctly on Mintlify! Let me know if you need further tweaks.
\ No newline at end of file
+
\ No newline at end of file
diff --git a/package.json b/package.json
index 0e997ed01e..558a25522c 100644
--- a/package.json
+++ b/package.json
@@ -45,22 +45,19 @@
"i18n:validate": "pnpm scan-translations"
},
"dependencies": {
- "react": "19.2.3",
- "react-dom": "19.2.3",
- "next": "16.1.6"
+ "react": "19.2.4",
+ "react-dom": "19.2.4"
},
"devDependencies": {
- "@azure/identity": "4.13.0",
- "@azure/playwright": "1.0.0",
+ "@azure/playwright": "1.1.2",
"@formbricks/eslint-config": "workspace:*",
- "@playwright/test": "1.56.1",
- "eslint": "8.57.0",
- "glob": "^11.1.0",
+ "@playwright/test": "1.58.2",
+ "eslint": "8.57.1",
"husky": "9.1.7",
- "lint-staged": "16.0.0",
- "rimraf": "6.0.1",
- "tsx": "4.19.4",
- "turbo": "2.5.3"
+ "lint-staged": "16.3.1",
+ "rimraf": "6.1.3",
+ "tsx": "4.21.0",
+ "turbo": "2.8.12"
},
"lint-staged": {
"(apps|packages)/**/*.{js,ts,jsx,tsx}": [
@@ -76,7 +73,7 @@
"engines": {
"node": ">=20.0.0"
},
- "packageManager": "pnpm@10.28.2",
+ "packageManager": "pnpm@10.30.3",
"nextBundleAnalysis": {
"budget": 358400,
"budgetPercentIncreaseRed": 20,
@@ -95,13 +92,15 @@
"qs": ">=6.14.1",
"preact": ">=10.26.10",
"fast-xml-parser": ">=5.3.4",
- "diff": ">=8.0.3"
+ "diff": ">=8.0.3",
+ "@isaacs/brace-expansion": ">=5.0.1",
+ "@microsoft/api-extractor": ">=7.57.6"
},
"comments": {
- "overrides": "Security fixes for transitive dependencies. Remove when upstream packages update: axios (CVE-2025-58754) - awaiting @boxyhq/saml-jackson update | node-forge (Dependabot #230) - awaiting @boxyhq/saml-jackson update | tar-fs (Dependabot #205) - awaiting upstream dependency updates | tar (Dependabot #249/#264) - awaiting @boxyhq/saml-jackson/sqlite3 dependency updates | typeorm (Dependabot #223) - awaiting @boxyhq/saml-jackson update | systeminformation (Dependabot #241) - awaiting @opentelemetry/host-metrics update | qs (Dependabot #245) - awaiting googleapis-common and stripe updates | preact (Dependabot #247) - awaiting next-auth update | fast-xml-parser (Dependabot #270) - awaiting @boxyhq/saml-jackson update | diff (Dependabot #269) - awaiting @microsoft/api-extractor update"
+ "overrides": "Security fixes for transitive dependencies. Remove when upstream packages update: axios (CVE-2025-58754) - awaiting @boxyhq/saml-jackson update | node-forge (Dependabot #230) - awaiting @boxyhq/saml-jackson update | tar-fs (Dependabot #205) - awaiting upstream dependency updates | tar (Dependabot #249/#264) - awaiting @boxyhq/saml-jackson/sqlite3 dependency updates | typeorm (Dependabot #223) - awaiting @boxyhq/saml-jackson update | systeminformation (Dependabot #241) - awaiting @opentelemetry/host-metrics update | qs (Dependabot #245) - awaiting googleapis-common and stripe updates | preact (Dependabot #247) - awaiting next-auth update | fast-xml-parser (Dependabot #270) - awaiting @boxyhq/saml-jackson update | diff (Dependabot #269) - awaiting @microsoft/api-extractor update | @isaacs/brace-expansion (Dependabot #271) - awaiting upstream updates | @microsoft/api-extractor - overridden until vite-plugin-dts lock resolution catches up"
},
"patchedDependencies": {
- "next-auth@4.24.12": "patches/next-auth@4.24.12.patch"
+ "next-auth@4.24.13": "patches/next-auth@4.24.13.patch"
}
}
}
diff --git a/packages/cache/package.json b/packages/cache/package.json
index dbfb91a5a7..bf3c38b289 100644
--- a/packages/cache/package.json
+++ b/packages/cache/package.json
@@ -38,14 +38,14 @@
"author": "Formbricks
",
"dependencies": {
"@formbricks/logger": "workspace:*",
- "redis": "5.8.1",
+ "redis": "5.11.0",
"zod": "3.25.76"
},
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*",
"vite": "6.4.1",
- "vitest": "3.1.3",
- "@vitest/coverage-v8": "3.1.3"
+ "vitest": "3.2.4",
+ "@vitest/coverage-v8": "3.2.4"
}
}
diff --git a/packages/config-eslint/package.json b/packages/config-eslint/package.json
index c3f2e7ceb0..1d022e0419 100644
--- a/packages/config-eslint/package.json
+++ b/packages/config-eslint/package.json
@@ -3,16 +3,16 @@
"version": "0.0.0",
"private": true,
"devDependencies": {
- "@next/eslint-plugin-next": "15.3.2",
- "@typescript-eslint/eslint-plugin": "8.32.1",
- "@typescript-eslint/parser": "8.32.1",
+ "@next/eslint-plugin-next": "15.5.12",
+ "@typescript-eslint/eslint-plugin": "8.56.1",
+ "@typescript-eslint/parser": "8.56.1",
"@vercel/style-guide": "6.0.0",
- "eslint-config-next": "15.3.2",
- "eslint-config-prettier": "10.1.5",
- "eslint-config-turbo": "2.5.3",
+ "eslint-config-next": "15.5.12",
+ "eslint-config-prettier": "10.1.8",
+ "eslint-config-turbo": "2.8.12",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-react-refresh": "0.4.20",
- "@vitest/eslint-plugin": "1.1.44"
+ "@vitest/eslint-plugin": "1.6.9"
}
}
diff --git a/packages/config-prettier/package.json b/packages/config-prettier/package.json
index 81dda53942..d8e0b681e8 100644
--- a/packages/config-prettier/package.json
+++ b/packages/config-prettier/package.json
@@ -8,8 +8,8 @@
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "5.2.2",
- "prettier": "3.5.3",
- "prettier-plugin-tailwindcss": "0.6.11",
- "prettier-plugin-sort-json": "4.1.1"
+ "prettier": "3.8.1",
+ "prettier-plugin-tailwindcss": "0.7.2",
+ "prettier-plugin-sort-json": "4.2.0"
}
}
diff --git a/packages/config-typescript/package.json b/packages/config-typescript/package.json
index 0338b4521f..d175101a17 100644
--- a/packages/config-typescript/package.json
+++ b/packages/config-typescript/package.json
@@ -7,9 +7,9 @@
"clean": "rimraf node_modules dist turbo"
},
"devDependencies": {
- "@types/node": "22.15.18",
- "@types/react": "19.1.4",
- "@types/react-dom": "19.1.5",
- "typescript": "5.8.3"
+ "@types/node": "22.19.13",
+ "@types/react": "19.2.14",
+ "@types/react-dom": "19.2.3",
+ "typescript": "5.9.3"
}
}
diff --git a/packages/database/README.md b/packages/database/README.md
index 457592b072..597c6a91f8 100644
--- a/packages/database/README.md
+++ b/packages/database/README.md
@@ -134,6 +134,7 @@ ALLOW_SEED=true
### Seeding Logic
The `pnpm db:seed` script:
+
1. **Infrastructure**: Upserts a default organization, project, and environments.
2. **Users**: Creates default users with the following credentials (passwords are hashed):
- **Admin**: `admin@formbricks.com` / `password123`
diff --git a/packages/database/package.json b/packages/database/package.json
index b0920e0389..07bebcc7f7 100644
--- a/packages/database/package.json
+++ b/packages/database/package.json
@@ -48,8 +48,8 @@
},
"dependencies": {
"@formbricks/logger": "workspace:*",
- "@paralleldrive/cuid2": "2.2.2",
- "@prisma/client": "6.14.0",
+ "@paralleldrive/cuid2": "2.3.1",
+ "@prisma/client": "6.19.2",
"bcryptjs": "2.4.3",
"uuid": "11.1.0",
"zod": "3.25.76",
@@ -58,14 +58,12 @@
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*",
- "@types/bcryptjs": "2.4.6",
"dotenv-cli": "8.0.0",
- "glob": "11.1.0",
- "prisma": "6.14.0",
- "prisma-json-types-generator": "3.5.4",
- "ts-node": "10.9.2",
- "tsx": "4.19.2",
+ "glob": "13.0.6",
+ "prisma": "6.19.2",
+ "prisma-json-types-generator": "3.6.2",
+ "tsx": "4.21.0",
"vite": "6.4.1",
- "vite-plugin-dts": "4.5.3"
+ "vite-plugin-dts": "4.5.4"
}
}
diff --git a/packages/email/.prettierrc.cjs b/packages/email/.prettierrc.cjs
new file mode 100644
index 0000000000..21886d1fe2
--- /dev/null
+++ b/packages/email/.prettierrc.cjs
@@ -0,0 +1,6 @@
+const baseConfig = require("../../.prettierrc.js");
+
+module.exports = {
+ ...baseConfig,
+ tailwindConfig: "./tailwind.config.js",
+};
diff --git a/packages/email/emails/auth/new-email-verification.tsx b/packages/email/emails/auth/new-email-verification.tsx
index f2e6a102f4..782e8ad689 100644
--- a/packages/email/emails/auth/new-email-verification.tsx
+++ b/packages/email/emails/auth/new-email-verification.tsx
@@ -25,7 +25,7 @@ export function NewEmailVerification({
{t("emails.verification_security_notice")}
{t("emails.verification_email_click_on_this_link")}
-
+
{verifyLink}
{t("emails.verification_email_link_valid_for_24_hours")}
diff --git a/packages/email/emails/auth/verification-email.tsx b/packages/email/emails/auth/verification-email.tsx
index 96cdab8d32..30cf457a15 100644
--- a/packages/email/emails/auth/verification-email.tsx
+++ b/packages/email/emails/auth/verification-email.tsx
@@ -26,7 +26,7 @@ export function VerificationEmail({
{t("emails.verification_email_text")}
{t("emails.verification_email_click_on_this_link")}
-
+
{verifyLink}
{t("emails.verification_email_link_valid_for_24_hours")}
diff --git a/packages/email/emails/survey/follow-up-email.tsx b/packages/email/emails/survey/follow-up-email.tsx
index 20e6022c3d..636de1cbeb 100644
--- a/packages/email/emails/survey/follow-up-email.tsx
+++ b/packages/email/emails/survey/follow-up-email.tsx
@@ -57,7 +57,7 @@ export function FollowUpEmail({
? `${t("emails.number_variable")}: ${variable.name}`
: `${t("emails.text_variable")}: ${variable.name}`}
-
+
{variable.value}
@@ -70,7 +70,7 @@ export function FollowUpEmail({
{t("emails.hidden_field")}: {hiddenField.id}
-
+
{hiddenField.value}
diff --git a/packages/email/emails/survey/response-finished-email.tsx b/packages/email/emails/survey/response-finished-email.tsx
index 1a1673d823..00c335ca81 100644
--- a/packages/email/emails/survey/response-finished-email.tsx
+++ b/packages/email/emails/survey/response-finished-email.tsx
@@ -90,7 +90,7 @@ export function ResponseFinishedEmail({
)}
{variable.name}
-
+
{variableResponse}
@@ -110,7 +110,7 @@ export function ResponseFinishedEmail({
{hiddenFieldId}
-
+
{hiddenFieldResponse}
diff --git a/packages/email/package.json b/packages/email/package.json
index 44785b94de..a6642f93b2 100644
--- a/packages/email/package.json
+++ b/packages/email/package.json
@@ -13,18 +13,18 @@
"clean": "rimraf .turbo node_modules dist"
},
"dependencies": {
- "@react-email/components": "1.0.6",
- "react-email": "5.2.5"
+ "@react-email/components": "1.0.8",
+ "react-email": "5.2.9"
},
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*",
"@formbricks/types": "workspace:*",
- "@react-email/preview-server": "5.2.5",
- "autoprefixer": "10.4.21",
+ "@react-email/preview-server": "5.2.9",
+ "autoprefixer": "10.4.27",
"clsx": "2.1.1",
- "postcss": "8.5.3",
- "tailwind-merge": "3.2.0",
- "tailwindcss": "3.4.17"
+ "postcss": "8.5.8",
+ "tailwind-merge": "3.5.0",
+ "tailwindcss": "3.4.19"
}
}
diff --git a/packages/email/src/components/email-element-header.tsx b/packages/email/src/components/email-element-header.tsx
index 1ba1436734..486bc248fd 100644
--- a/packages/email/src/components/email-element-header.tsx
+++ b/packages/email/src/components/email-element-header.tsx
@@ -10,11 +10,11 @@ interface ElementHeaderProps {
export function ElementHeader({ headline, subheader, className }: ElementHeaderProps): React.JSX.Element {
return (
<>
-
+
{subheader && (
-
+
)}
diff --git a/packages/email/src/lib/email-utils.tsx b/packages/email/src/lib/email-utils.tsx
index 639e478c7b..c38ae0713b 100644
--- a/packages/email/src/lib/email-utils.tsx
+++ b/packages/email/src/lib/email-utils.tsx
@@ -26,7 +26,7 @@ export const renderEmailResponseValue = (
return (
{overrideFileUploadResponse ? (
-
+
{t("emails.render_email_response_value_file_upload_response_link_not_included")}
) : (
@@ -76,6 +76,6 @@ export const renderEmailResponseValue = (
);
default:
- return {response as string};
+ return {response as string};
}
};
diff --git a/packages/email/src/lib/example-data.ts b/packages/email/src/lib/example-data.ts
index e011b9353e..13a01ce0c0 100644
--- a/packages/email/src/lib/example-data.ts
+++ b/packages/email/src/lib/example-data.ts
@@ -106,7 +106,6 @@ export const exampleData = {
url: "https://example.com",
},
tags: [],
- notes: [],
ttc: {},
singleUseId: null,
language: "default",
diff --git a/packages/i18n-utils/package.json b/packages/i18n-utils/package.json
index caab552d7d..aba9e69c02 100644
--- a/packages/i18n-utils/package.json
+++ b/packages/i18n-utils/package.json
@@ -35,15 +35,15 @@
"scan-translations": "tsx src/scan-translations.ts"
},
"dependencies": {
- "glob": "^11.1.0"
+ "glob": "^13.0.6"
},
"devDependencies": {
- "vite": "6.4.1",
"@formbricks/config-typescript": "workspace:*",
- "vitest": "3.1.3",
"@formbricks/eslint-config": "workspace:*",
- "vite-plugin-dts": "4.5.3",
- "@types/node": "^22.10.5",
- "tsx": "^4.19.4"
+ "@types/node": "^22.19.13",
+ "tsx": "^4.21.0",
+ "vite": "6.4.1",
+ "vite-plugin-dts": "4.5.4",
+ "vitest": "3.2.4"
}
}
diff --git a/packages/js-core/package.json b/packages/js-core/package.json
index ecb34ec4c8..a49edfe7e1 100644
--- a/packages/js-core/package.json
+++ b/packages/js-core/package.json
@@ -44,12 +44,11 @@
"author": "Formbricks ",
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",
- "@trivago/prettier-plugin-sort-imports": "5.2.2",
"@formbricks/eslint-config": "workspace:*",
- "@vitest/coverage-v8": "3.1.3",
- "terser": "5.39.1",
+ "@vitest/coverage-v8": "3.2.4",
+ "terser": "5.46.0",
"vite": "6.4.1",
- "vite-plugin-dts": "4.5.3",
- "vitest": "3.1.3"
+ "vite-plugin-dts": "4.5.4",
+ "vitest": "3.2.4"
}
}
diff --git a/packages/js-core/src/lib/common/tests/utils.test.ts b/packages/js-core/src/lib/common/tests/utils.test.ts
index 3c90c962a0..fc24d0d213 100644
--- a/packages/js-core/src/lib/common/tests/utils.test.ts
+++ b/packages/js-core/src/lib/common/tests/utils.test.ts
@@ -887,6 +887,7 @@ describe("utils.ts", () => {
targetElement.className = "other";
targetElement.matches = vi.fn(() => false);
+ targetElement.closest = vi.fn(() => null); // no ancestor matches either
const action: TEnvironmentStateActionClass = {
id: "clabc123abc",
@@ -993,13 +994,93 @@ describe("utils.ts", () => {
expect(result).toBe(true);
});
+ // --- Regression tests for nested child click target (issue #7314) ---
+ // In this test environment document.createElement() returns a plain mock object,
+ // so we set .matches and .closest as vi.fn() — the same pattern used by existing tests.
+ // This exercises the exact code path of the fix: matches() fails → closest() succeeds.
+
+ test("returns true when clicking a child element inside a button matched by cssSelector", () => {
+ const button = document.createElement("button");
+ const icon = document.createElement("span");
+
+ // Simulate: icon does NOT directly match ".my-btn", but its closest ancestor does
+ (icon as unknown as { matches: ReturnType }).matches = vi.fn(() => false);
+ (icon as unknown as { closest: ReturnType }).closest = vi.fn(() => button);
+
+ const action: TEnvironmentStateActionClass = {
+ id: "clabc123abc",
+ name: "Test Action",
+ type: "noCode",
+ key: null,
+ noCodeConfig: {
+ type: "click",
+ urlFilters: [],
+ elementSelector: { cssSelector: ".my-btn" },
+ },
+ };
+
+ // Before fix: matches() → false → returns false (bug)
+ // After fix: matches() → false → closest() → button → returns true (correct)
+ const result = evaluateNoCodeConfigClick(icon as unknown as HTMLElement, action);
+ expect(result).toBe(true);
+ });
+
+ test("returns false when clicking a child element with no matching ancestor", () => {
+ const other = document.createElement("div");
+
+ // Simulate: element doesn't match, and no ancestor matches either
+ (other as unknown as { matches: ReturnType }).matches = vi.fn(() => false);
+ (other as unknown as { closest: ReturnType }).closest = vi.fn(() => null);
+
+ const action: TEnvironmentStateActionClass = {
+ id: "clabc123abc",
+ name: "Test Action",
+ type: "noCode",
+ key: null,
+ noCodeConfig: {
+ type: "click",
+ urlFilters: [],
+ elementSelector: { cssSelector: ".my-btn" },
+ },
+ };
+
+ const result = evaluateNoCodeConfigClick(other as unknown as HTMLElement, action);
+ expect(result).toBe(false);
+ });
+
+ test("uses direct target (not closest) when target directly matches cssSelector", () => {
+ const button = document.createElement("button");
+
+ // Simulate: click on the button itself — matches() succeeds, closest() should NOT be called
+ (button as unknown as { matches: ReturnType }).matches = vi.fn(() => true);
+ const closestSpy = vi.fn();
+ (button as unknown as { closest: ReturnType }).closest = closestSpy;
+
+ const action: TEnvironmentStateActionClass = {
+ id: "clabc123abc",
+ name: "Test Action",
+ type: "noCode",
+ key: null,
+ noCodeConfig: {
+ type: "click",
+ urlFilters: [],
+ elementSelector: { cssSelector: ".my-btn" },
+ },
+ };
+
+ const result = evaluateNoCodeConfigClick(button as unknown as HTMLElement, action);
+ expect(result).toBe(true);
+ expect(closestSpy).not.toHaveBeenCalled(); // closest() is only a fallback
+ });
+
test("handles multiple cssSelectors correctly", () => {
const targetElement = document.createElement("div");
targetElement.className = "test other";
targetElement.matches = vi.fn((selector) => {
- return selector === ".test" || selector === ".other";
+ return selector === ".test" || selector === ".other" || selector === ".test .other";
});
+ targetElement.closest = vi.fn(() => null); // not needed but consistent with mock environment
const action: TEnvironmentStateActionClass = {
id: "clabc123abc",
diff --git a/packages/js-core/src/lib/common/utils.ts b/packages/js-core/src/lib/common/utils.ts
index c2c082c59b..f6dd081e83 100644
--- a/packages/js-core/src/lib/common/utils.ts
+++ b/packages/js-core/src/lib/common/utils.ts
@@ -304,20 +304,28 @@ export const evaluateNoCodeConfigClick = (
if (!innerHtml && !cssSelector) return false;
- if (innerHtml && targetElement.innerHTML !== innerHtml) return false;
+ // Resolve the element to test: prefer the direct click target, but walk up to
+ // the nearest ancestor that matches the CSS selector (event delegation for nested markup,
+ // e.g.