mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 00:49:42 -06:00
fix: emails font size (#6228)
This commit is contained in:
@@ -9,6 +9,11 @@ vi.mock("@/lib/utils/recall", () => ({
|
||||
|
||||
vi.mock("./i18n/utils", () => ({
|
||||
getLocalizedValue: vi.fn((obj, lang) => obj[lang] || obj.default),
|
||||
getLanguageCode: vi.fn((surveyLanguages, languageCode) => {
|
||||
if (!surveyLanguages?.length || !languageCode) return null; // Changed from "default" to null
|
||||
const language = surveyLanguages.find((surveyLanguage) => surveyLanguage.language.code === languageCode);
|
||||
return language?.default ? "default" : language?.language.code || "default";
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("Response Processing", () => {
|
||||
@@ -43,6 +48,16 @@ describe("Response Processing", () => {
|
||||
test("should return empty string for unsupported types", () => {
|
||||
expect(processResponseData(undefined as any)).toBe("");
|
||||
});
|
||||
|
||||
test("should filter out null values from array", () => {
|
||||
const input = ["a", null, "c"] as any;
|
||||
expect(processResponseData(input)).toBe("a; c");
|
||||
});
|
||||
|
||||
test("should filter out undefined values from array", () => {
|
||||
const input = ["a", undefined, "c"] as any;
|
||||
expect(processResponseData(input)).toBe("a; c");
|
||||
});
|
||||
});
|
||||
|
||||
describe("convertResponseValue", () => {
|
||||
@@ -125,6 +140,22 @@ describe("Response Processing", () => {
|
||||
expect(convertResponseValue("invalid", mockPictureSelectionQuestion)).toEqual([]);
|
||||
});
|
||||
|
||||
test("should handle pictureSelection type with number input", () => {
|
||||
expect(convertResponseValue(42, mockPictureSelectionQuestion)).toEqual([]);
|
||||
});
|
||||
|
||||
test("should handle pictureSelection type with object input", () => {
|
||||
expect(convertResponseValue({ key: "value" }, mockPictureSelectionQuestion)).toEqual([]);
|
||||
});
|
||||
|
||||
test("should handle pictureSelection type with null input", () => {
|
||||
expect(convertResponseValue(null as any, mockPictureSelectionQuestion)).toEqual([]);
|
||||
});
|
||||
|
||||
test("should handle pictureSelection type with undefined input", () => {
|
||||
expect(convertResponseValue(undefined as any, mockPictureSelectionQuestion)).toEqual([]);
|
||||
});
|
||||
|
||||
test("should handle default case with string input", () => {
|
||||
expect(convertResponseValue("answer", mockOpenTextQuestion)).toBe("answer");
|
||||
});
|
||||
@@ -320,6 +351,32 @@ describe("Response Processing", () => {
|
||||
charLimit: { enabled: false },
|
||||
},
|
||||
],
|
||||
languages: [
|
||||
{
|
||||
language: {
|
||||
id: "lang1",
|
||||
code: "default",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
alias: null,
|
||||
projectId: "proj1",
|
||||
},
|
||||
default: true,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
language: {
|
||||
id: "lang2",
|
||||
code: "en",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
alias: null,
|
||||
projectId: "proj1",
|
||||
},
|
||||
default: false,
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
const response = {
|
||||
id: "response1",
|
||||
@@ -349,5 +406,102 @@ describe("Response Processing", () => {
|
||||
const mapping = getQuestionResponseMapping(survey, response);
|
||||
expect(mapping[0].question).toBe("Question 1 EN");
|
||||
});
|
||||
|
||||
test("should handle null response language", () => {
|
||||
const response = {
|
||||
id: "response1",
|
||||
surveyId: "survey1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
finished: true,
|
||||
data: { q1: "Answer 1" },
|
||||
language: null,
|
||||
meta: {
|
||||
url: undefined,
|
||||
country: undefined,
|
||||
action: undefined,
|
||||
source: undefined,
|
||||
userAgent: undefined,
|
||||
},
|
||||
notes: [],
|
||||
tags: [],
|
||||
person: null,
|
||||
personAttributes: {},
|
||||
ttc: {},
|
||||
variables: {},
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
singleUseId: null,
|
||||
};
|
||||
const mapping = getQuestionResponseMapping(mockSurvey, response);
|
||||
expect(mapping).toHaveLength(2);
|
||||
expect(mapping[0].question).toBe("Question 1");
|
||||
});
|
||||
|
||||
test("should handle undefined response language", () => {
|
||||
const response = {
|
||||
id: "response1",
|
||||
surveyId: "survey1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
finished: true,
|
||||
data: { q1: "Answer 1" },
|
||||
language: null,
|
||||
meta: {
|
||||
url: undefined,
|
||||
country: undefined,
|
||||
action: undefined,
|
||||
source: undefined,
|
||||
userAgent: undefined,
|
||||
},
|
||||
notes: [],
|
||||
tags: [],
|
||||
person: null,
|
||||
personAttributes: {},
|
||||
ttc: {},
|
||||
variables: {},
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
singleUseId: null,
|
||||
};
|
||||
const mapping = getQuestionResponseMapping(mockSurvey, response);
|
||||
expect(mapping).toHaveLength(2);
|
||||
expect(mapping[0].question).toBe("Question 1");
|
||||
});
|
||||
|
||||
test("should handle empty survey languages", () => {
|
||||
const survey = {
|
||||
...mockSurvey,
|
||||
languages: [], // Empty languages array
|
||||
};
|
||||
const response = {
|
||||
id: "response1",
|
||||
surveyId: "survey1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
finished: true,
|
||||
data: { q1: "Answer 1" },
|
||||
language: "en",
|
||||
meta: {
|
||||
url: undefined,
|
||||
country: undefined,
|
||||
action: undefined,
|
||||
source: undefined,
|
||||
userAgent: undefined,
|
||||
},
|
||||
notes: [],
|
||||
tags: [],
|
||||
person: null,
|
||||
personAttributes: {},
|
||||
ttc: {},
|
||||
variables: {},
|
||||
contact: null,
|
||||
contactAttributes: {},
|
||||
singleUseId: null,
|
||||
};
|
||||
const mapping = getQuestionResponseMapping(survey, response);
|
||||
expect(mapping).toHaveLength(2);
|
||||
expect(mapping[0].question).toBe("Question 1"); // Should fallback to default
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { parseRecallInfo } from "@/lib/utils/recall";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey, TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys/types";
|
||||
import { getLocalizedValue } from "./i18n/utils";
|
||||
import { getLanguageCode, getLocalizedValue } from "./i18n/utils";
|
||||
|
||||
// function to convert response value of type string | number | string[] or Record<string, string> to string | string[]
|
||||
export const convertResponseValue = (
|
||||
@@ -39,12 +39,14 @@ export const getQuestionResponseMapping = (
|
||||
response: string | string[];
|
||||
type: TSurveyQuestionType;
|
||||
}[] = [];
|
||||
const responseLanguageCode = getLanguageCode(survey.languages, response.language);
|
||||
|
||||
for (const question of survey.questions) {
|
||||
const answer = response.data[question.id];
|
||||
|
||||
questionResponseMapping.push({
|
||||
question: parseRecallInfo(
|
||||
getLocalizedValue(question.headline, response.language ?? "default"),
|
||||
getLocalizedValue(question.headline, responseLanguageCode ?? "default"),
|
||||
response.data
|
||||
),
|
||||
response: convertResponseValue(answer, question),
|
||||
|
||||
@@ -8,7 +8,7 @@ interface EmailButtonProps {
|
||||
|
||||
export function EmailButton({ label, href }: EmailButtonProps): React.JSX.Element {
|
||||
return (
|
||||
<Button className="rounded-md bg-black px-6 py-3 text-white" href={href}>
|
||||
<Button className="rounded-md bg-black px-6 py-3 text-sm text-white" href={href}>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import React from "react";
|
||||
|
||||
export function EmailFooter({ t }: { t: TFnType }): React.JSX.Element {
|
||||
return (
|
||||
<Text>
|
||||
<Text className="text-sm">
|
||||
{t("emails.email_footer_text_1")}
|
||||
<br />
|
||||
{t("emails.email_footer_text_2")}
|
||||
|
||||
@@ -23,7 +23,7 @@ export async function EmailTemplate({
|
||||
<Html>
|
||||
<Tailwind>
|
||||
<Body
|
||||
className="m-0 h-full w-full justify-center bg-slate-50 p-6 text-center text-base font-medium text-slate-800"
|
||||
className="m-0 h-full w-full justify-center bg-slate-50 p-6 text-center text-sm text-slate-800"
|
||||
style={{
|
||||
fontFamily: "'Jost', 'Helvetica Neue', 'Segoe UI', 'Helvetica', 'sans-serif'",
|
||||
}}>
|
||||
@@ -47,24 +47,32 @@ export async function EmailTemplate({
|
||||
|
||||
<Section className="mt-4 text-center text-sm">
|
||||
<Link
|
||||
className="m-0 font-normal text-slate-500"
|
||||
className="m-0 text-sm font-normal text-slate-500"
|
||||
href="https://formbricks.com/?utm_source=email_header&utm_medium=email"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
{t("emails.email_template_text_1")}
|
||||
</Link>
|
||||
{IMPRINT_ADDRESS && (
|
||||
<Text className="m-0 font-normal text-slate-500 opacity-50">{IMPRINT_ADDRESS}</Text>
|
||||
<Text className="m-0 text-sm font-normal text-slate-500 opacity-50">{IMPRINT_ADDRESS}</Text>
|
||||
)}
|
||||
<Text className="m-0 font-normal text-slate-500 opacity-50">
|
||||
<Text className="m-0 text-sm font-normal text-slate-500 opacity-50">
|
||||
{IMPRINT_URL && (
|
||||
<Link href={IMPRINT_URL} target="_blank" rel="noopener noreferrer" className="text-slate-500">
|
||||
<Link
|
||||
href={IMPRINT_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-slate-500">
|
||||
{t("emails.imprint")}
|
||||
</Link>
|
||||
)}
|
||||
{IMPRINT_URL && PRIVACY_URL && " • "}
|
||||
{PRIVACY_URL && (
|
||||
<Link href={PRIVACY_URL} target="_blank" rel="noopener noreferrer" className="text-slate-500">
|
||||
<Link
|
||||
href={PRIVACY_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-slate-500">
|
||||
{t("emails.privacy_policy")}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -17,10 +17,10 @@ export async function ForgotPasswordEmail({
|
||||
<EmailTemplate t={t}>
|
||||
<Container>
|
||||
<Heading>{t("emails.forgot_password_email_heading")}</Heading>
|
||||
<Text>{t("emails.forgot_password_email_text")}</Text>
|
||||
<Text className="text-sm">{t("emails.forgot_password_email_text")}</Text>
|
||||
<EmailButton href={verifyLink} label={t("emails.forgot_password_email_change_password")} />
|
||||
<Text className="font-bold">{t("emails.forgot_password_email_link_valid_for_24_hours")}</Text>
|
||||
<Text className="mb-0">{t("emails.forgot_password_email_did_not_request")}</Text>
|
||||
<Text className="text-sm font-bold">{t("emails.forgot_password_email_link_valid_for_24_hours")}</Text>
|
||||
<Text className="mb-0 text-sm">{t("emails.forgot_password_email_did_not_request")}</Text>
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
|
||||
@@ -17,14 +17,14 @@ export async function NewEmailVerification({
|
||||
<EmailTemplate t={t}>
|
||||
<Container>
|
||||
<Heading>{t("emails.verification_email_heading")}</Heading>
|
||||
<Text>{t("emails.new_email_verification_text")}</Text>
|
||||
<Text>{t("emails.verification_security_notice")}</Text>
|
||||
<Text className="text-sm">{t("emails.new_email_verification_text")}</Text>
|
||||
<Text className="text-sm">{t("emails.verification_security_notice")}</Text>
|
||||
<EmailButton href={verifyLink} label={t("emails.verification_email_verify_email")} />
|
||||
<Text>{t("emails.verification_email_click_on_this_link")}</Text>
|
||||
<Link className="break-all text-black" href={verifyLink}>
|
||||
<Text className="text-sm">{t("emails.verification_email_click_on_this_link")}</Text>
|
||||
<Link className="break-all text-sm text-black" href={verifyLink}>
|
||||
{verifyLink}
|
||||
</Link>
|
||||
<Text className="font-bold">{t("emails.verification_email_link_valid_for_24_hours")}</Text>
|
||||
<Text className="text-sm font-bold">{t("emails.verification_email_link_valid_for_24_hours")}</Text>
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
|
||||
@@ -10,7 +10,7 @@ export async function PasswordResetNotifyEmail(): Promise<React.JSX.Element> {
|
||||
<EmailTemplate t={t}>
|
||||
<Container>
|
||||
<Heading>{t("emails.password_changed_email_heading")}</Heading>
|
||||
<Text>{t("emails.password_changed_email_text")}</Text>
|
||||
<Text className="text-sm">{t("emails.password_changed_email_text")}</Text>
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
|
||||
@@ -19,16 +19,16 @@ export async function VerificationEmail({
|
||||
<EmailTemplate t={t}>
|
||||
<Container>
|
||||
<Heading>{t("emails.verification_email_heading")}</Heading>
|
||||
<Text>{t("emails.verification_email_text")}</Text>
|
||||
<Text className="text-sm">{t("emails.verification_email_text")}</Text>
|
||||
<EmailButton href={verifyLink} label={t("emails.verification_email_verify_email")} />
|
||||
<Text>{t("emails.verification_email_click_on_this_link")}</Text>
|
||||
<Link className="break-all text-black" href={verifyLink}>
|
||||
<Text className="text-sm">{t("emails.verification_email_click_on_this_link")}</Text>
|
||||
<Link className="break-all text-sm text-black" href={verifyLink}>
|
||||
{verifyLink}
|
||||
</Link>
|
||||
<Text className="font-bold">{t("emails.verification_email_link_valid_for_24_hours")}</Text>
|
||||
<Text>
|
||||
<Text className="text-sm font-bold">{t("emails.verification_email_link_valid_for_24_hours")}</Text>
|
||||
<Text className="text-sm">
|
||||
{t("emails.verification_email_if_expired_request_new_token")}
|
||||
<Link className="text-black underline" href={verificationRequestLink}>
|
||||
<Link className="text-sm text-black underline" href={verificationRequestLink}>
|
||||
{t("emails.verification_email_request_new_verification")}
|
||||
</Link>
|
||||
</Text>
|
||||
|
||||
@@ -16,10 +16,8 @@ export async function EmailCustomizationPreviewEmail({
|
||||
return (
|
||||
<EmailTemplate logoUrl={logoUrl} t={t}>
|
||||
<Container>
|
||||
<Heading className="text-xl">
|
||||
{t("emails.email_customization_preview_email_heading", { userName })}
|
||||
</Heading>
|
||||
<Text className="font-normal">{t("emails.email_customization_preview_email_text")}</Text>
|
||||
<Heading>{t("emails.email_customization_preview_email_heading", { userName })}</Heading>
|
||||
<Text className="text-sm">{t("emails.email_customization_preview_email_text")}</Text>
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
|
||||
@@ -17,10 +17,10 @@ export async function InviteAcceptedEmail({
|
||||
return (
|
||||
<EmailTemplate t={t}>
|
||||
<Container>
|
||||
<Heading className="text-xl">
|
||||
<Heading>
|
||||
{t("emails.invite_accepted_email_heading", { inviterName })} {inviterName}
|
||||
</Heading>
|
||||
<Text className="font-normal">
|
||||
<Text className="text-sm">
|
||||
{t("emails.invite_accepted_email_text_par1", { inviteeName })} {inviteeName}{" "}
|
||||
{t("emails.invite_accepted_email_text_par2")}
|
||||
</Text>
|
||||
|
||||
@@ -20,10 +20,10 @@ export async function InviteEmail({
|
||||
return (
|
||||
<EmailTemplate t={t}>
|
||||
<Container>
|
||||
<Heading className="text-xl">
|
||||
<Heading>
|
||||
{t("emails.invite_email_heading", { inviteeName })} {inviteeName}
|
||||
</Heading>
|
||||
<Text className="font-normal">
|
||||
<Text className="text-sm">
|
||||
{t("emails.invite_email_text_par1", { inviterName })} {inviterName}{" "}
|
||||
{t("emails.invite_email_text_par2")}
|
||||
</Text>
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { Container, Heading, Text } from "@react-email/components";
|
||||
import { EmailButton } from "../../components/email-button";
|
||||
import { EmailFooter } from "../../components/email-footer";
|
||||
import { EmailTemplate } from "../../components/email-template";
|
||||
|
||||
interface OnboardingInviteEmailProps {
|
||||
inviteMessage: string;
|
||||
inviterName: string;
|
||||
verifyLink: string;
|
||||
inviteeName: string;
|
||||
}
|
||||
|
||||
export async function OnboardingInviteEmail({
|
||||
inviteMessage,
|
||||
inviterName,
|
||||
verifyLink,
|
||||
inviteeName,
|
||||
}: OnboardingInviteEmailProps): Promise<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<EmailTemplate t={t}>
|
||||
<Container>
|
||||
<Heading>{t("emails.onboarding_invite_email_heading", { inviteeName })}</Heading>
|
||||
<Text>{inviteMessage}</Text>
|
||||
<Text className="font-medium">{t("emails.onboarding_invite_email_get_started_in_minutes")}</Text>
|
||||
<ol>
|
||||
<li>{t("emails.onboarding_invite_email_create_account", { inviterName })}</li>
|
||||
<li>{t("emails.onboarding_invite_email_connect_formbricks")}</li>
|
||||
<li>{t("emails.onboarding_invite_email_done")} ✅</li>
|
||||
</ol>
|
||||
<EmailButton
|
||||
href={verifyLink}
|
||||
label={t("emails.onboarding_invite_email_button_label", { inviterName })}
|
||||
/>
|
||||
<EmailFooter t={t} />
|
||||
</Container>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export default OnboardingInviteEmail;
|
||||
@@ -83,10 +83,10 @@ describe("renderEmailResponseValue", () => {
|
||||
expect(screen.getByText(expectedMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(expectedMessage)).toHaveClass(
|
||||
"mt-0",
|
||||
"font-bold",
|
||||
"break-words",
|
||||
"whitespace-pre-wrap",
|
||||
"italic"
|
||||
"italic",
|
||||
"text-sm"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -225,7 +225,7 @@ ${"This is a very long sentence that should wrap properly within the email layou
|
||||
|
||||
// Check if the text has the expected styling classes
|
||||
const textElement = screen.getByText(response);
|
||||
expect(textElement).toHaveClass("mt-0", "font-bold", "break-words", "whitespace-pre-wrap");
|
||||
expect(textElement).toHaveClass("mt-0", "break-words", "whitespace-pre-wrap", "text-sm");
|
||||
});
|
||||
|
||||
test("handles array responses in the default case by rendering them as text", async () => {
|
||||
@@ -248,7 +248,7 @@ ${"This is a very long sentence that should wrap properly within the email layou
|
||||
// Check if the text element contains all items from the response array
|
||||
const textElement = container.querySelector("p");
|
||||
expect(textElement).not.toBeNull();
|
||||
expect(textElement).toHaveClass("mt-0", "font-bold", "break-words", "whitespace-pre-wrap");
|
||||
expect(textElement).toHaveClass("mt-0", "break-words", "whitespace-pre-wrap", "text-sm");
|
||||
|
||||
// Verify each item is present in the text content
|
||||
response.forEach((item) => {
|
||||
|
||||
@@ -15,18 +15,20 @@ export const renderEmailResponseValue = async (
|
||||
return (
|
||||
<Container>
|
||||
{overrideFileUploadResponse ? (
|
||||
<Text className="mt-0 whitespace-pre-wrap break-words font-bold italic">
|
||||
<Text className="mt-0 whitespace-pre-wrap break-words text-sm italic">
|
||||
{t("emails.render_email_response_value_file_upload_response_link_not_included")}
|
||||
</Text>
|
||||
) : (
|
||||
Array.isArray(response) &&
|
||||
response.map((responseItem) => (
|
||||
<Link
|
||||
className="mt-2 flex flex-col items-center justify-center rounded-lg bg-slate-200 p-2 text-black shadow-sm"
|
||||
className="mt-2 flex flex-col items-center justify-center rounded-lg bg-slate-200 p-2 text-sm text-black shadow-sm"
|
||||
href={responseItem}
|
||||
key={responseItem}>
|
||||
<FileIcon />
|
||||
<Text className="mx-auto mb-0 truncate">{getOriginalFileNameFromUrl(responseItem)}</Text>
|
||||
<FileIcon className="h-4 w-4" />
|
||||
<Text className="mx-auto mb-0 truncate text-sm">
|
||||
{getOriginalFileNameFromUrl(responseItem)}
|
||||
</Text>
|
||||
</Link>
|
||||
))
|
||||
)}
|
||||
@@ -50,7 +52,7 @@ export const renderEmailResponseValue = async (
|
||||
case TSurveyQuestionTypeEnum.Ranking:
|
||||
return (
|
||||
<Container>
|
||||
<Row className="my-1 font-semibold text-slate-700" dir="auto">
|
||||
<Row className="mb-2 text-sm text-slate-700" dir="auto">
|
||||
{Array.isArray(response) &&
|
||||
response.map(
|
||||
(item, index) =>
|
||||
@@ -66,6 +68,6 @@ export const renderEmailResponseValue = async (
|
||||
);
|
||||
|
||||
default:
|
||||
return <Text className="mt-0 whitespace-pre-wrap break-words font-bold">{response}</Text>;
|
||||
return <Text className="mt-0 whitespace-pre-wrap break-words text-sm">{response}</Text>;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,13 +18,13 @@ export async function EmbedSurveyPreviewEmail({
|
||||
return (
|
||||
<EmailTemplate logoUrl={logoUrl} t={t}>
|
||||
<Container>
|
||||
<Heading className="text-xl">{t("emails.embed_survey_preview_email_heading")}</Heading>
|
||||
<Text className="font-normal">{t("emails.embed_survey_preview_email_text")}</Text>
|
||||
<Text className="text-sm font-normal">
|
||||
<Heading>{t("emails.embed_survey_preview_email_heading")}</Heading>
|
||||
<Text className="text-sm">{t("emails.embed_survey_preview_email_text")}</Text>
|
||||
<Text className="text-sm">
|
||||
<b>{t("emails.embed_survey_preview_email_didnt_request")}</b>{" "}
|
||||
{t("emails.embed_survey_preview_email_fight_spam")}
|
||||
</Text>
|
||||
<div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
<div className="text-sm" dangerouslySetInnerHTML={{ __html: html }} />
|
||||
<Text className="text-center text-sm text-slate-700">
|
||||
{t("emails.embed_survey_preview_email_environment_id")}: {environmentId}
|
||||
</Text>
|
||||
|
||||
@@ -20,11 +20,11 @@ export async function LinkSurveyEmail({
|
||||
return (
|
||||
<EmailTemplate logoUrl={logoUrl} t={t}>
|
||||
<Container>
|
||||
<Heading className="text-xl">{t("emails.verification_email_hey")}</Heading>
|
||||
<Text className="font-normal">{t("emails.verification_email_thanks")}</Text>
|
||||
<Text className="font-normal">{t("emails.verification_email_to_fill_survey")}</Text>
|
||||
<Heading>{t("emails.verification_email_hey")}</Heading>
|
||||
<Text className="text-sm">{t("emails.verification_email_thanks")}</Text>
|
||||
<Text className="text-sm">{t("emails.verification_email_to_fill_survey")}</Text>
|
||||
<EmailButton href={surveyLink} label={t("emails.verification_email_take_survey")} />
|
||||
<Text className="text-xs text-slate-400">
|
||||
<Text className="text-sm text-slate-400">
|
||||
{t("emails.verification_email_survey_name")}: {surveyName}
|
||||
</Text>
|
||||
<EmailFooter t={t} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getQuestionResponseMapping } from "@/lib/responses";
|
||||
import { renderEmailResponseValue } from "@/modules/email/emails/lib/utils";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { Column, Container, Hr, Link, Row, Section, Text } from "@react-email/components";
|
||||
import { Column, Container, Heading, Hr, Link, Row, Section, Text } from "@react-email/components";
|
||||
import { FileDigitIcon, FileType2Icon } from "lucide-react";
|
||||
import type { TOrganization } from "@formbricks/types/organizations";
|
||||
import type { TResponse } from "@formbricks/types/responses";
|
||||
@@ -34,8 +34,8 @@ export async function ResponseFinishedEmail({
|
||||
<Container>
|
||||
<Row>
|
||||
<Column>
|
||||
<Text className="mb-4 text-xl font-bold"> {t("emails.survey_response_finished_email_hey")}</Text>
|
||||
<Text className="mb-4 font-normal">
|
||||
<Heading> {t("emails.survey_response_finished_email_hey")}</Heading>
|
||||
<Text className="mb-4 text-sm">
|
||||
{t("emails.survey_response_finished_email_congrats", {
|
||||
surveyName: survey.name,
|
||||
})}
|
||||
@@ -45,8 +45,8 @@ export async function ResponseFinishedEmail({
|
||||
if (!question.response) return;
|
||||
return (
|
||||
<Row key={question.question}>
|
||||
<Column className="w-full">
|
||||
<Text className="mb-2 font-medium">{question.question}</Text>
|
||||
<Column className="w-full font-medium">
|
||||
<Text className="mb-2 text-sm">{question.question}</Text>
|
||||
{renderEmailResponseValue(question.response, question.type, t)}
|
||||
</Column>
|
||||
</Row>
|
||||
@@ -57,8 +57,8 @@ export async function ResponseFinishedEmail({
|
||||
if (variableResponse && ["number", "string"].includes(typeof variable)) {
|
||||
return (
|
||||
<Row key={variable.id}>
|
||||
<Column className="w-full">
|
||||
<Text className="mb-2 flex items-center gap-2 font-medium">
|
||||
<Column className="w-full text-sm font-medium">
|
||||
<Text className="mb-1 flex items-center gap-2">
|
||||
{variable.type === "number" ? (
|
||||
<FileDigitIcon className="h-4 w-4" />
|
||||
) : (
|
||||
@@ -66,7 +66,7 @@ export async function ResponseFinishedEmail({
|
||||
)}
|
||||
{variable.name}
|
||||
</Text>
|
||||
<Text className="mt-0 whitespace-pre-wrap break-words font-bold">
|
||||
<Text className="mt-0 whitespace-pre-wrap break-words font-medium">
|
||||
{variableResponse}
|
||||
</Text>
|
||||
</Column>
|
||||
@@ -80,11 +80,11 @@ export async function ResponseFinishedEmail({
|
||||
if (hiddenFieldResponse && typeof hiddenFieldResponse === "string") {
|
||||
return (
|
||||
<Row key={hiddenFieldId}>
|
||||
<Column className="w-full">
|
||||
<Text className="mb-2 flex items-center gap-2 font-medium">
|
||||
<Column className="w-full font-medium">
|
||||
<Text className="mb-2 flex items-center gap-2 text-sm">
|
||||
{hiddenFieldId} <EyeOffIcon />
|
||||
</Text>
|
||||
<Text className="mt-0 whitespace-pre-wrap break-words font-bold">
|
||||
<Text className="mt-0 whitespace-pre-wrap break-words text-sm">
|
||||
{hiddenFieldResponse}
|
||||
</Text>
|
||||
</Column>
|
||||
@@ -105,19 +105,19 @@ export async function ResponseFinishedEmail({
|
||||
/>
|
||||
<Hr />
|
||||
<Section className="mt-4 text-center text-sm">
|
||||
<Text className="font-bold">
|
||||
<Text className="text-sm font-medium">
|
||||
{t("emails.survey_response_finished_email_dont_want_notifications")}
|
||||
</Text>
|
||||
<Text className="mb-0">
|
||||
<Link
|
||||
className="text-black underline"
|
||||
className="text-sm text-black underline"
|
||||
href={`${WEBAPP_URL}/environments/${environmentId}/settings/notifications?type=alert&elementId=${survey.id}`}>
|
||||
{t("emails.survey_response_finished_email_turn_off_notifications_for_this_form")}
|
||||
</Link>
|
||||
</Text>
|
||||
<Text className="mt-0">
|
||||
<Link
|
||||
className="text-black underline"
|
||||
className="text-sm text-black underline"
|
||||
href={`${WEBAPP_URL}/environments/${environmentId}/settings/notifications?type=unsubscribedOrganizationIds&elementId=${organization.id}`}>
|
||||
{t("emails.survey_response_finished_email_turn_off_notifications_for_all_new_forms")}
|
||||
</Link>
|
||||
|
||||
@@ -16,19 +16,19 @@ export async function CreateReminderNotificationBody({
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<Container>
|
||||
<Text>
|
||||
<Text className="text-sm">
|
||||
{t("emails.weekly_summary_create_reminder_notification_body_text", {
|
||||
projectName: notificationData.projectName,
|
||||
})}
|
||||
</Text>
|
||||
<Text className="pt-4 font-bold">
|
||||
<Text className="pt-4 text-sm font-medium">
|
||||
{t("emails.weekly_summary_create_reminder_notification_body_dont_let_a_week_pass")}
|
||||
</Text>
|
||||
<EmailButton
|
||||
href={`${WEBAPP_URL}/environments/${notificationData.environmentId}/surveys?utm_source=weekly&utm_medium=email&utm_content=SetupANewSurveyCTA`}
|
||||
label={t("emails.weekly_summary_create_reminder_notification_body_setup_a_new_survey")}
|
||||
/>
|
||||
<Text className="pt-4">
|
||||
<Text className="pt-4 text-sm">
|
||||
{t("emails.weekly_summary_create_reminder_notification_body_need_help")}
|
||||
<a href="https://cal.com/johannes/15">
|
||||
{t("emails.weekly_summary_create_reminder_notification_body_cal_slot")}
|
||||
|
||||
@@ -48,7 +48,9 @@ export async function LiveSurveyNotification({
|
||||
if (surveyResponses.length === 0) {
|
||||
return (
|
||||
<Container className="mt-4">
|
||||
<Text className="m-0 font-bold">{t("emails.live_survey_notification_no_responses_yet")}</Text>
|
||||
<Text className="m-0 text-sm font-medium">
|
||||
{t("emails.live_survey_notification_no_responses_yet")}
|
||||
</Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -62,7 +64,7 @@ export async function LiveSurveyNotification({
|
||||
|
||||
surveyFields.push(
|
||||
<Container className="mt-4" key={`${index.toString()}-${surveyResponse.headline}`}>
|
||||
<Text className="m-0">{surveyResponse.headline}</Text>
|
||||
<Text className="m-0 text-sm">{surveyResponse.headline}</Text>
|
||||
{renderEmailResponseValue(surveyResponse.responseValue, surveyResponse.questionType, t)}
|
||||
</Container>
|
||||
);
|
||||
@@ -87,7 +89,7 @@ export async function LiveSurveyNotification({
|
||||
<Container className="mt-12">
|
||||
<Text className="mb-0 inline">
|
||||
<Link
|
||||
className="text-xl text-black underline"
|
||||
className="text-sm text-black underline"
|
||||
href={`${WEBAPP_URL}/environments/${environmentId}/surveys/${survey.id}/responses?utm_source=weekly&utm_medium=email&utm_content=ViewResponsesCTA`}>
|
||||
{survey.name}
|
||||
</Link>
|
||||
@@ -98,7 +100,7 @@ export async function LiveSurveyNotification({
|
||||
{displayStatus}
|
||||
</Text>
|
||||
{noResponseLastWeek ? (
|
||||
<Text>{t("emails.live_survey_notification_no_new_response")}</Text>
|
||||
<Text className="text-sm">{t("emails.live_survey_notification_no_new_response")}</Text>
|
||||
) : (
|
||||
createSurveyFields(survey.responses)
|
||||
)}
|
||||
|
||||
@@ -13,15 +13,15 @@ export async function NotificationFooter({
|
||||
return (
|
||||
<Tailwind>
|
||||
<Container className="w-full">
|
||||
<Text className="mb-0 pt-4 font-medium">{t("emails.notification_footer_all_the_best")}</Text>
|
||||
<Text className="mt-0">{t("emails.notification_footer_the_formbricks_team")}</Text>
|
||||
<Text className="mb-0 pt-4 text-sm font-medium">{t("emails.notification_footer_all_the_best")}</Text>
|
||||
<Text className="mt-0 text-sm">{t("emails.notification_footer_the_formbricks_team")}</Text>
|
||||
<Container
|
||||
className="mt-0 w-full rounded-md bg-slate-100 px-4 text-center text-xs leading-5"
|
||||
style={{ fontStyle: "italic" }}>
|
||||
<Text>
|
||||
<Text className="text-sm">
|
||||
{t("emails.notification_footer_to_halt_weekly_updates")}
|
||||
<Link
|
||||
className="text-black underline"
|
||||
className="text-sm text-black underline"
|
||||
href={`${WEBAPP_URL}/environments/${environmentId}/settings/notifications`}>
|
||||
{t("emails.notification_footer_please_turn_them_off")}
|
||||
</Link>{" "}
|
||||
|
||||
@@ -18,17 +18,17 @@ export async function NotificationHeader({
|
||||
endYear,
|
||||
}: NotificationHeaderProps): Promise<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
const getNotificationHeaderimePeriod = (): React.JSX.Element => {
|
||||
const getNotificationHeaderTimePeriod = (): React.JSX.Element => {
|
||||
if (startYear === endYear) {
|
||||
return (
|
||||
<Text className="m-0 text-right">
|
||||
<Text className="m-0 text-right text-sm">
|
||||
{startDate} - {endDate} {endYear}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text className="m-0 text-right">
|
||||
<Text className="m-0 text-right text-sm">
|
||||
{startDate} {startYear} - {endDate} {endYear}
|
||||
</Text>
|
||||
);
|
||||
@@ -40,10 +40,10 @@ export async function NotificationHeader({
|
||||
<Heading className="m-0">{t("emails.notification_header_hey")}</Heading>
|
||||
</div>
|
||||
<div className="float-right">
|
||||
<Text className="m-0 text-right font-semibold">
|
||||
<Text className="m-0 text-right text-sm font-medium">
|
||||
{t("emails.notification_header_weekly_report_for")} {projectName}
|
||||
</Text>
|
||||
{getNotificationHeaderimePeriod()}
|
||||
{getNotificationHeaderTimePeriod()}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
@@ -17,24 +17,24 @@ export async function NotificationInsight({
|
||||
<Row>
|
||||
<Column className="text-center">
|
||||
<Text className="text-sm">{t("emails.notification_insight_surveys")}</Text>
|
||||
<Text className="text-lg font-bold">{insights.numLiveSurvey}</Text>
|
||||
<Text className="text-sm font-medium">{insights.numLiveSurvey}</Text>
|
||||
</Column>
|
||||
<Column className="text-center">
|
||||
<Text className="text-sm">{t("emails.notification_insight_displays")}</Text>
|
||||
<Text className="text-lg font-bold">{insights.totalDisplays}</Text>
|
||||
<Text className="text-sm font-medium">{insights.totalDisplays}</Text>
|
||||
</Column>
|
||||
<Column className="text-center">
|
||||
<Text className="text-sm">{t("emails.notification_insight_responses")}</Text>
|
||||
<Text className="text-lg font-bold">{insights.totalResponses}</Text>
|
||||
<Text className="text-sm font-medium">{insights.totalResponses}</Text>
|
||||
</Column>
|
||||
<Column className="text-center">
|
||||
<Text className="text-sm">{t("emails.notification_insight_completed")}</Text>
|
||||
<Text className="text-lg font-bold">{insights.totalCompletedResponses}</Text>
|
||||
<Text className="text-sm font-medium">{insights.totalCompletedResponses}</Text>
|
||||
</Column>
|
||||
{insights.totalDisplays !== 0 ? (
|
||||
<Column className="text-center">
|
||||
<Text className="text-sm">{t("emails.notification_insight_completion_rate")}</Text>
|
||||
<Text className="text-lg font-bold">{Math.round(insights.completionRate)}%</Text>
|
||||
<Text className="text-sm font-medium">{Math.round(insights.completionRate)}%</Text>
|
||||
</Column>
|
||||
) : (
|
||||
""
|
||||
|
||||
@@ -32,7 +32,6 @@ import { PasswordResetNotifyEmail } from "./emails/auth/password-reset-notify-em
|
||||
import { VerificationEmail } from "./emails/auth/verification-email";
|
||||
import { InviteAcceptedEmail } from "./emails/invite/invite-accepted-email";
|
||||
import { InviteEmail } from "./emails/invite/invite-email";
|
||||
import { OnboardingInviteEmail } from "./emails/invite/onboarding-invite-email";
|
||||
import { EmbedSurveyPreviewEmail } from "./emails/survey/embed-survey-preview-email";
|
||||
import { LinkSurveyEmail } from "./emails/survey/link-survey-email";
|
||||
import { ResponseFinishedEmail } from "./emails/survey/response-finished-email";
|
||||
@@ -166,9 +165,7 @@ export const sendInviteMemberEmail = async (
|
||||
inviteId: string,
|
||||
email: string,
|
||||
inviterName: string,
|
||||
inviteeName: string,
|
||||
isOnboardingInvite?: boolean,
|
||||
inviteMessage?: string
|
||||
inviteeName: string
|
||||
): Promise<boolean> => {
|
||||
const token = createInviteToken(inviteId, email, {
|
||||
expiresIn: "7d",
|
||||
@@ -177,26 +174,12 @@ export const sendInviteMemberEmail = async (
|
||||
|
||||
const verifyLink = `${WEBAPP_URL}/invite?token=${encodeURIComponent(token)}`;
|
||||
|
||||
if (isOnboardingInvite && inviteMessage) {
|
||||
const html = await render(
|
||||
await OnboardingInviteEmail({ verifyLink, inviteMessage, inviterName, inviteeName })
|
||||
);
|
||||
return await sendEmail({
|
||||
to: email,
|
||||
subject: t("emails.onboarding_invite_email_subject", {
|
||||
inviterName,
|
||||
}),
|
||||
html,
|
||||
});
|
||||
} else {
|
||||
const t = await getTranslate();
|
||||
const html = await render(await InviteEmail({ inviteeName, inviterName, verifyLink }));
|
||||
return await sendEmail({
|
||||
to: email,
|
||||
subject: t("emails.invite_member_email_subject"),
|
||||
html,
|
||||
});
|
||||
}
|
||||
const html = await render(await InviteEmail({ inviteeName, inviterName, verifyLink }));
|
||||
return await sendEmail({
|
||||
to: email,
|
||||
subject: t("emails.invite_member_email_subject"),
|
||||
html,
|
||||
});
|
||||
};
|
||||
|
||||
export const sendInviteAcceptedEmail = async (
|
||||
|
||||
@@ -188,9 +188,7 @@ export const resendInviteAction = authenticatedActionClient.schema(ZResendInvite
|
||||
parsedInput.inviteId,
|
||||
updatedInvite.email,
|
||||
invite?.creator?.name ?? "",
|
||||
updatedInvite.name ?? "",
|
||||
undefined,
|
||||
ctx.user.locale
|
||||
updatedInvite.name ?? ""
|
||||
);
|
||||
return updatedInvite;
|
||||
}
|
||||
@@ -266,14 +264,7 @@ export const inviteUserAction = authenticatedActionClient.schema(ZInviteUserActi
|
||||
};
|
||||
|
||||
if (inviteId) {
|
||||
await sendInviteMemberEmail(
|
||||
inviteId,
|
||||
parsedInput.email,
|
||||
ctx.user.name ?? "",
|
||||
parsedInput.name ?? "",
|
||||
false,
|
||||
undefined
|
||||
);
|
||||
await sendInviteMemberEmail(inviteId, parsedInput.email, ctx.user.name ?? "", parsedInput.name ?? "");
|
||||
}
|
||||
|
||||
return inviteId;
|
||||
|
||||
@@ -57,14 +57,7 @@ export const inviteOrganizationMemberAction = authenticatedActionClient
|
||||
currentUserId: ctx.user.id,
|
||||
});
|
||||
|
||||
await sendInviteMemberEmail(
|
||||
invitedUserId,
|
||||
parsedInput.email,
|
||||
ctx.user.name,
|
||||
"",
|
||||
false, // is onboarding invite
|
||||
undefined
|
||||
);
|
||||
await sendInviteMemberEmail(invitedUserId, parsedInput.email, ctx.user.name, "");
|
||||
|
||||
ctx.auditLoggingCtx.inviteId = invitedUserId;
|
||||
ctx.auditLoggingCtx.newObject = {
|
||||
|
||||
@@ -76,8 +76,8 @@ export async function FollowUpEmail(props: FollowUpEmailProps): Promise<React.JS
|
||||
if (!question.response) return;
|
||||
return (
|
||||
<Row key={question.question}>
|
||||
<Column className="w-full">
|
||||
<Text className="mb-2 font-medium">{question.question}</Text>
|
||||
<Column className="w-full font-medium">
|
||||
<Text className="mb-2 text-sm">{question.question}</Text>
|
||||
{renderEmailResponseValue(question.response, question.type, t, true)}
|
||||
</Column>
|
||||
</Row>
|
||||
@@ -89,22 +89,22 @@ export async function FollowUpEmail(props: FollowUpEmailProps): Promise<React.JS
|
||||
{isDefaultLogo ? (
|
||||
<Section className="mt-4 text-center text-sm">
|
||||
<Link
|
||||
className="m-0 font-normal text-slate-500"
|
||||
className="m-0 text-sm text-slate-500"
|
||||
href="https://formbricks.com/?utm_source=email_header&utm_medium=email"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
{t("emails.email_template_text_1")}
|
||||
</Link>
|
||||
{IMPRINT_ADDRESS && (
|
||||
<Text className="m-0 font-normal text-slate-500 opacity-50">{IMPRINT_ADDRESS}</Text>
|
||||
<Text className="m-0 text-sm text-slate-500 opacity-50">{IMPRINT_ADDRESS}</Text>
|
||||
)}
|
||||
<Text className="m-0 font-normal text-slate-500 opacity-50">
|
||||
<Text className="m-0 text-sm text-slate-500 opacity-50">
|
||||
{IMPRINT_URL && (
|
||||
<Link
|
||||
href={IMPRINT_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-slate-500">
|
||||
className="text-sm text-slate-500">
|
||||
{t("emails.imprint")}
|
||||
</Link>
|
||||
)}
|
||||
@@ -114,7 +114,7 @@ export async function FollowUpEmail(props: FollowUpEmailProps): Promise<React.JS
|
||||
href={PRIVACY_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-slate-500">
|
||||
className="text-sm text-slate-500">
|
||||
{t("emails.privacy_policy")}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user