diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.test.tsx index bee0c45150..161c6eaafe 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.test.tsx @@ -1,348 +1,314 @@ -import { ShareSurveyModal } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal"; -import { cleanup, render, screen } from "@testing-library/react"; +import { ShareViewType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/types/share"; +import "@testing-library/jest-dom/vitest"; +import { cleanup, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { LucideIcon } from "lucide-react"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { - TSurvey, - TSurveyQuestion, - TSurveyQuestionTypeEnum, - TSurveySingleUse, -} from "@formbricks/types/surveys/types"; +import { TSegment } from "@formbricks/types/segment"; +import { TSurvey } from "@formbricks/types/surveys/types"; import { TUser } from "@formbricks/types/user"; +import { ShareSurveyModal } from "./share-survey-modal"; -// Mock data -const mockSurveyWeb = { - id: "survey1", - name: "Web Survey", - environmentId: "env1", - type: "app", - status: "inProgress", - questions: [ - { - id: "q1", - type: TSurveyQuestionTypeEnum.OpenText, - headline: { default: "Q1" }, - required: true, - } as unknown as TSurveyQuestion, - ], - displayOption: "displayOnce", - recontactDays: 0, - autoClose: null, - delay: 0, - autoComplete: null, - runOnDate: null, - closeOnDate: null, - singleUse: { enabled: false, isEncrypted: false } as TSurveySingleUse, - triggers: [], - createdAt: new Date(), - updatedAt: new Date(), - languages: [], - styling: null, -} as unknown as TSurvey; - -vi.mock("@/lib/constants", () => ({ - INTERCOM_SECRET_KEY: "test-secret-key", - IS_INTERCOM_CONFIGURED: true, - INTERCOM_APP_ID: "test-app-id", - ENCRYPTION_KEY: "test-encryption-key", - ENTERPRISE_LICENSE_KEY: "test-enterprise-license-key", - GITHUB_ID: "test-github-id", - GITHUB_SECRET: "test-githubID", - GOOGLE_CLIENT_ID: "test-google-client-id", - GOOGLE_CLIENT_SECRET: "test-google-client-secret", - AZUREAD_CLIENT_ID: "test-azuread-client-id", - AZUREAD_CLIENT_SECRET: "test-azure", - AZUREAD_TENANT_ID: "test-azuread-tenant-id", - OIDC_DISPLAY_NAME: "test-oidc-display-name", - OIDC_CLIENT_ID: "test-oidc-client-id", - OIDC_ISSUER: "test-oidc-issuer", - OIDC_CLIENT_SECRET: "test-oidc-client-secret", - OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", - WEBAPP_URL: "test-webapp-url", - IS_POSTHOG_CONFIGURED: true, - POSTHOG_API_HOST: "test-posthog-api-host", - POSTHOG_API_KEY: "test-posthog-api-key", - FORMBRICKS_ENVIRONMENT_ID: "mock-formbricks-environment-id", - IS_FORMBRICKS_ENABLED: true, - SESSION_MAX_AGE: 1000, - REDIS_URL: "test-redis-url", - AUDIT_LOG_ENABLED: true, - IS_FORMBRICKS_CLOUD: false, +// Mock child components to simplify testing +vi.mock("./shareEmbedModal/share-view", () => ({ + ShareView: ({ tabs, activeId, setActiveId, survey }: any) => ( +
+
{survey.type}
+
{activeId}
+ {tabs.map((tab: any) => ( + + ))} +
+ ), })); -const mockSurveyLink = { - ...mockSurveyWeb, - id: "survey2", - name: "Link Survey", - type: "link", - singleUse: { enabled: false, isEncrypted: false } as TSurveySingleUse, -} as unknown as TSurvey; +vi.mock("./shareEmbedModal/success-view", () => ({ + SuccessView: ({ survey, handleViewChange, handleEmbedViewWithTab }: any) => ( +
+
{survey.id}
+ + +
+ ), +})); + +// Mock tab components +vi.mock("./shareEmbedModal/anonymous-links-tab", () => ({ + AnonymousLinksTab: () =>
Anonymous Links Tab
, +})); + +vi.mock("./shareEmbedModal/qr-code-tab", () => ({ + QRCodeTab: () =>
QR Code Tab
, +})); + +vi.mock("./shareEmbedModal/personal-links-tab", () => ({ + PersonalLinksTab: () =>
Personal Links Tab
, +})); + +vi.mock("./shareEmbedModal/email-tab", () => ({ + EmailTab: () =>
Email Tab
, +})); + +vi.mock("./shareEmbedModal/website-embed-tab", () => ({ + WebsiteEmbedTab: () =>
Website Embed Tab
, +})); + +vi.mock("./shareEmbedModal/social-media-tab", () => ({ + SocialMediaTab: () =>
Social Media Tab
, +})); + +vi.mock("./shareEmbedModal/dynamic-popup-tab", () => ({ + DynamicPopupTab: () =>
Dynamic Popup Tab
, +})); + +vi.mock("./shareEmbedModal/app-tab", () => ({ + AppTab: () =>
App Tab
, +})); + +// Mock analysis utils +vi.mock("@/modules/analysis/utils", () => ({ + getSurveyUrl: vi.fn((survey, publicDomain, type) => `${publicDomain}/${survey.id}?type=${type}`), +})); const mockUser = { - id: "user1", - name: "Test User", + id: "user-123", email: "test@example.com", - role: "project_manager", - objective: "other", - createdAt: new Date(), - updatedAt: new Date(), + name: "Test User", locale: "en-US", -} as unknown as TUser; +} as TUser; -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (str: string) => str, - }), -})); +const mockSegments: TSegment[] = [ + { + id: "segment-1", + title: "Test Segment", + description: "Test segment description", + environmentId: "env-123", + filters: [], + isPrivate: false, + surveys: [], + createdAt: new Date(), + updatedAt: new Date(), + }, +]; -vi.mock("@/modules/analysis/components/ShareSurveyLink", () => ({ - ShareSurveyLink: vi.fn(() =>
ShareSurveyLinkMock
), -})); +const mockLinkSurvey = { + id: "survey-123", + name: "Test Link Survey", + type: "link", + environmentId: "env-123", + status: "draft", +} as TSurvey; -vi.mock("@/modules/ui/components/badge", () => ({ - Badge: vi.fn(({ text }) => {text}), -})); - -const mockShareViewComponent = vi.fn(); -vi.mock("./shareEmbedModal/share-view", () => ({ - ShareView: (props: any) => mockShareViewComponent(props), -})); - -// Mock getSurveyUrl to return a predictable URL -vi.mock("@/modules/analysis/utils", () => ({ - getSurveyUrl: vi.fn().mockResolvedValue("https://public-domain.com/s/survey1"), -})); - -let capturedDialogOnOpenChange: ((open: boolean) => void) | undefined; -vi.mock("@/modules/ui/components/dialog", async () => { - const actual = await vi.importActual( - "@/modules/ui/components/dialog" - ); - return { - ...actual, - Dialog: (props: React.ComponentProps) => { - capturedDialogOnOpenChange = props.onOpenChange; - return ; - }, - }; -}); - -describe("ShareEmbedSurvey", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - capturedDialogOnOpenChange = undefined; - }); - - const mockSetOpen = vi.fn(); +const mockAppSurvey = { + id: "app-survey-123", + name: "Test App Survey", + type: "app", + environmentId: "env-123", + status: "draft", +} as TSurvey; +describe("ShareSurveyModal", () => { const defaultProps = { - survey: mockSurveyWeb, - publicDomain: "https://public-domain.com", + publicDomain: "https://formbricks.com", open: true, - modalView: "start" as "start" | "share", - setOpen: mockSetOpen, + modalView: "start" as const, + setOpen: vi.fn(), user: mockUser, - segments: [], + segments: mockSegments, isContactsEnabled: true, isFormbricksCloud: true, }; beforeEach(() => { - mockShareViewComponent.mockImplementation( - ({ tabs, activeId, survey, email, surveyUrl, publicDomain, locale }) => ( -
-
{JSON.stringify(tabs)}
-
{activeId}
-
{survey.id}
-
{email}
-
{surveyUrl}
-
{publicDomain}
-
{locale}
-
- ) - ); + vi.clearAllMocks(); }); - test("renders initial 'start' view correctly when open and modalView is 'start' for link survey", () => { - render(); - expect(screen.getByText("environments.surveys.summary.your_survey_is_public 🎉")).toBeInTheDocument(); - expect(screen.getByText("ShareSurveyLinkMock")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.summary.whats_next")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.summary.share_survey")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.summary.configure_alerts")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.summary.setup_integrations")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.summary.use_personal_links")).toBeInTheDocument(); - expect(screen.getByTestId("badge-mock")).toHaveTextContent("common.new"); - }); - - test("renders initial 'start' view correctly when open and modalView is 'start' for app survey", () => { - render(); - // For app surveys, ShareSurveyLink should not be rendered - expect(screen.queryByText("ShareSurveyLinkMock")).not.toBeInTheDocument(); - expect(screen.getByText("environments.surveys.summary.whats_next")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.summary.share_survey")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.summary.configure_alerts")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.summary.setup_integrations")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.summary.use_personal_links")).toBeInTheDocument(); - expect(screen.getByTestId("badge-mock")).toHaveTextContent("common.new"); - }); - - test("switches to 'embed' view when 'Embed survey' button is clicked", async () => { - render(); - const embedButton = screen.getByText("environments.surveys.summary.share_survey"); - await userEvent.click(embedButton); - expect(mockShareViewComponent).toHaveBeenCalled(); - expect(screen.getByTestId("shareview-tabs")).toBeInTheDocument(); - }); - - test("handleOpenChange (when Dialog calls its onOpenChange prop)", () => { - render(); - expect(capturedDialogOnOpenChange).toBeDefined(); - - // Simulate Dialog closing - if (capturedDialogOnOpenChange) capturedDialogOnOpenChange(false); - expect(mockSetOpen).toHaveBeenCalledWith(false); - - // Simulate Dialog opening - mockSetOpen.mockClear(); - if (capturedDialogOnOpenChange) capturedDialogOnOpenChange(true); - expect(mockSetOpen).toHaveBeenCalledWith(true); - }); - - test("correctly configures for 'anon-links' survey type in embed view", () => { - render(); - const embedViewProps = vi.mocked(mockShareViewComponent).mock.calls[0][0] as { - tabs: { id: string; label: string; icon: LucideIcon }[]; - activeId: string; - }; - expect(embedViewProps.tabs.length).toBe(6); - expect(embedViewProps.tabs.find((tab) => tab.id === "app")).toBeUndefined(); - expect(embedViewProps.tabs.find((tab) => tab.id === "dynamic-popup")).toBeDefined(); - expect(embedViewProps.tabs.find((tab) => tab.id === "website-embed")).toBeDefined(); - expect(embedViewProps.tabs[0].id).toBe("anon-links"); - expect(embedViewProps.tabs[1].id).toBe("qr-code"); - expect(embedViewProps.tabs[2].id).toBe("personal-links"); - expect(embedViewProps.tabs[3].id).toBe("email"); - expect(embedViewProps.tabs[4].id).toBe("website-embed"); - expect(embedViewProps.activeId).toBe("anon-links"); - }); - - test("correctly configures for 'web' survey type in embed view", () => { - render(); - const embedViewProps = vi.mocked(mockShareViewComponent).mock.calls[0][0] as { - tabs: { id: string; label: string; icon: LucideIcon }[]; - activeId: string; - }; - expect(embedViewProps.tabs.length).toBe(1); - expect(embedViewProps.tabs.find((tab) => tab.id === "app")).toBeDefined(); - expect(embedViewProps.tabs.find((tab) => tab.id === "website-embed")).toBeUndefined(); - expect(embedViewProps.tabs.find((tab) => tab.id === "dynamic-popup")).toBeUndefined(); - expect(embedViewProps.activeId).toBe("app"); - }); - - test("useEffect does not change activeId if survey.type changes from web to link (while in embed view)", () => { - const { rerender } = render( - - ); - expect(vi.mocked(mockShareViewComponent).mock.calls[0][0].activeId).toBe("app"); - - rerender(); - expect(vi.mocked(mockShareViewComponent).mock.calls[1][0].activeId).toBe("app"); // Current behavior - }); - - test("initial showView is set by modalView prop when open is true", () => { - render(); - expect(mockShareViewComponent).toHaveBeenCalled(); - expect(screen.getByTestId("shareview-tabs")).toBeInTheDocument(); + afterEach(() => { cleanup(); - - render(); - // Start view shows the share survey button - expect(screen.getByText("environments.surveys.summary.share_survey")).toBeInTheDocument(); }); - test("useEffect sets showView to 'start' when open becomes false", () => { - const { rerender } = render(); - expect(screen.getByTestId("shareview-tabs")).toBeInTheDocument(); // Starts in embed + describe("Modal rendering and basic functionality", () => { + test("renders modal when open is true", () => { + render(); - rerender(); - // Dialog mock returns null when open is false, so EmbedViewMockContent is not found - expect(screen.queryByTestId("shareview-tabs")).not.toBeInTheDocument(); + expect(screen.getByTestId("success-view")).toBeInTheDocument(); + }); + + test("does not render modal content when open is false", () => { + render(); + + expect(screen.queryByTestId("success-view")).not.toBeInTheDocument(); + expect(screen.queryByTestId("share-view")).not.toBeInTheDocument(); + }); + + test("calls setOpen when modal is closed", async () => { + const mockSetOpen = vi.fn(); + render(); + + // Simulate modal close by pressing escape + await userEvent.keyboard("{Escape}"); + + await waitFor(() => { + expect(mockSetOpen).toHaveBeenCalledWith(false); + }); + }); }); - test("renders correct label for link tab based on singleUse survey property", () => { - render(); - let embedViewProps = vi.mocked(mockShareViewComponent).mock.calls[0][0] as { - tabs: { id: string; label: string }[]; - }; - let linkTab = embedViewProps.tabs.find((tab) => tab.id === "anon-links"); - expect(linkTab?.label).toBe("environments.surveys.share.anonymous_links.nav_title"); - cleanup(); - vi.mocked(mockShareViewComponent).mockClear(); + describe("View switching functionality", () => { + test("starts with SuccessView when modalView is 'start'", () => { + render(); - const mockSurveyLinkSingleUse: TSurvey = { - ...mockSurveyLink, - singleUse: { enabled: true, isEncrypted: true }, - }; - render(); - embedViewProps = vi.mocked(mockShareViewComponent).mock.calls[0][0] as { - tabs: { id: string; label: string }[]; - }; - linkTab = embedViewProps.tabs.find((tab) => tab.id === "anon-links"); - expect(linkTab?.label).toBe("environments.surveys.share.anonymous_links.nav_title"); + expect(screen.getByTestId("success-view")).toBeInTheDocument(); + expect(screen.queryByTestId("share-view")).not.toBeInTheDocument(); + }); + + test("starts with ShareView when modalView is 'share'", () => { + render(); + + expect(screen.getByTestId("share-view")).toBeInTheDocument(); + expect(screen.queryByTestId("success-view")).not.toBeInTheDocument(); + }); + + test("switches from SuccessView to ShareView when button is clicked", async () => { + render(); + + expect(screen.getByTestId("success-view")).toBeInTheDocument(); + + const changeViewButton = screen.getByTestId("change-to-share-view"); + await userEvent.click(changeViewButton); + + await waitFor(() => { + expect(screen.getByTestId("share-view")).toBeInTheDocument(); + expect(screen.queryByTestId("success-view")).not.toBeInTheDocument(); + }); + }); + + test("switches to ShareView with specific tab when handleEmbedViewWithTab is called", async () => { + render(); + + const embedButton = screen.getByTestId("embed-with-tab"); + await userEvent.click(embedButton); + + await waitFor(() => { + expect(screen.getByTestId("share-view")).toBeInTheDocument(); + expect(screen.getByTestId("active-tab")).toHaveTextContent("email"); + }); + }); }); - test("includes QR code tab for link surveys", () => { - render(); - const embedViewProps = vi.mocked(mockShareViewComponent).mock.calls[0][0] as { - tabs: { id: string; label: string }[]; - }; - const qrCodeTab = embedViewProps.tabs.find((tab) => tab.id === "qr-code"); - expect(qrCodeTab).toBeDefined(); - expect(qrCodeTab?.label).toBe("environments.surveys.summary.qr_code"); + describe("Survey type specific behavior", () => { + test("displays link survey tabs for link type survey", () => { + render(); + + expect(screen.getByTestId("survey-type")).toHaveTextContent("link"); + expect(screen.getByTestId("tab-anon-links")).toBeInTheDocument(); + expect(screen.getByTestId("tab-personal-links")).toBeInTheDocument(); + expect(screen.getByTestId("tab-website-embed")).toBeInTheDocument(); + expect(screen.getByTestId("tab-email")).toBeInTheDocument(); + expect(screen.getByTestId("tab-social-media")).toBeInTheDocument(); + expect(screen.getByTestId("tab-qr-code")).toBeInTheDocument(); + expect(screen.getByTestId("tab-dynamic-popup")).toBeInTheDocument(); + }); + + test("displays app survey tabs for app type survey", () => { + render(); + + expect(screen.getByTestId("survey-type")).toHaveTextContent("app"); + expect(screen.getByTestId("tab-app")).toBeInTheDocument(); + + // Link-specific tabs should not be present for app surveys + expect(screen.queryByTestId("tab-anonymous_links")).not.toBeInTheDocument(); + expect(screen.queryByTestId("tab-personal_links")).not.toBeInTheDocument(); + }); + + test("sets correct default active tab based on survey type", () => { + const linkSurveyRender = render( + + ); + + expect(screen.getByTestId("active-tab")).toHaveTextContent(ShareViewType.ANON_LINKS); + + linkSurveyRender.unmount(); + + render(); + + expect(screen.getByTestId("active-tab")).toHaveTextContent(ShareViewType.APP); + }); }); - test("does not include QR code tab for app surveys", () => { - render(); - const embedViewProps = vi.mocked(mockShareViewComponent).mock.calls[0][0] as { - tabs: { id: string; label: string }[]; - }; - const qrCodeTab = embedViewProps.tabs.find((tab) => tab.id === "qr-code"); - expect(qrCodeTab).toBeUndefined(); + describe("Tab switching functionality", () => { + test("switches active tab when tab button is clicked", async () => { + render(); + + expect(screen.getByTestId("active-tab")).toHaveTextContent(ShareViewType.ANON_LINKS); + + const emailTab = screen.getByTestId("tab-email"); + await userEvent.click(emailTab); + + await waitFor(() => { + expect(screen.getByTestId("active-tab")).toHaveTextContent(ShareViewType.EMAIL); + }); + }); }); - test("dynamic popup tab is only visible for link surveys", () => { - // Test link survey includes dynamic popup tab - render(); - let embedViewProps = vi.mocked(mockShareViewComponent).mock.calls[0][0] as { - tabs: { id: string; label: string }[]; - }; - expect(embedViewProps.tabs.find((tab) => tab.id === "dynamic-popup")).toBeDefined(); - cleanup(); - vi.mocked(mockShareViewComponent).mockClear(); + describe("Props passing", () => { + test("passes correct props to SuccessView", () => { + render(); - // Test web survey excludes dynamic popup tab - render(); - embedViewProps = vi.mocked(mockShareViewComponent).mock.calls[0][0] as { - tabs: { id: string; label: string }[]; - }; - expect(embedViewProps.tabs.find((tab) => tab.id === "dynamic-popup")).toBeUndefined(); + expect(screen.getByTestId("survey-id")).toHaveTextContent(mockLinkSurvey.id); + }); + + test("passes correct props to ShareView", () => { + render(); + + expect(screen.getByTestId("survey-type")).toHaveTextContent(mockLinkSurvey.type); + expect(screen.getByTestId("active-tab")).toHaveTextContent(ShareViewType.ANON_LINKS); + }); }); - render(); - const embedViewProps = vi.mocked(mockShareViewComponent).mock.calls[0][0] as { - tabs: { id: string; label: string }[]; - }; - test("QR code tab appears after link tab in the tabs array", () => { - const linkTabIndex = embedViewProps.tabs.findIndex((tab) => tab.id === "anon-links"); - const qrCodeTabIndex = embedViewProps.tabs.findIndex((tab) => tab.id === "qr-code"); - expect(qrCodeTabIndex).toBe(linkTabIndex + 1); + describe("URL handling", () => { + test("initializes survey URL correctly", async () => { + const { getSurveyUrl } = await import("@/modules/analysis/utils"); + const getSurveyUrlMock = vi.mocked(getSurveyUrl); + + render(); + + expect(getSurveyUrlMock).toHaveBeenCalledWith(mockLinkSurvey, defaultProps.publicDomain, "default"); + }); }); - test("website-embed and dynamic-popup tabs replace old webpage tab", () => { - expect(embedViewProps.tabs.find((tab) => tab.id === "webpage")).toBeUndefined(); - expect(embedViewProps.tabs.find((tab) => tab.id === "website-embed")).toBeDefined(); - expect(embedViewProps.tabs.find((tab) => tab.id === "dynamic-popup")).toBeDefined(); + + describe("Effect handling", () => { + test("updates showView when modalView prop changes", async () => { + const { rerender } = render( + + ); + + expect(screen.getByTestId("success-view")).toBeInTheDocument(); + + rerender(); + + await waitFor(() => { + expect(screen.getByTestId("share-view")).toBeInTheDocument(); + }); + }); + + test("updates showView when open prop changes", async () => { + const { rerender } = render( + + ); + + expect(screen.queryByTestId("success-view")).not.toBeInTheDocument(); + + rerender(); + + await waitFor(() => { + expect(screen.getByTestId("success-view")).toBeInTheDocument(); + }); + }); }); }); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.tsx index cc5ce05634..1e0b1c46ba 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.tsx @@ -1,5 +1,13 @@ "use client"; +import { AnonymousLinksTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/anonymous-links-tab"; +import { AppTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab"; +import { DynamicPopupTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab"; +import { EmailTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/email-tab"; +import { PersonalLinksTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab"; +import { QRCodeTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab"; +import { SocialMediaTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab"; +import { WebsiteEmbedTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/website-embed-tab"; import { ShareViewType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/types/share"; import { getSurveyUrl } from "@/modules/analysis/utils"; import { Dialog, DialogContent, DialogTitle } from "@/modules/ui/components/dialog"; @@ -10,6 +18,7 @@ import { LinkIcon, MailIcon, QrCodeIcon, + Share2Icon, SmartphoneIcon, SquareStack, UserIcon, @@ -47,42 +56,109 @@ export const ShareSurveyModal = ({ isFormbricksCloud, }: ShareSurveyModalProps) => { const environmentId = survey.environmentId; + const [surveyUrl, setSurveyUrl] = useState(getSurveyUrl(survey, publicDomain, "default")); + const [showView, setShowView] = useState(modalView); const { email } = user; const { t } = useTranslate(); - const linkTabs: { id: ShareViewType; label: string; icon: React.ElementType }[] = useMemo( + const linkTabs: { + id: ShareViewType; + label: string; + icon: React.ElementType; + title: string; + description: string; + componentType: React.ComponentType; + componentProps: any; + }[] = useMemo( () => [ { id: ShareViewType.ANON_LINKS, label: t("environments.surveys.share.anonymous_links.nav_title"), icon: LinkIcon, - }, - { - id: ShareViewType.QR_CODE, - label: t("environments.surveys.summary.qr_code"), - icon: QrCodeIcon, + title: t("environments.surveys.share.anonymous_links.nav_title"), + description: t("environments.surveys.share.anonymous_links.description"), + componentType: AnonymousLinksTab, + componentProps: { + survey, + publicDomain, + setSurveyUrl, + locale: user.locale, + surveyUrl, + }, }, { id: ShareViewType.PERSONAL_LINKS, label: t("environments.surveys.share.personal_links.nav_title"), icon: UserIcon, - }, - { - id: ShareViewType.EMAIL, - label: t("environments.surveys.share.send_email.nav_title"), - icon: MailIcon, + title: t("environments.surveys.share.personal_links.nav_title"), + description: t("environments.surveys.share.personal_links.description"), + componentType: PersonalLinksTab, + componentProps: { + environmentId, + surveyId: survey.id, + segments, + isContactsEnabled, + isFormbricksCloud, + }, }, { id: ShareViewType.WEBSITE_EMBED, label: t("environments.surveys.share.embed_on_website.nav_title"), icon: Code2Icon, + title: t("environments.surveys.share.embed_on_website.nav_title"), + description: t("environments.surveys.share.embed_on_website.description"), + componentType: WebsiteEmbedTab, + componentProps: { surveyUrl }, + }, + { + id: ShareViewType.EMAIL, + label: t("environments.surveys.share.send_email.nav_title"), + icon: MailIcon, + title: t("environments.surveys.share.send_email.nav_title"), + description: t("environments.surveys.share.send_email.description"), + componentType: EmailTab, + componentProps: { surveyId: survey.id, email }, + }, + { + id: ShareViewType.SOCIAL_MEDIA, + label: t("environments.surveys.share.social_media.title"), + icon: Share2Icon, + title: t("environments.surveys.share.social_media.title"), + description: t("environments.surveys.share.social_media.description"), + componentType: SocialMediaTab, + componentProps: { surveyUrl, surveyTitle: survey.name }, + }, + { + id: ShareViewType.QR_CODE, + label: t("environments.surveys.summary.qr_code"), + icon: QrCodeIcon, + title: t("environments.surveys.summary.qr_code"), + description: t("environments.surveys.summary.qr_code_description"), + componentType: QRCodeTab, + componentProps: { surveyUrl }, }, { id: ShareViewType.DYNAMIC_POPUP, label: t("environments.surveys.share.dynamic_popup.nav_title"), icon: SquareStack, + title: t("environments.surveys.share.dynamic_popup.nav_title"), + description: t("environments.surveys.share.dynamic_popup.description"), + componentType: DynamicPopupTab, + componentProps: { environmentId, surveyId: survey.id }, }, ], - [t] + [ + t, + survey, + publicDomain, + setSurveyUrl, + user.locale, + surveyUrl, + environmentId, + segments, + isContactsEnabled, + isFormbricksCloud, + email, + ] ); const appTabs = [ @@ -90,6 +166,9 @@ export const ShareSurveyModal = ({ id: ShareViewType.APP, label: t("environments.surveys.share.embed_on_website.embed_in_app"), icon: SmartphoneIcon, + title: t("environments.surveys.share.embed_on_website.embed_in_app"), + componentType: AppTab, + componentProps: {}, }, ]; @@ -97,9 +176,6 @@ export const ShareSurveyModal = ({ survey.type === "link" ? ShareViewType.ANON_LINKS : ShareViewType.APP ); - const [surveyUrl, setSurveyUrl] = useState(() => getSurveyUrl(survey, publicDomain, "default")); - const [showView, setShowView] = useState(modalView); - useEffect(() => { if (open) { setShowView(modalView); @@ -148,17 +224,8 @@ export const ShareSurveyModal = ({ )} diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/anonymous-links-tab.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/anonymous-links-tab.test.tsx index d4ea467b46..d1f7414071 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/anonymous-links-tab.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/anonymous-links-tab.test.tsx @@ -192,14 +192,6 @@ describe("AnonymousLinksTab", () => { cleanup(); }); - test("renders with multi-use link enabled by default", () => { - render(); - - expect(screen.getByTestId("tab-container")).toBeInTheDocument(); - expect(screen.getByTestId("toggle-multi-use-link-switch")).toHaveAttribute("data-checked", "true"); - expect(screen.getByTestId("toggle-single-use-link-switch")).toHaveAttribute("data-checked", "false"); - }); - test("renders with single-use link enabled when survey has singleUse enabled", () => { render(); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/anonymous-links-tab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/anonymous-links-tab.tsx index 135cd41975..335c964ca0 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/anonymous-links-tab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/anonymous-links-tab.tsx @@ -3,7 +3,6 @@ import { updateSingleUseLinksAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions"; import { DisableLinkModal } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/disable-link-modal"; import { DocumentationLinks } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/documentation-links"; -import { TabContainer } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container"; import { ShareSurveyLink } from "@/modules/analysis/components/ShareSurveyLink"; import { getSurveyUrl } from "@/modules/analysis/utils"; import { generateSingleUseIdsAction } from "@/modules/survey/list/actions"; @@ -205,9 +204,7 @@ export const AnonymousLinksTab = ({ }; return ( - + <>
)} - + ); }; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab.tsx index 3d72b38aef..7d824216a9 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab.tsx @@ -11,7 +11,7 @@ export const AppTab = () => { const [selectedTab, setSelectedTab] = useState("webapp"); return ( -
+
{ expect(link).toHaveTextContent("environments.surveys.share.dynamic_popup.alert_button"); }); - test("renders title with correct text", () => { - render(); - - const h3 = screen.getByTestId("h3"); - expect(h3).toBeInTheDocument(); - expect(h3).toHaveTextContent("environments.surveys.share.dynamic_popup.title"); - }); - test("renders attribute-based targeting documentation button", () => { render(); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab.tsx index 95c8910fc6..0d95846afc 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab.tsx @@ -1,7 +1,6 @@ "use client"; import { DocumentationLinks } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/documentation-links"; -import { TabContainer } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container"; import { Alert, AlertButton, AlertDescription, AlertTitle } from "@/modules/ui/components/alert"; import { useTranslate } from "@tolgee/react"; import Link from "next/link"; @@ -15,39 +14,33 @@ export const DynamicPopupTab = ({ environmentId, surveyId }: DynamicPopupTabProp const { t } = useTranslate(); return ( - -
- - {t("environments.surveys.share.dynamic_popup.alert_title")} - - {t("environments.surveys.share.dynamic_popup.alert_description")} - - - - {t("environments.surveys.share.dynamic_popup.alert_button")} - - - +
+ + {t("environments.surveys.share.dynamic_popup.alert_title")} + {t("environments.surveys.share.dynamic_popup.alert_description")} + + + {t("environments.surveys.share.dynamic_popup.alert_button")} + + + - -
- + +
); }; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/email-tab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/email-tab.tsx index bf744c0c8b..4ccd6cbbe6 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/email-tab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/email-tab.tsx @@ -1,6 +1,5 @@ "use client"; -import { TabContainer } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { Button } from "@/modules/ui/components/button"; import { CodeBlock } from "@/modules/ui/components/code-block"; @@ -142,19 +141,15 @@ export const EmailTab = ({ surveyId, email }: EmailTabProps) => { }; return ( - -
- -
{renderTabContent()}
-
-
+
+ +
{renderTabContent()}
+
); }; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab.test.tsx index 790bf73ab7..e7e0ad8d83 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab.test.tsx @@ -192,13 +192,6 @@ describe("PersonalLinksTab", () => { cleanup(); }); - test("renders the component with correct title and description", () => { - render(); - - expect(screen.getByText("environments.surveys.share.personal_links.title")).toBeInTheDocument(); - expect(screen.getByText("environments.surveys.share.personal_links.description")).toBeInTheDocument(); - }); - test("renders recipients section with segment selection", () => { render(); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab.tsx index c15c7140e8..85d2b53aae 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab.tsx @@ -27,7 +27,6 @@ import { useForm } from "react-hook-form"; import toast from "react-hot-toast"; import { TSegment } from "@formbricks/types/segment"; import { generatePersonalLinksAction } from "../../actions"; -import { TabContainer } from "./tab-container"; interface PersonalLinksTabProps { environmentId: string; @@ -169,86 +168,82 @@ export const PersonalLinksTab = ({ return ( - -
- {/* Recipients Section */} - ( - - {t("common.recipients")} - - - - - {t("environments.surveys.share.personal_links.create_and_manage_segments")} - - - )} - /> - - {/* Expiry Date Section */} - ( - - {t("environments.surveys.share.personal_links.expiry_date_optional")} - - - - - {t("environments.surveys.share.personal_links.expiry_date_description")} - - - )} - /> - - {/* Generate Button */} - -
-
- - {/* Info Box */} - + {/* Recipients Section */} + ( + + {t("common.recipients")} + + + + + {t("environments.surveys.share.personal_links.create_and_manage_segments")} + + + )} /> -
+ + {/* Expiry Date Section */} + ( + + {t("environments.surveys.share.personal_links.expiry_date_optional")} + + + + + {t("environments.surveys.share.personal_links.expiry_date_description")} + + + )} + /> + + {/* Generate Button */} + +
+
+ + {/* Info Box */} + ); }; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab.test.tsx index 05c0264951..c8492f5f42 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab.test.tsx @@ -157,30 +157,6 @@ describe("QRCodeTab", () => { cleanup(); }); - describe("Component rendering", () => { - test("renders component with title and description", () => { - render(); - - expect( - screen.getByText("environments.surveys.summary.make_survey_accessible_via_qr_code") - ).toBeInTheDocument(); - expect( - screen.getByText("environments.surveys.summary.responses_collected_via_qr_code_are_anonymous") - ).toBeInTheDocument(); - }); - - test("renders without QR code when surveyUrl is empty", () => { - render(); - - expect( - screen.getByText("environments.surveys.summary.make_survey_accessible_via_qr_code") - ).toBeInTheDocument(); - expect( - screen.getByText("environments.surveys.summary.responses_collected_via_qr_code_are_anonymous") - ).toBeInTheDocument(); - }); - }); - describe("QR Code generation", () => { test("attempts to generate QR code when surveyUrl is provided", async () => { render(); @@ -256,12 +232,6 @@ describe("QRCodeTab", () => { test("shows appropriate state when surveyUrl is empty", async () => { render(); - // Component should render some content - await waitFor(() => { - const content = screen.getByText("environments.surveys.summary.make_survey_accessible_via_qr_code"); - expect(content).toBeInTheDocument(); - }); - // Should show button (but disabled) when URL is empty, no alert const button = screen.getByTestId("button"); expect(button).toBeInTheDocument(); @@ -287,20 +257,6 @@ describe("QRCodeTab", () => { expect(screen.getByTestId("button")).toBeInTheDocument(); }); }); - - test("handles empty surveyUrl gracefully", async () => { - render(); - - // Component should render basic content even with empty URL - await waitFor(() => { - const title = screen.getByText("environments.surveys.summary.make_survey_accessible_via_qr_code"); - const description = screen.getByText( - "environments.surveys.summary.responses_collected_via_qr_code_are_anonymous" - ); - expect(title).toBeInTheDocument(); - expect(description).toBeInTheDocument(); - }); - }); }); describe("Accessibility", () => { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab.tsx index fe639384ab..8589f81b60 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab.tsx @@ -1,6 +1,5 @@ "use client"; -import { TabContainer } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container"; import { getQRCodeOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/get-qr-code-options"; import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert"; import { Button } from "@/modules/ui/components/button"; @@ -75,46 +74,42 @@ export const QRCodeTab = ({ surveyUrl }: QRCodeTabProps) => { }; return ( -
- - {isLoading && ( -
- -

{t("environments.surveys.summary.generating_qr_code")}

-
- )} + <> + {isLoading && ( +
+ +

{t("environments.surveys.summary.generating_qr_code")}

+
+ )} - {hasError && ( - - {t("common.something_went_wrong")} - {t("environments.surveys.summary.qr_code_generation_failed")} - - )} + {hasError && ( + + {t("common.something_went_wrong")} + {t("environments.surveys.summary.qr_code_generation_failed")} + + )} - {!isLoading && !hasError && ( -
-
-
-
- + {!isLoading && !hasError && ( +
+
+
- )} - -
+ +
+ )} + ); }; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/share-view.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/share-view.test.tsx index a2370ea583..3b86a33f1a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/share-view.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/share-view.test.tsx @@ -1,11 +1,56 @@ import { cleanup, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { afterEach, describe, expect, test, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { TSurvey } from "@formbricks/types/surveys/types"; import { TUserLocale } from "@formbricks/types/user"; import { ShareViewType } from "../../types/share"; import { ShareView } from "./share-view"; +// Mock sidebar components +vi.mock("@/modules/ui/components/sidebar", () => ({ + SidebarProvider: ({ children, open, className, style }: any) => ( +
+ {children} +
+ ), + Sidebar: ({ children }: { children: React.ReactNode }) =>
{children}
, + SidebarContent: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + SidebarGroup: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + SidebarGroupContent: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + SidebarGroupLabel: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + SidebarMenu: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + SidebarMenuItem: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + SidebarMenuButton: ({ + children, + onClick, + tooltip, + className, + isActive, + }: { + children: React.ReactNode; + onClick: () => void; + tooltip: string; + className?: string; + isActive?: boolean; + }) => ( + + ), +})); + // Mock child components vi.mock("./app-tab", () => ({ AppTab: () =>
AppTab Content
, @@ -79,13 +124,6 @@ vi.mock("@/modules/ui/components/upgrade-prompt", () => ({ ), })); -// Mock @tolgee/react -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: (key: string) => key, - }), -})); - // Mock lucide-react vi.mock("lucide-react", () => ({ CopyIcon: () =>
CopyIcon
, @@ -118,33 +156,6 @@ vi.mock("lucide-react", () => ({ ), })); -// Mock sidebar components -vi.mock("@/modules/ui/components/sidebar", () => ({ - SidebarProvider: ({ children }: { children: React.ReactNode }) =>
{children}
, - Sidebar: ({ children }: { children: React.ReactNode }) =>
{children}
, - SidebarContent: ({ children }: { children: React.ReactNode }) =>
{children}
, - SidebarGroup: ({ children }: { children: React.ReactNode }) =>
{children}
, - SidebarGroupContent: ({ children }: { children: React.ReactNode }) =>
{children}
, - SidebarGroupLabel: ({ children }: { children: React.ReactNode }) =>
{children}
, - SidebarMenu: ({ children }: { children: React.ReactNode }) =>
{children}
, - SidebarMenuItem: ({ children }: { children: React.ReactNode }) =>
{children}
, - SidebarMenuButton: ({ - children, - onClick, - tooltip, - className, - }: { - children: React.ReactNode; - onClick: () => void; - tooltip: string; - className?: string; - }) => ( - - ), -})); - // Mock tooltip and typography components vi.mock("@/modules/ui/components/tooltip", () => ({ TooltipRenderer: ({ children }: { children: React.ReactNode }) =>
{children}
, @@ -178,21 +189,69 @@ vi.mock("@/lib/cn", () => ({ cn: (...args: any[]) => args.filter(Boolean).join(" "), })); -const mockTabs: Array<{ id: ShareViewType; label: string; icon: React.ElementType }> = [ - { id: ShareViewType.EMAIL, label: "Email", icon: () =>
}, +const mockTabs: Array<{ + id: ShareViewType; + label: string; + icon: React.ElementType; + componentType: React.ComponentType; + componentProps: any; + title: string; + description?: string; +}> = [ + { + id: ShareViewType.EMAIL, + label: "Email", + icon: () =>
, + componentType: () =>
Email Content
, + componentProps: {}, + title: "Email", + description: "Email Description", + }, { id: ShareViewType.WEBSITE_EMBED, label: "Website Embed", icon: () =>
, + componentType: () =>
Website Embed Content
, + componentProps: {}, + title: "Website Embed", + description: "Website Embed Description", }, { id: ShareViewType.DYNAMIC_POPUP, label: "Dynamic Popup", icon: () =>
, + componentType: () =>
Dynamic Popup Content
, + componentProps: {}, + title: "Dynamic Popup", + description: "Dynamic Popup Description", + }, + { + id: ShareViewType.ANON_LINKS, + label: "Anonymous Links", + icon: () =>
, + componentType: () =>
Anonymous Links Content
, + componentProps: {}, + title: "Anonymous Links", + description: "Anonymous Links Description", + }, + { + id: ShareViewType.QR_CODE, + label: "QR Code", + icon: () =>
, + componentType: () =>
QR Code Content
, + componentProps: {}, + title: "QR Code", + description: "QR Code Description", + }, + { + id: ShareViewType.APP, + label: "App", + icon: () =>
, + componentType: () =>
App Content
, + componentProps: {}, + title: "App", + description: "App Description", }, - { id: ShareViewType.ANON_LINKS, label: "Anonymous Links", icon: () =>
}, - { id: ShareViewType.QR_CODE, label: "QR Code", icon: () =>
}, - { id: ShareViewType.APP, label: "App", icon: () =>
}, ]; const mockSurveyLink = { @@ -254,7 +313,20 @@ const defaultProps = { isFormbricksCloud: false, }; +// Mock window object for resize testing +Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 1024, +}); + describe("ShareView", () => { + beforeEach(() => { + // Reset window size to default before each test + window.innerWidth = 1024; + vi.clearAllMocks(); + }); + afterEach(() => { cleanup(); vi.clearAllMocks(); @@ -285,58 +357,6 @@ describe("ShareView", () => { expect(defaultProps.setActiveId).toHaveBeenCalledWith(ShareViewType.WEBSITE_EMBED); }); - test("renders EmailTab when activeId is 'email'", () => { - render(); - expect(screen.getByTestId("email-tab")).toBeInTheDocument(); - expect( - screen.getByText(`EmailTab Content for ${defaultProps.survey.id} with ${defaultProps.email}`) - ).toBeInTheDocument(); - }); - - test("renders WebsiteEmbedTab when activeId is 'website-embed'", () => { - render(); - expect(screen.getByTestId("website-embed-tab")).toBeInTheDocument(); - expect(screen.getByText(`WebsiteEmbedTab Content for ${defaultProps.surveyUrl}`)).toBeInTheDocument(); - }); - - test("renders DynamicPopupTab when activeId is 'dynamic-popup'", () => { - render(); - expect(screen.getByTestId("dynamic-popup-tab")).toBeInTheDocument(); - expect( - screen.getByText( - `DynamicPopupTab Content for ${defaultProps.survey.id} in ${defaultProps.environmentId}` - ) - ).toBeInTheDocument(); - }); - - test("renders AnonymousLinksTab when activeId is 'anon-links'", () => { - render(); - expect(screen.getByTestId("anonymous-links-tab")).toBeInTheDocument(); - expect( - screen.getByText(`AnonymousLinksTab Content for ${defaultProps.survey.id} at ${defaultProps.surveyUrl}`) - ).toBeInTheDocument(); - }); - - test("renders QRCodeTab when activeId is 'qr-code'", () => { - render(); - expect(screen.getByTestId("qr-code-tab")).toBeInTheDocument(); - }); - - test("renders AppTab when activeId is 'app'", () => { - render(); - expect(screen.getByTestId("app-tab")).toBeInTheDocument(); - }); - - test("renders PersonalLinksTab when activeId is 'personal-links'", () => { - render(); - expect(screen.getByTestId("personal-links-tab")).toBeInTheDocument(); - expect( - screen.getByText( - `PersonalLinksTab Content for ${defaultProps.survey.id} in ${defaultProps.environmentId}` - ) - ).toBeInTheDocument(); - }); - test("calls setActiveId when a responsive tab is clicked", async () => { render(); @@ -400,4 +420,269 @@ describe("ShareView", () => { ); } }); + + describe("Responsive Behavior", () => { + test("detects large screen size on mount", () => { + window.innerWidth = 1200; + render(); + + // SidebarProvider should be rendered with open=true for large screens + const sidebarProvider = screen.getByTestId("sidebar-provider"); + expect(sidebarProvider).toHaveAttribute("data-open", "true"); + }); + + test("detects small screen size on mount", () => { + window.innerWidth = 800; + render(); + + // SidebarProvider should be rendered with open=false for small screens + const sidebarProvider = screen.getByTestId("sidebar-provider"); + expect(sidebarProvider).toHaveAttribute("data-open", "false"); + }); + + test("updates screen size on window resize", async () => { + window.innerWidth = 1200; + const { rerender } = render(); + + // Initially large screen + let sidebarProvider = screen.getByTestId("sidebar-provider"); + expect(sidebarProvider).toHaveAttribute("data-open", "true"); + + // Simulate window resize to small screen + window.innerWidth = 800; + window.dispatchEvent(new Event("resize")); + + // Force re-render to trigger useEffect + rerender(); + + // Should now be small screen + sidebarProvider = screen.getByTestId("sidebar-provider"); + expect(sidebarProvider).toHaveAttribute("data-open", "false"); + }); + + test("cleans up resize listener on unmount", () => { + const removeEventListenerSpy = vi.spyOn(window, "removeEventListener"); + const { unmount } = render(); + + unmount(); + + expect(removeEventListenerSpy).toHaveBeenCalledWith("resize", expect.any(Function)); + }); + }); + + describe("TabContainer Integration", () => { + test("renders active tab with correct title and description", () => { + render(); + + const tabContainer = screen.getByTestId("tab-container"); + expect(tabContainer).toBeInTheDocument(); + + const tabTitle = screen.getByTestId("tab-title"); + expect(tabTitle).toHaveTextContent("Email"); + + const tabDescription = screen.getByTestId("tab-description"); + expect(tabDescription).toHaveTextContent("Email Description"); + + const tabContent = screen.getByTestId("email-tab-content"); + expect(tabContent).toBeInTheDocument(); + }); + + test("renders different tab when activeId changes", () => { + const { rerender } = render(); + + // Initially shows Email tab + expect(screen.getByTestId("tab-title")).toHaveTextContent("Email"); + expect(screen.getByTestId("email-tab-content")).toBeInTheDocument(); + + // Change to Website Embed tab + rerender(); + + expect(screen.getByTestId("tab-title")).toHaveTextContent("Website Embed"); + expect(screen.getByTestId("website-embed-tab-content")).toBeInTheDocument(); + expect(screen.queryByTestId("email-tab-content")).not.toBeInTheDocument(); + }); + + test("handles tab without description", () => { + const tabsWithoutDescription = [ + { + id: ShareViewType.EMAIL, + label: "Email", + icon: () =>
, + componentType: () =>
Email Content
, + componentProps: {}, + title: "Email", + // No description property + }, + ]; + + render(); + + const tabDescription = screen.getByTestId("tab-description"); + expect(tabDescription).toHaveTextContent(""); + }); + + test("returns null when no active tab is found", () => { + const emptyTabs: typeof mockTabs = []; + + render(); + + const tabContainer = screen.queryByTestId("tab-container"); + expect(tabContainer).not.toBeInTheDocument(); + }); + }); + + describe("SidebarProvider Configuration", () => { + test("renders SidebarProvider with correct props for link surveys", () => { + render(); + + const sidebarProvider = screen.getByTestId("sidebar-provider"); + expect(sidebarProvider).toBeInTheDocument(); + expect(sidebarProvider).toHaveAttribute("data-open", "true"); + expect(sidebarProvider).toHaveClass("flex min-h-0 w-auto lg:col-span-1"); + expect(sidebarProvider).toHaveStyle("--sidebar-width: 100%"); + }); + + test("does not render SidebarProvider for non-link surveys", () => { + render(); + + expect(screen.queryByTestId("sidebar-provider")).not.toBeInTheDocument(); + }); + + test("renders correct grid layout for link surveys", () => { + render(); + + const container = screen.getByTestId("sidebar-provider").parentElement; + expect(container).toHaveClass("lg:grid lg:grid-cols-4"); + }); + + test("does not render grid layout for non-link surveys", () => { + const { container } = render(); + + const mainDiv = container.querySelector(".h-full > div"); + expect(mainDiv).not.toHaveClass("lg:grid lg:grid-cols-4"); + }); + }); + + describe("Sidebar Menu Buttons", () => { + test("renders SidebarMenuButton with correct isActive prop", () => { + render(); + + const emailButton = screen.getByLabelText("Email"); + expect(emailButton).toHaveAttribute("data-active", "true"); + + const websiteEmbedButton = screen.getByLabelText("Website Embed"); + expect(websiteEmbedButton).toHaveAttribute("data-active", "false"); + }); + + test("renders all tabs in sidebar menu", () => { + render(); + + mockTabs.forEach((tab) => { + const button = screen.getByLabelText(tab.label); + expect(button).toBeInTheDocument(); + expect(button).toHaveAttribute("data-active", tab.id === ShareViewType.EMAIL ? "true" : "false"); + }); + }); + }); + + describe("Mobile Responsive Buttons", () => { + test("renders mobile buttons for all tabs", () => { + render(); + + // Mobile buttons should be present for all tabs + mockTabs.forEach((tab) => { + // Map ShareViewType to actual testid used in the component + const testIdMap: Record = { + [ShareViewType.ANON_LINKS]: "link-tab-icon", + [ShareViewType.PERSONAL_LINKS]: "personal-links-tab-icon", + [ShareViewType.WEBSITE_EMBED]: "website-embed-tab-icon", + [ShareViewType.EMAIL]: "email-tab-icon", + [ShareViewType.SOCIAL_MEDIA]: "social-media-tab-icon", + [ShareViewType.QR_CODE]: "qr-code-tab-icon", + [ShareViewType.DYNAMIC_POPUP]: "dynamic-popup-tab-icon", + [ShareViewType.APP]: "app-tab-icon", + }; + + const expectedTestId = testIdMap[tab.id] || `${tab.id}-tab-icon`; + const mobileButtons = screen.getAllByTestId(expectedTestId); + const mobileButton = mobileButtons.find((icon) => { + const button = icon.closest("button"); + return button && button.getAttribute("data-variant") === "ghost"; + }); + expect(mobileButton).toBeInTheDocument(); + }); + }); + + test("applies correct classes to mobile buttons based on active state", () => { + render(); + + const websiteEmbedIcons = screen.getAllByTestId("website-embed-tab-icon"); + const activeMobileButton = websiteEmbedIcons + .find((icon) => { + const button = icon.closest("button"); + return button && button.getAttribute("data-variant") === "ghost"; + }) + ?.closest("button"); + + if (activeMobileButton) { + expect(activeMobileButton).toHaveClass("bg-white text-slate-900 shadow-sm hover:bg-white"); + } + }); + }); + + describe("Content Area Layout", () => { + test("applies correct column span for link surveys", () => { + const { container } = render(); + + const contentArea = container.querySelector('[class*="lg:col-span-3"]'); + expect(contentArea).toBeInTheDocument(); + expect(contentArea).toHaveClass("lg:col-span-3"); + }); + + test("does not apply column span for non-link surveys", () => { + const { container } = render(); + + const contentArea = container.querySelector('[class*="lg:col-span-3"]'); + expect(contentArea).toBeNull(); + }); + + test("renders mobile button container with correct visibility class", () => { + const { container } = render(); + + const mobileButtonContainer = container.querySelector(".md\\:hidden"); + expect(mobileButtonContainer).toBeInTheDocument(); + expect(mobileButtonContainer).toHaveClass("md:hidden"); + }); + }); + + describe("Enhanced Tab Structure", () => { + test("handles tabs with all required properties", () => { + const completeTab = { + id: ShareViewType.EMAIL, + label: "Test Email", + icon: () =>
, + componentType: () =>
Test Content
, + componentProps: {}, + title: "Test Title", + description: "Test Description", + }; + + render(); + + expect(screen.getByTestId("tab-title")).toHaveTextContent("Test Title"); + expect(screen.getByTestId("tab-description")).toHaveTextContent("Test Description"); + expect(screen.getByTestId("test-content")).toBeInTheDocument(); + }); + + test("uses title from tab definition in TabContainer", () => { + const customTitleTab = { + ...mockTabs[0], + title: "Custom Email Title", + }; + + render(); + + expect(screen.getByTestId("tab-title")).toHaveTextContent("Custom Email Title"); + }); + }); }); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/share-view.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/share-view.tsx index 58b8bb320f..c6abec1f6a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/share-view.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/share-view.tsx @@ -1,7 +1,6 @@ "use client"; -import { DynamicPopupTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab"; -import { QRCodeTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab"; +import { TabContainer } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container"; import { ShareViewType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/types/share"; import { cn } from "@/lib/cn"; import { Button } from "@/modules/ui/components/button"; @@ -20,46 +19,24 @@ import { TooltipRenderer } from "@/modules/ui/components/tooltip"; import { Small } from "@/modules/ui/components/typography"; import { useTranslate } from "@tolgee/react"; import { useEffect, useState } from "react"; -import { TSegment } from "@formbricks/types/segment"; import { TSurvey } from "@formbricks/types/surveys/types"; -import { TUserLocale } from "@formbricks/types/user"; -import { AnonymousLinksTab } from "./anonymous-links-tab"; -import { AppTab } from "./app-tab"; -import { EmailTab } from "./email-tab"; -import { PersonalLinksTab } from "./personal-links-tab"; -import { WebsiteEmbedTab } from "./website-embed-tab"; interface ShareViewProps { - tabs: Array<{ id: ShareViewType; label: string; icon: React.ElementType }>; + tabs: Array<{ + id: ShareViewType; + label: string; + icon: React.ElementType; + componentType: React.ComponentType; + componentProps: any; + title: string; + description?: string; + }>; activeId: ShareViewType; setActiveId: React.Dispatch>; - environmentId: string; survey: TSurvey; - email: string; - surveyUrl: string; - publicDomain: string; - setSurveyUrl: React.Dispatch>; - locale: TUserLocale; - segments: TSegment[]; - isContactsEnabled: boolean; - isFormbricksCloud: boolean; } -export const ShareView = ({ - tabs, - activeId, - setActiveId, - environmentId, - survey, - email, - surveyUrl, - publicDomain, - setSurveyUrl, - locale, - segments, - isContactsEnabled, - isFormbricksCloud, -}: ShareViewProps) => { +export const ShareView = ({ tabs, activeId, setActiveId, survey }: ShareViewProps) => { const { t } = useTranslate(); const [isLargeScreen, setIsLargeScreen] = useState(true); @@ -76,40 +53,16 @@ export const ShareView = ({ }, []); const renderActiveTab = () => { - switch (activeId) { - case ShareViewType.EMAIL: - return ; - case ShareViewType.WEBSITE_EMBED: - return ; - case ShareViewType.DYNAMIC_POPUP: - return ; - case ShareViewType.ANON_LINKS: - return ( - - ); - case ShareViewType.APP: - return ; - case ShareViewType.QR_CODE: - return ; - case ShareViewType.PERSONAL_LINKS: - return ( - - ); - default: - return null; - } + const activeTab = tabs.find((tab) => tab.id === activeId); + if (!activeTab) return null; + + const { componentType: Component, componentProps } = activeTab; + + return ( + + + + ); }; return ( diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab.test.tsx new file mode 100644 index 0000000000..aad6e879d2 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab.test.tsx @@ -0,0 +1,138 @@ +import "@testing-library/jest-dom/vitest"; +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { SocialMediaTab } from "./social-media-tab"; + +// Mock next/link +vi.mock("next/link", () => ({ + default: ({ href, children, ...props }: any) => ( + + {children} + + ), +})); + +// Mock window.open +Object.defineProperty(window, "open", { + writable: true, + value: vi.fn(), +}); + +const mockSurveyUrl = "https://app.formbricks.com/s/survey1"; +const mockSurveyTitle = "Test Survey"; + +const expectedPlatforms = [ + { name: "LinkedIn", description: "Share on LinkedIn" }, + { name: "Threads", description: "Share on Threads" }, + { name: "Facebook", description: "Share on Facebook" }, + { name: "Reddit", description: "Share on Reddit" }, + { name: "X", description: "Share on X (formerly Twitter)" }, +]; + +describe("SocialMediaTab", () => { + afterEach(() => { + cleanup(); + vi.clearAllMocks(); + }); + + test("renders all social media platforms with correct names", () => { + render(); + + expectedPlatforms.forEach((platform) => { + expect(screen.getByText(platform.name)).toBeInTheDocument(); + }); + }); + + test("renders source tracking alert with correct content", () => { + render(); + + expect( + screen.getByText("environments.surveys.share.social_media.source_tracking_enabled") + ).toBeInTheDocument(); + expect( + screen.getByText("environments.surveys.share.social_media.source_tracking_enabled_alert_description") + ).toBeInTheDocument(); + expect(screen.getByText("common.learn_more")).toBeInTheDocument(); + + const learnMoreButton = screen.getByRole("button", { name: "common.learn_more" }); + expect(learnMoreButton).toBeInTheDocument(); + }); + + test("renders platform buttons for all platforms", () => { + render(); + + const platformButtons = expectedPlatforms.map((platform) => + screen.getByRole("button", { name: new RegExp(platform.name, "i") }) + ); + expect(platformButtons).toHaveLength(expectedPlatforms.length); + }); + + test("opens sharing window when LinkedIn button is clicked", async () => { + const mockWindowOpen = vi.spyOn(window, "open"); + render(); + + const linkedInButton = screen.getByRole("button", { name: /linkedin/i }); + await userEvent.click(linkedInButton); + + expect(mockWindowOpen).toHaveBeenCalledWith( + expect.stringContaining("linkedin.com/shareArticle"), + "share-dialog", + "width=1024,height=768,location=no,toolbar=no,status=no,menubar=no,scrollbars=yes,resizable=yes,noopener=yes,noreferrer=yes" + ); + }); + + test("includes source tracking in shared URLs", async () => { + const mockWindowOpen = vi.spyOn(window, "open"); + render(); + + const linkedInButton = screen.getByRole("button", { name: /linkedin/i }); + await userEvent.click(linkedInButton); + + const calledUrl = mockWindowOpen.mock.calls[0][0] as string; + const decodedUrl = decodeURIComponent(calledUrl); + expect(decodedUrl).toContain("source=linkedin"); + }); + + test("opens sharing window when Facebook button is clicked", async () => { + const mockWindowOpen = vi.spyOn(window, "open"); + render(); + + const facebookButton = screen.getByRole("button", { name: /facebook/i }); + await userEvent.click(facebookButton); + + expect(mockWindowOpen).toHaveBeenCalledWith( + expect.stringContaining("facebook.com/sharer"), + "share-dialog", + "width=1024,height=768,location=no,toolbar=no,status=no,menubar=no,scrollbars=yes,resizable=yes,noopener=yes,noreferrer=yes" + ); + }); + + test("opens sharing window when X button is clicked", async () => { + const mockWindowOpen = vi.spyOn(window, "open"); + render(); + + const xButton = screen.getByRole("button", { name: /^x$/i }); + await userEvent.click(xButton); + + expect(mockWindowOpen).toHaveBeenCalledWith( + expect.stringContaining("twitter.com/intent/tweet"), + "share-dialog", + "width=1024,height=768,location=no,toolbar=no,status=no,menubar=no,scrollbars=yes,resizable=yes,noopener=yes,noreferrer=yes" + ); + }); + + test("encodes URLs and titles correctly for sharing", async () => { + const specialCharUrl = "https://app.formbricks.com/s/survey1?param=test&other=value"; + const specialCharTitle = "Test Survey & More"; + const mockWindowOpen = vi.spyOn(window, "open"); + + render(); + + const linkedInButton = screen.getByRole("button", { name: /linkedin/i }); + await userEvent.click(linkedInButton); + + const calledUrl = mockWindowOpen.mock.calls[0][0] as string; + expect(calledUrl).toContain(encodeURIComponent(specialCharTitle)); + }); +}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab.tsx new file mode 100644 index 0000000000..d64f3ca367 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { Alert, AlertButton, AlertDescription, AlertTitle } from "@/modules/ui/components/alert"; +import { Button } from "@/modules/ui/components/button"; +import { FacebookIcon } from "@/modules/ui/components/icons/facebook-icon"; +import { LinkedinIcon } from "@/modules/ui/components/icons/linkedin-icon"; +import { RedditIcon } from "@/modules/ui/components/icons/reddit-icon"; +import { ThreadsIcon } from "@/modules/ui/components/icons/threads-icon"; +import { XIcon } from "@/modules/ui/components/icons/x-icon"; +import { useTranslate } from "@tolgee/react"; +import { AlertCircleIcon } from "lucide-react"; +import { useMemo } from "react"; + +interface SocialMediaTabProps { + surveyUrl: string; + surveyTitle: string; +} + +export const SocialMediaTab: React.FC = ({ surveyUrl, surveyTitle }) => { + const { t } = useTranslate(); + + const socialMediaPlatforms = useMemo(() => { + const shareText = surveyTitle; + + // Add source tracking to the survey URL + const getTrackedUrl = (platform: string) => { + const sourceParam = `source=${platform.toLowerCase()}`; + const separator = surveyUrl.includes("?") ? "&" : "?"; + return `${surveyUrl}${separator}${sourceParam}`; + }; + + return [ + { + id: "linkedin", + name: "LinkedIn", + icon: , + url: `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(getTrackedUrl("linkedin"))}&title=${encodeURIComponent(shareText)}`, + description: "Share on LinkedIn", + }, + { + id: "threads", + name: "Threads", + icon: , + url: `https://www.threads.net/intent/post?text=${encodeURIComponent(shareText)}%20${encodeURIComponent(getTrackedUrl("threads"))}`, + description: "Share on Threads", + }, + { + id: "facebook", + name: "Facebook", + icon: , + url: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(getTrackedUrl("facebook"))}`, + description: "Share on Facebook", + }, + { + id: "reddit", + name: "Reddit", + icon: , + url: `https://www.reddit.com/submit?url=${encodeURIComponent(getTrackedUrl("reddit"))}&title=${encodeURIComponent(shareText)}`, + description: "Share on Reddit", + }, + { + id: "x", + name: "X", + icon: , + url: `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(getTrackedUrl("x"))}`, + description: "Share on X (formerly Twitter)", + }, + ]; + }, [surveyUrl, surveyTitle]); + + const handleSocialShare = (url: string) => { + // Open sharing window + window.open( + url, + "share-dialog", + "width=1024,height=768,location=no,toolbar=no,status=no,menubar=no,scrollbars=yes,resizable=yes,noopener=yes,noreferrer=yes" + ); + }; + + return ( + <> +
+ {socialMediaPlatforms.map((platform) => ( + + ))} +
+ + + + {t("environments.surveys.share.social_media.source_tracking_enabled")} + + {t("environments.surveys.share.social_media.source_tracking_enabled_alert_description")} + + { + window.open( + "https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/source-tracking", + "_blank", + "noopener,noreferrer" + ); + }}> + {t("common.learn_more")} + + + + ); +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/website-embed-tab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/website-embed-tab.tsx index 33b42f76c7..54b9757879 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/website-embed-tab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/website-embed-tab.tsx @@ -7,7 +7,6 @@ import { useTranslate } from "@tolgee/react"; import { CopyIcon } from "lucide-react"; import { useState } from "react"; import toast from "react-hot-toast"; -import { TabContainer } from "./tab-container"; interface WebsiteEmbedTabProps { surveyUrl: string; @@ -25,9 +24,7 @@ export const WebsiteEmbedTab = ({ surveyUrl }: WebsiteEmbedTabProps) => {
`; return ( - + <> {iframeCode} @@ -50,6 +47,6 @@ export const WebsiteEmbedTab = ({ surveyUrl }: WebsiteEmbedTabProps) => { {t("common.copy_code")} - + ); }; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/types/share.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/types/share.ts index 7691f13742..94bccb0b7d 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/types/share.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/types/share.ts @@ -6,5 +6,6 @@ export enum ShareViewType { APP = "app", WEBSITE_EMBED = "website-embed", DYNAMIC_POPUP = "dynamic-popup", + SOCIAL_MEDIA = "social-media", QR_CODE = "qr-code", } diff --git a/apps/web/lib/responses.test.ts b/apps/web/lib/responses.test.ts index d534f8c46c..9c32e5ed2f 100644 --- a/apps/web/lib/responses.test.ts +++ b/apps/web/lib/responses.test.ts @@ -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 + }); }); }); diff --git a/apps/web/lib/responses.ts b/apps/web/lib/responses.ts index e5e4f7e9f7..e8760e1377 100644 --- a/apps/web/lib/responses.ts +++ b/apps/web/lib/responses.ts @@ -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 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), diff --git a/apps/web/locales/de-DE.json b/apps/web/locales/de-DE.json index 7a5b48e922..65318f1080 100644 --- a/apps/web/locales/de-DE.json +++ b/apps/web/locales/de-DE.json @@ -1799,7 +1799,14 @@ "send_preview_email": "Vorschau-E-Mail senden", "title": "Binden Sie Ihre Umfrage in eine E-Mail ein" }, - "share_view_title": "Teilen über" + "share_view_title": "Teilen über", + "social_media": { + "description": "Erhalte Rückmeldungen von deinen Kontakten auf verschiedenen sozialen Medien.", + "share_your_survey_on_social_media": "Teilen Sie Ihre Umfrage in sozialen Medien", + "source_tracking_enabled": "Quellenverfolgung aktiviert", + "source_tracking_enabled_alert_description": "Wenn Sie aus diesem Dialogfenster teilen, wird das soziale Netzwerk an den Umfragelink angehängt, sodass Sie wissen, welche Antworten über welches Netzwerk eingegangen sind.", + "title": "Soziale Medien" + } }, "summary": { "added_filter_for_responses_where_answer_to_question": "Filter hinzugefügt für Antworten, bei denen die Antwort auf Frage {questionIdx} {filterComboBoxValue} - {filterValue} ist", @@ -1849,6 +1856,7 @@ "publish_to_web_warning": "Du bist dabei, diese Umfrageergebnisse öffentlich zugänglich zu machen.", "publish_to_web_warning_description": "Deine Umfrageergebnisse werden öffentlich sein. Jeder außerhalb deiner Organisation kann darauf zugreifen, wenn er den Link hat.", "qr_code": "QR-Code", + "qr_code_description": "Antworten, die per QR-Code gesammelt werden, sind anonym.", "qr_code_download_failed": "QR-Code-Download fehlgeschlagen", "qr_code_download_with_start_soon": "QR Code-Download startet bald", "qr_code_generation_failed": "Es gab ein Problem beim Laden des QR-Codes für die Umfrage. Bitte versuchen Sie es erneut.", diff --git a/apps/web/locales/en-US.json b/apps/web/locales/en-US.json index 0e30c60adb..b9c2d7313d 100644 --- a/apps/web/locales/en-US.json +++ b/apps/web/locales/en-US.json @@ -1799,7 +1799,14 @@ "send_preview_email": "Send preview email", "title": "Embed your survey in an email" }, - "share_view_title": "Share via" + "share_view_title": "Share via", + "social_media": { + "description": "Get responses from your contacts on various social media networks.", + "share_your_survey_on_social_media": "Share your survey on social media", + "source_tracking_enabled": "Source tracking enabled", + "source_tracking_enabled_alert_description": "When sharing from this dialog, the social media network will be appended to the survey link so you know which responses came via each network.", + "title": "Social media" + } }, "summary": { "added_filter_for_responses_where_answer_to_question": "Added filter for responses where answer to question {questionIdx} is {filterComboBoxValue} - {filterValue} ", @@ -1849,6 +1856,7 @@ "publish_to_web_warning": "You are about to release these survey results to the public.", "publish_to_web_warning_description": "Your survey results will be public. Anyone outside your organization can access them if they have the link.", "qr_code": "QR code", + "qr_code_description": "Responses collected via QR code are anonymous.", "qr_code_download_failed": "QR code download failed", "qr_code_download_with_start_soon": "QR code download will start soon", "qr_code_generation_failed": "There was a problem, loading the survey QR Code. Please try again.", diff --git a/apps/web/locales/fr-FR.json b/apps/web/locales/fr-FR.json index 9e117d0d97..afc56be799 100644 --- a/apps/web/locales/fr-FR.json +++ b/apps/web/locales/fr-FR.json @@ -1799,7 +1799,14 @@ "send_preview_email": "Envoyer un e-mail d'aperçu", "title": "Intégrez votre sondage dans un e-mail" }, - "share_view_title": "Partager par" + "share_view_title": "Partager par", + "social_media": { + "description": "Obtenez des réponses de vos contacts sur divers réseaux sociaux.", + "share_your_survey_on_social_media": "Partagez votre sondage sur les réseaux sociaux", + "source_tracking_enabled": "Suivi des sources activé", + "source_tracking_enabled_alert_description": "En partageant depuis cette boîte de dialogue, le réseau social sera ajouté au lien du sondage afin que vous sachiez quelles réponses proviennent de chaque réseau.", + "title": "Médias sociaux" + } }, "summary": { "added_filter_for_responses_where_answer_to_question": "Filtre ajouté pour les réponses où la réponse à la question '{'questionIdx'}' est '{'filterComboBoxValue'}' - '{'filterValue'}' ", @@ -1849,6 +1856,7 @@ "publish_to_web_warning": "Vous êtes sur le point de rendre ces résultats d'enquête publics.", "publish_to_web_warning_description": "Les résultats de votre enquête seront publics. Toute personne en dehors de votre organisation pourra y accéder si elle a le lien.", "qr_code": "Code QR", + "qr_code_description": "Les réponses collectées via le code QR sont anonymes.", "qr_code_download_failed": "Échec du téléchargement du code QR", "qr_code_download_with_start_soon": "Le téléchargement du code QR débutera bientôt", "qr_code_generation_failed": "\"Un problème est survenu lors du chargement du code QR du sondage. Veuillez réessayer.\"", diff --git a/apps/web/locales/pt-BR.json b/apps/web/locales/pt-BR.json index ea434840c4..563990f8a9 100644 --- a/apps/web/locales/pt-BR.json +++ b/apps/web/locales/pt-BR.json @@ -1799,7 +1799,14 @@ "send_preview_email": "Enviar prévia de e-mail", "title": "Incorpore sua pesquisa em um e-mail" }, - "share_view_title": "Compartilhar via" + "share_view_title": "Compartilhar via", + "social_media": { + "description": "Obtenha respostas de seus contatos em várias redes sociais.", + "share_your_survey_on_social_media": "Compartilhe sua pesquisa nas redes sociais", + "source_tracking_enabled": "rastreamento de origem ativado", + "source_tracking_enabled_alert_description": "Ao compartilhar a partir deste diálogo, a rede social será adicionada ao link da pesquisa para que você saiba de qual rede vieram as respostas.", + "title": "Mídia Social" + } }, "summary": { "added_filter_for_responses_where_answer_to_question": "Adicionado filtro para respostas onde a resposta à pergunta {questionIdx} é {filterComboBoxValue} - {filterValue} ", @@ -1849,6 +1856,7 @@ "publish_to_web_warning": "Você está prestes a divulgar esses resultados da pesquisa para o público.", "publish_to_web_warning_description": "Os resultados da sua pesquisa serão públicos. Qualquer pessoa fora da sua organização pode acessá-los se tiver o link.", "qr_code": "Código QR", + "qr_code_description": "Respostas coletadas via código QR são anônimas.", "qr_code_download_failed": "falha no download do código QR", "qr_code_download_with_start_soon": "O download do código QR começará em breve", "qr_code_generation_failed": "Houve um problema ao carregar o Código QR do questionário. Por favor, tente novamente.", diff --git a/apps/web/locales/pt-PT.json b/apps/web/locales/pt-PT.json index 9d3324fd36..c3bcc5ab6b 100644 --- a/apps/web/locales/pt-PT.json +++ b/apps/web/locales/pt-PT.json @@ -1799,7 +1799,14 @@ "send_preview_email": "Enviar pré-visualização de email", "title": "Incorporar o seu inquérito num email" }, - "share_view_title": "Partilhar via" + "share_view_title": "Partilhar via", + "social_media": { + "description": "Obtenha respostas dos seus contactos em várias redes sociais.", + "share_your_survey_on_social_media": "Partilhe o seu inquérito nas redes sociais", + "source_tracking_enabled": "Rastreamento de origem ativado", + "source_tracking_enabled_alert_description": "Ao partilhar a partir deste diálogo, a rede social será anexada ao link do inquérito para que saiba de que rede vieram as respostas.", + "title": "Redes Sociais" + } }, "summary": { "added_filter_for_responses_where_answer_to_question": "Adicionado filtro para respostas onde a resposta à pergunta {questionIdx} é {filterComboBoxValue} - {filterValue} ", @@ -1849,6 +1856,7 @@ "publish_to_web_warning": "Está prestes a divulgar estes resultados do inquérito ao público.", "publish_to_web_warning_description": "Os resultados do seu inquérito serão públicos. Qualquer pessoa fora da sua organização pode aceder a eles se tiver o link.", "qr_code": "Código QR", + "qr_code_description": "Respostas recolhidas através de código QR são anónimas.", "qr_code_download_failed": "Falha ao transferir o código QR", "qr_code_download_with_start_soon": "O download do código QR começará em breve", "qr_code_generation_failed": "Ocorreu um problema ao carregar o Código QR do questionário. Por favor, tente novamente.", diff --git a/apps/web/locales/zh-Hant-TW.json b/apps/web/locales/zh-Hant-TW.json index 22f80e174f..6885fb0dad 100644 --- a/apps/web/locales/zh-Hant-TW.json +++ b/apps/web/locales/zh-Hant-TW.json @@ -1799,7 +1799,14 @@ "send_preview_email": "發送預覽電子郵件", "title": "嵌入 你的 調查 在 電子郵件 中" }, - "share_view_title": "透過 分享" + "share_view_title": "透過 分享", + "social_media": { + "description": "從 您 的 聯絡人 在 各 種 社交 媒體 網絡 上 獲得 回應。", + "share_your_survey_on_social_media": "分享 您 的 問卷 在 社交媒體 上", + "source_tracking_enabled": "來源追蹤已啟用", + "source_tracking_enabled_alert_description": "從 此 對 話 框 共 享 時,社 交 媒 體 網 絡 會 被 附 加 到 調 查鏈 接 下,讓 您 知 道 各 網 絡 的 回 應 來 源。", + "title": "社群媒體" + } }, "summary": { "added_filter_for_responses_where_answer_to_question": "已新增回應的篩選器,其中問題 '{'questionIdx'}' 的答案為 '{'filterComboBoxValue'}' - '{'filterValue'}'", @@ -1849,6 +1856,7 @@ "publish_to_web_warning": "您即將將這些問卷結果發布到公共領域。", "publish_to_web_warning_description": "您的問卷結果將會是公開的。任何組織外的人員都可以存取這些結果(如果他們有連結)。", "qr_code": "QR 碼", + "qr_code_description": "透過 QR code 收集的回應都是匿名的。", "qr_code_download_failed": "QR code 下載失敗", "qr_code_download_with_start_soon": "QR code 下載即將開始", "qr_code_generation_failed": "載入調查 QR Code 時發生問題。請再試一次。", diff --git a/apps/web/modules/email/components/email-button.tsx b/apps/web/modules/email/components/email-button.tsx index c767edd9c7..814e801bbe 100644 --- a/apps/web/modules/email/components/email-button.tsx +++ b/apps/web/modules/email/components/email-button.tsx @@ -8,7 +8,7 @@ interface EmailButtonProps { export function EmailButton({ label, href }: EmailButtonProps): React.JSX.Element { return ( - ); diff --git a/apps/web/modules/email/components/email-footer.tsx b/apps/web/modules/email/components/email-footer.tsx index 9e094109b5..6ba2e6e315 100644 --- a/apps/web/modules/email/components/email-footer.tsx +++ b/apps/web/modules/email/components/email-footer.tsx @@ -4,7 +4,7 @@ import React from "react"; export function EmailFooter({ t }: { t: TFnType }): React.JSX.Element { return ( - + {t("emails.email_footer_text_1")}
{t("emails.email_footer_text_2")} diff --git a/apps/web/modules/email/components/email-template.tsx b/apps/web/modules/email/components/email-template.tsx index b137ef57df..0bc9db0a20 100644 --- a/apps/web/modules/email/components/email-template.tsx +++ b/apps/web/modules/email/components/email-template.tsx @@ -23,7 +23,7 @@ export async function EmailTemplate({ @@ -47,24 +47,32 @@ export async function EmailTemplate({
{t("emails.email_template_text_1")} {IMPRINT_ADDRESS && ( - {IMPRINT_ADDRESS} + {IMPRINT_ADDRESS} )} - + {IMPRINT_URL && ( - + {t("emails.imprint")} )} {IMPRINT_URL && PRIVACY_URL && " • "} {PRIVACY_URL && ( - + {t("emails.privacy_policy")} )} diff --git a/apps/web/modules/email/emails/auth/forgot-password-email.tsx b/apps/web/modules/email/emails/auth/forgot-password-email.tsx index a34f5d22f1..45ba88c873 100644 --- a/apps/web/modules/email/emails/auth/forgot-password-email.tsx +++ b/apps/web/modules/email/emails/auth/forgot-password-email.tsx @@ -17,10 +17,10 @@ export async function ForgotPasswordEmail({ {t("emails.forgot_password_email_heading")} - {t("emails.forgot_password_email_text")} + {t("emails.forgot_password_email_text")} - {t("emails.forgot_password_email_link_valid_for_24_hours")} - {t("emails.forgot_password_email_did_not_request")} + {t("emails.forgot_password_email_link_valid_for_24_hours")} + {t("emails.forgot_password_email_did_not_request")} diff --git a/apps/web/modules/email/emails/auth/new-email-verification.tsx b/apps/web/modules/email/emails/auth/new-email-verification.tsx index b20bc79a81..f7c4451ec3 100644 --- a/apps/web/modules/email/emails/auth/new-email-verification.tsx +++ b/apps/web/modules/email/emails/auth/new-email-verification.tsx @@ -17,14 +17,14 @@ export async function NewEmailVerification({ {t("emails.verification_email_heading")} - {t("emails.new_email_verification_text")} - {t("emails.verification_security_notice")} + {t("emails.new_email_verification_text")} + {t("emails.verification_security_notice")} - {t("emails.verification_email_click_on_this_link")} - + {t("emails.verification_email_click_on_this_link")} + {verifyLink} - {t("emails.verification_email_link_valid_for_24_hours")} + {t("emails.verification_email_link_valid_for_24_hours")} diff --git a/apps/web/modules/email/emails/auth/password-reset-notify-email.tsx b/apps/web/modules/email/emails/auth/password-reset-notify-email.tsx index c44b67b79a..a3799a736e 100644 --- a/apps/web/modules/email/emails/auth/password-reset-notify-email.tsx +++ b/apps/web/modules/email/emails/auth/password-reset-notify-email.tsx @@ -10,7 +10,7 @@ export async function PasswordResetNotifyEmail(): Promise { {t("emails.password_changed_email_heading")} - {t("emails.password_changed_email_text")} + {t("emails.password_changed_email_text")} diff --git a/apps/web/modules/email/emails/auth/verification-email.tsx b/apps/web/modules/email/emails/auth/verification-email.tsx index 07be03ab05..c68ac0f018 100644 --- a/apps/web/modules/email/emails/auth/verification-email.tsx +++ b/apps/web/modules/email/emails/auth/verification-email.tsx @@ -19,16 +19,16 @@ export async function VerificationEmail({ {t("emails.verification_email_heading")} - {t("emails.verification_email_text")} + {t("emails.verification_email_text")} - {t("emails.verification_email_click_on_this_link")} - + {t("emails.verification_email_click_on_this_link")} + {verifyLink} - {t("emails.verification_email_link_valid_for_24_hours")} - + {t("emails.verification_email_link_valid_for_24_hours")} + {t("emails.verification_email_if_expired_request_new_token")} - + {t("emails.verification_email_request_new_verification")} diff --git a/apps/web/modules/email/emails/general/email-customization-preview-email.tsx b/apps/web/modules/email/emails/general/email-customization-preview-email.tsx index 309e77697d..252e15fe48 100644 --- a/apps/web/modules/email/emails/general/email-customization-preview-email.tsx +++ b/apps/web/modules/email/emails/general/email-customization-preview-email.tsx @@ -16,10 +16,8 @@ export async function EmailCustomizationPreviewEmail({ return ( - - {t("emails.email_customization_preview_email_heading", { userName })} - - {t("emails.email_customization_preview_email_text")} + {t("emails.email_customization_preview_email_heading", { userName })} + {t("emails.email_customization_preview_email_text")} ); diff --git a/apps/web/modules/email/emails/invite/invite-accepted-email.tsx b/apps/web/modules/email/emails/invite/invite-accepted-email.tsx index 976f311858..12a06cc140 100644 --- a/apps/web/modules/email/emails/invite/invite-accepted-email.tsx +++ b/apps/web/modules/email/emails/invite/invite-accepted-email.tsx @@ -17,10 +17,10 @@ export async function InviteAcceptedEmail({ return ( - + {t("emails.invite_accepted_email_heading", { inviterName })} {inviterName} - + {t("emails.invite_accepted_email_text_par1", { inviteeName })} {inviteeName}{" "} {t("emails.invite_accepted_email_text_par2")} diff --git a/apps/web/modules/email/emails/invite/invite-email.tsx b/apps/web/modules/email/emails/invite/invite-email.tsx index 1e87f50204..9a4e6a1ed6 100644 --- a/apps/web/modules/email/emails/invite/invite-email.tsx +++ b/apps/web/modules/email/emails/invite/invite-email.tsx @@ -20,10 +20,10 @@ export async function InviteEmail({ return ( - + {t("emails.invite_email_heading", { inviteeName })} {inviteeName} - + {t("emails.invite_email_text_par1", { inviterName })} {inviterName}{" "} {t("emails.invite_email_text_par2")} diff --git a/apps/web/modules/email/emails/invite/onboarding-invite-email.tsx b/apps/web/modules/email/emails/invite/onboarding-invite-email.tsx deleted file mode 100644 index 1185c2828e..0000000000 --- a/apps/web/modules/email/emails/invite/onboarding-invite-email.tsx +++ /dev/null @@ -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 { - const t = await getTranslate(); - return ( - - - {t("emails.onboarding_invite_email_heading", { inviteeName })} - {inviteMessage} - {t("emails.onboarding_invite_email_get_started_in_minutes")} -
    -
  1. {t("emails.onboarding_invite_email_create_account", { inviterName })}
  2. -
  3. {t("emails.onboarding_invite_email_connect_formbricks")}
  4. -
  5. {t("emails.onboarding_invite_email_done")} ✅
  6. -
- - -
-
- ); -} - -export default OnboardingInviteEmail; diff --git a/apps/web/modules/email/emails/lib/tests/utils.test.tsx b/apps/web/modules/email/emails/lib/tests/utils.test.tsx index 907f31a7d0..5979d0f7d3 100644 --- a/apps/web/modules/email/emails/lib/tests/utils.test.tsx +++ b/apps/web/modules/email/emails/lib/tests/utils.test.tsx @@ -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) => { diff --git a/apps/web/modules/email/emails/lib/utils.tsx b/apps/web/modules/email/emails/lib/utils.tsx index a62b0603dc..466d333487 100644 --- a/apps/web/modules/email/emails/lib/utils.tsx +++ b/apps/web/modules/email/emails/lib/utils.tsx @@ -15,18 +15,20 @@ export const renderEmailResponseValue = async ( return ( {overrideFileUploadResponse ? ( - + {t("emails.render_email_response_value_file_upload_response_link_not_included")} ) : ( Array.isArray(response) && response.map((responseItem) => ( - - {getOriginalFileNameFromUrl(responseItem)} + + + {getOriginalFileNameFromUrl(responseItem)} + )) )} @@ -50,7 +52,7 @@ export const renderEmailResponseValue = async ( case TSurveyQuestionTypeEnum.Ranking: return ( - + {Array.isArray(response) && response.map( (item, index) => @@ -66,6 +68,6 @@ export const renderEmailResponseValue = async ( ); default: - return {response}; + return {response}; } }; diff --git a/apps/web/modules/email/emails/survey/embed-survey-preview-email.tsx b/apps/web/modules/email/emails/survey/embed-survey-preview-email.tsx index b88c460a14..4a57ed60d4 100644 --- a/apps/web/modules/email/emails/survey/embed-survey-preview-email.tsx +++ b/apps/web/modules/email/emails/survey/embed-survey-preview-email.tsx @@ -18,13 +18,13 @@ export async function EmbedSurveyPreviewEmail({ return ( - {t("emails.embed_survey_preview_email_heading")} - {t("emails.embed_survey_preview_email_text")} - + {t("emails.embed_survey_preview_email_heading")} + {t("emails.embed_survey_preview_email_text")} + {t("emails.embed_survey_preview_email_didnt_request")}{" "} {t("emails.embed_survey_preview_email_fight_spam")} -
+
{t("emails.embed_survey_preview_email_environment_id")}: {environmentId} diff --git a/apps/web/modules/email/emails/survey/link-survey-email.tsx b/apps/web/modules/email/emails/survey/link-survey-email.tsx index d59e2df77d..69267c07c9 100644 --- a/apps/web/modules/email/emails/survey/link-survey-email.tsx +++ b/apps/web/modules/email/emails/survey/link-survey-email.tsx @@ -20,11 +20,11 @@ export async function LinkSurveyEmail({ return ( - {t("emails.verification_email_hey")} - {t("emails.verification_email_thanks")} - {t("emails.verification_email_to_fill_survey")} + {t("emails.verification_email_hey")} + {t("emails.verification_email_thanks")} + {t("emails.verification_email_to_fill_survey")} - + {t("emails.verification_email_survey_name")}: {surveyName} diff --git a/apps/web/modules/email/emails/survey/response-finished-email.tsx b/apps/web/modules/email/emails/survey/response-finished-email.tsx index 9a7dea48ff..863a05e393 100644 --- a/apps/web/modules/email/emails/survey/response-finished-email.tsx +++ b/apps/web/modules/email/emails/survey/response-finished-email.tsx @@ -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({ - {t("emails.survey_response_finished_email_hey")} - + {t("emails.survey_response_finished_email_hey")} + {t("emails.survey_response_finished_email_congrats", { surveyName: survey.name, })} @@ -45,8 +45,8 @@ export async function ResponseFinishedEmail({ if (!question.response) return; return ( - - {question.question} + + {question.question} {renderEmailResponseValue(question.response, question.type, t)} @@ -57,8 +57,8 @@ export async function ResponseFinishedEmail({ if (variableResponse && ["number", "string"].includes(typeof variable)) { return ( - - + + {variable.type === "number" ? ( ) : ( @@ -66,7 +66,7 @@ export async function ResponseFinishedEmail({ )} {variable.name} - + {variableResponse} @@ -80,11 +80,11 @@ export async function ResponseFinishedEmail({ if (hiddenFieldResponse && typeof hiddenFieldResponse === "string") { return ( - - + + {hiddenFieldId} - + {hiddenFieldResponse} @@ -105,19 +105,19 @@ export async function ResponseFinishedEmail({ />
- + {t("emails.survey_response_finished_email_dont_want_notifications")} {t("emails.survey_response_finished_email_turn_off_notifications_for_this_form")} {t("emails.survey_response_finished_email_turn_off_notifications_for_all_new_forms")} diff --git a/apps/web/modules/email/emails/weekly-summary/create-reminder-notification-body.tsx b/apps/web/modules/email/emails/weekly-summary/create-reminder-notification-body.tsx index e0c8e25e6c..3fbe96f2b0 100644 --- a/apps/web/modules/email/emails/weekly-summary/create-reminder-notification-body.tsx +++ b/apps/web/modules/email/emails/weekly-summary/create-reminder-notification-body.tsx @@ -16,19 +16,19 @@ export async function CreateReminderNotificationBody({ const t = await getTranslate(); return ( - + {t("emails.weekly_summary_create_reminder_notification_body_text", { projectName: notificationData.projectName, })} - + {t("emails.weekly_summary_create_reminder_notification_body_dont_let_a_week_pass")} - + {t("emails.weekly_summary_create_reminder_notification_body_need_help")} {t("emails.weekly_summary_create_reminder_notification_body_cal_slot")} diff --git a/apps/web/modules/email/emails/weekly-summary/live-survey-notification.tsx b/apps/web/modules/email/emails/weekly-summary/live-survey-notification.tsx index 5fe9f9558b..3f31811ef7 100644 --- a/apps/web/modules/email/emails/weekly-summary/live-survey-notification.tsx +++ b/apps/web/modules/email/emails/weekly-summary/live-survey-notification.tsx @@ -48,7 +48,9 @@ export async function LiveSurveyNotification({ if (surveyResponses.length === 0) { return ( - {t("emails.live_survey_notification_no_responses_yet")} + + {t("emails.live_survey_notification_no_responses_yet")} + ); } @@ -62,7 +64,7 @@ export async function LiveSurveyNotification({ surveyFields.push( - {surveyResponse.headline} + {surveyResponse.headline} {renderEmailResponseValue(surveyResponse.responseValue, surveyResponse.questionType, t)} ); @@ -87,7 +89,7 @@ export async function LiveSurveyNotification({ {survey.name} @@ -98,7 +100,7 @@ export async function LiveSurveyNotification({ {displayStatus} {noResponseLastWeek ? ( - {t("emails.live_survey_notification_no_new_response")} + {t("emails.live_survey_notification_no_new_response")} ) : ( createSurveyFields(survey.responses) )} diff --git a/apps/web/modules/email/emails/weekly-summary/notification-footer.tsx b/apps/web/modules/email/emails/weekly-summary/notification-footer.tsx index d4632e47a4..19c8d417ef 100644 --- a/apps/web/modules/email/emails/weekly-summary/notification-footer.tsx +++ b/apps/web/modules/email/emails/weekly-summary/notification-footer.tsx @@ -13,15 +13,15 @@ export async function NotificationFooter({ return ( - {t("emails.notification_footer_all_the_best")} - {t("emails.notification_footer_the_formbricks_team")} + {t("emails.notification_footer_all_the_best")} + {t("emails.notification_footer_the_formbricks_team")} - + {t("emails.notification_footer_to_halt_weekly_updates")} {t("emails.notification_footer_please_turn_them_off")} {" "} diff --git a/apps/web/modules/email/emails/weekly-summary/notification-header.tsx b/apps/web/modules/email/emails/weekly-summary/notification-header.tsx index ad9b00dbe9..542808d0a2 100644 --- a/apps/web/modules/email/emails/weekly-summary/notification-header.tsx +++ b/apps/web/modules/email/emails/weekly-summary/notification-header.tsx @@ -18,17 +18,17 @@ export async function NotificationHeader({ endYear, }: NotificationHeaderProps): Promise { const t = await getTranslate(); - const getNotificationHeaderimePeriod = (): React.JSX.Element => { + const getNotificationHeaderTimePeriod = (): React.JSX.Element => { if (startYear === endYear) { return ( - + {startDate} - {endDate} {endYear} ); } return ( - + {startDate} {startYear} - {endDate} {endYear} ); @@ -40,10 +40,10 @@ export async function NotificationHeader({ {t("emails.notification_header_hey")}
- + {t("emails.notification_header_weekly_report_for")} {projectName} - {getNotificationHeaderimePeriod()} + {getNotificationHeaderTimePeriod()}
diff --git a/apps/web/modules/email/emails/weekly-summary/notification-insight.tsx b/apps/web/modules/email/emails/weekly-summary/notification-insight.tsx index 679539e3ca..f9bce12bf2 100644 --- a/apps/web/modules/email/emails/weekly-summary/notification-insight.tsx +++ b/apps/web/modules/email/emails/weekly-summary/notification-insight.tsx @@ -17,24 +17,24 @@ export async function NotificationInsight({ {t("emails.notification_insight_surveys")} - {insights.numLiveSurvey} + {insights.numLiveSurvey} {t("emails.notification_insight_displays")} - {insights.totalDisplays} + {insights.totalDisplays} {t("emails.notification_insight_responses")} - {insights.totalResponses} + {insights.totalResponses} {t("emails.notification_insight_completed")} - {insights.totalCompletedResponses} + {insights.totalCompletedResponses} {insights.totalDisplays !== 0 ? ( {t("emails.notification_insight_completion_rate")} - {Math.round(insights.completionRate)}% + {Math.round(insights.completionRate)}% ) : ( "" diff --git a/apps/web/modules/email/index.tsx b/apps/web/modules/email/index.tsx index ca2411f3c0..a75e673556 100644 --- a/apps/web/modules/email/index.tsx +++ b/apps/web/modules/email/index.tsx @@ -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 => { 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 ( diff --git a/apps/web/modules/organization/settings/teams/actions.ts b/apps/web/modules/organization/settings/teams/actions.ts index 83b25e7b9d..9df2ce6427 100644 --- a/apps/web/modules/organization/settings/teams/actions.ts +++ b/apps/web/modules/organization/settings/teams/actions.ts @@ -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; diff --git a/apps/web/modules/setup/organization/[organizationId]/invite/actions.ts b/apps/web/modules/setup/organization/[organizationId]/invite/actions.ts index bf0b8a0af5..30c4b7aa9e 100644 --- a/apps/web/modules/setup/organization/[organizationId]/invite/actions.ts +++ b/apps/web/modules/setup/organization/[organizationId]/invite/actions.ts @@ -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 = { diff --git a/apps/web/modules/survey/follow-ups/components/follow-up-email.tsx b/apps/web/modules/survey/follow-ups/components/follow-up-email.tsx index df5c9683b9..bbee735da4 100644 --- a/apps/web/modules/survey/follow-ups/components/follow-up-email.tsx +++ b/apps/web/modules/survey/follow-ups/components/follow-up-email.tsx @@ -76,8 +76,8 @@ export async function FollowUpEmail(props: FollowUpEmailProps): Promise - - {question.question} + + {question.question} {renderEmailResponseValue(question.response, question.type, t, true)} @@ -89,22 +89,22 @@ export async function FollowUpEmail(props: FollowUpEmailProps): Promise {t("emails.email_template_text_1")} {IMPRINT_ADDRESS && ( - {IMPRINT_ADDRESS} + {IMPRINT_ADDRESS} )} - + {IMPRINT_URL && ( + className="text-sm text-slate-500"> {t("emails.imprint")} )} @@ -114,7 +114,7 @@ export async function FollowUpEmail(props: FollowUpEmailProps): Promise + className="text-sm text-slate-500"> {t("emails.privacy_policy")} )} diff --git a/apps/web/modules/ui/components/dialog/index.tsx b/apps/web/modules/ui/components/dialog/index.tsx index 252c52cc07..8d2fa8e321 100644 --- a/apps/web/modules/ui/components/dialog/index.tsx +++ b/apps/web/modules/ui/components/dialog/index.tsx @@ -11,7 +11,7 @@ const DialogTrigger = DialogPrimitive.Trigger; const DialogPortal = ({ children, ...props }: DialogPrimitive.DialogPortalProps) => ( -
{children}
+
{children}
); DialogPortal.displayName = DialogPrimitive.Portal.displayName; @@ -41,11 +41,11 @@ interface DialogContentProps { const getDialogWidthClass = (width: "default" | "wide" | "narrow"): string => { switch (width) { case "wide": - return "md:w-[720px] lg:w-[960px]"; + return "sm:w-[90dvw] md:w-[720px] lg:w-[960px]"; case "narrow": - return "md:w-[512px]"; + return "sm:w-[512px]"; default: - return "md:w-[720px]"; + return "sm:w-[90dvw] md:w-[720px]"; } }; @@ -73,8 +73,8 @@ const DialogContent = React.forwardRef< (
svg]:text-primary [&>svg]:absolute [&>svg]:size-4 [&>svg~*]:min-h-4 [&>svg~*]:items-center [&>svg~*]:pl-6 md:[&>svg~*]:flex", + "[&>svg]:text-primary [&>svg]:absolute [&>svg]:size-4 [&>svg~*]:min-h-4 [&>svg~*]:items-center [&>svg~*]:pl-6 sm:[&>svg~*]:flex", className )} {...props} @@ -122,7 +122,7 @@ type DialogFooterProps = Omit, "dangerously const DialogFooter = ({ className, ...props }: DialogFooterProps) => (
> = () => { + return ( + + + + ); +}; diff --git a/apps/web/modules/ui/components/icons/linkedin-icon.tsx b/apps/web/modules/ui/components/icons/linkedin-icon.tsx new file mode 100644 index 0000000000..d89c7a4849 --- /dev/null +++ b/apps/web/modules/ui/components/icons/linkedin-icon.tsx @@ -0,0 +1,27 @@ +export const LinkedinIcon: React.FC> = (props) => { + return ( + + + + + + ); +}; diff --git a/apps/web/modules/ui/components/icons/reddit-icon.tsx b/apps/web/modules/ui/components/icons/reddit-icon.tsx new file mode 100644 index 0000000000..6dced232ea --- /dev/null +++ b/apps/web/modules/ui/components/icons/reddit-icon.tsx @@ -0,0 +1,31 @@ +export const RedditIcon: React.FC> = () => { + return ( + + + + + + + + + + + + + + ); +}; diff --git a/apps/web/modules/ui/components/icons/threads-icon.tsx b/apps/web/modules/ui/components/icons/threads-icon.tsx new file mode 100644 index 0000000000..cf47ea38e0 --- /dev/null +++ b/apps/web/modules/ui/components/icons/threads-icon.tsx @@ -0,0 +1,10 @@ +export const ThreadsIcon: React.FC> = () => { + return ( + + + + ); +}; diff --git a/apps/web/modules/ui/components/icons/x-icon.tsx b/apps/web/modules/ui/components/icons/x-icon.tsx new file mode 100644 index 0000000000..ecfa363482 --- /dev/null +++ b/apps/web/modules/ui/components/icons/x-icon.tsx @@ -0,0 +1,10 @@ +export const XIcon: React.FC> = () => { + return ( + + + + ); +};