Compare commits

...

4 Commits

Author SHA1 Message Date
Matti Nannt
f08fabfb13 fix(backport): github release action fix (#6425) 2025-08-15 13:26:09 +02:00
Dhruwang Jariwala
ee8af9dd74 chore: metadata tweaks backport (#6421) 2025-08-15 13:02:28 +02:00
Dhruwang Jariwala
1091b40bd1 fix(backport): cross button hover (#6416) 2025-08-14 14:30:05 +02:00
Anshuman Pandey
87a2d727ed fix: disables tabs when single use is enabled [Backport] (#6412) 2025-08-14 04:07:35 -07:00
12 changed files with 87 additions and 67 deletions

View File

@@ -37,7 +37,7 @@ on:
permissions:
id-token: write
contents: write
contents: read
jobs:
helmfile-deploy:

View File

@@ -7,12 +7,13 @@ on:
permissions:
contents: read
env:
ENVIRONMENT: ${{ github.event.release.prerelease && 'staging' || 'production' }}
jobs:
docker-build:
name: Build & release docker image
permissions:
contents: read
packages: write
id-token: write
uses: ./.github/workflows/release-docker-github.yml
secrets: inherit
with:
@@ -20,6 +21,9 @@ jobs:
helm-chart-release:
name: Release Helm Chart
permissions:
contents: read
packages: write
uses: ./.github/workflows/release-helm-chart.yml
secrets: inherit
needs:
@@ -29,6 +33,9 @@ jobs:
deploy-formbricks-cloud:
name: Deploy Helm Chart to Formbricks Cloud
permissions:
contents: read
id-token: write
secrets: inherit
uses: ./.github/workflows/deploy-formbricks-cloud.yml
needs:
@@ -36,7 +43,7 @@ jobs:
- helm-chart-release
with:
VERSION: v${{ needs.docker-build.outputs.VERSION }}
ENVIRONMENT: ${{ env.ENVIRONMENT }}
ENVIRONMENT: ${{ github.event.release.prerelease && 'staging' || 'production' }}
upload-sentry-sourcemaps:
name: Upload Sentry Sourcemaps
@@ -64,4 +71,4 @@ jobs:
docker_image: ghcr.io/formbricks/formbricks:v${{ needs.docker-build.outputs.VERSION }}
release_version: v${{ needs.docker-build.outputs.VERSION }}
sentry_auth_token: ${{ secrets.SENTRY_AUTH_TOKEN }}
environment: ${{ env.ENVIRONMENT }}
environment: ${{ github.event.release.prerelease && 'staging' || 'production' }}

View File

@@ -29,7 +29,7 @@ import {
SquareStack,
UserIcon,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { TSegment } from "@formbricks/types/segment";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUser } from "@formbricks/types/user";
@@ -77,6 +77,7 @@ export const ShareSurveyModal = ({
description: string;
componentType: React.ComponentType<unknown>;
componentProps: unknown;
disabled?: boolean;
}[] = useMemo(
() => [
{
@@ -111,6 +112,7 @@ export const ShareSurveyModal = ({
isContactsEnabled,
isFormbricksCloud,
},
disabled: survey.singleUse?.enabled,
},
{
id: ShareViaType.WEBSITE_EMBED,
@@ -121,6 +123,7 @@ export const ShareSurveyModal = ({
description: t("environments.surveys.share.embed_on_website.description"),
componentType: WebsiteEmbedTab,
componentProps: { surveyUrl },
disabled: survey.singleUse?.enabled,
},
{
id: ShareViaType.EMAIL,
@@ -131,6 +134,7 @@ export const ShareSurveyModal = ({
description: t("environments.surveys.share.send_email.description"),
componentType: EmailTab,
componentProps: { surveyId: survey.id, email },
disabled: survey.singleUse?.enabled,
},
{
id: ShareViaType.SOCIAL_MEDIA,
@@ -141,6 +145,7 @@ export const ShareSurveyModal = ({
description: t("environments.surveys.share.social_media.description"),
componentType: SocialMediaTab,
componentProps: { surveyUrl, surveyTitle: survey.name },
disabled: survey.singleUse?.enabled,
},
{
id: ShareViaType.QR_CODE,
@@ -151,6 +156,7 @@ export const ShareSurveyModal = ({
description: t("environments.surveys.summary.qr_code_description"),
componentType: QRCodeTab,
componentProps: { surveyUrl },
disabled: survey.singleUse?.enabled,
},
{
id: ShareViaType.DYNAMIC_POPUP,
@@ -177,9 +183,9 @@ export const ShareSurveyModal = ({
t,
survey,
publicDomain,
setSurveyUrl,
user.locale,
surveyUrl,
isReadOnly,
environmentId,
segments,
isContactsEnabled,
@@ -188,9 +194,14 @@ export const ShareSurveyModal = ({
]
);
const [activeId, setActiveId] = useState<ShareViaType | ShareSettingsType>(
survey.type === "link" ? ShareViaType.ANON_LINKS : ShareViaType.APP
);
const getDefaultActiveId = useCallback(() => {
if (survey.type !== "link") {
return ShareViaType.APP;
}
return ShareViaType.ANON_LINKS;
}, [survey.type]);
const [activeId, setActiveId] = useState<ShareViaType | ShareSettingsType>(getDefaultActiveId());
useEffect(() => {
if (open) {
@@ -198,11 +209,19 @@ export const ShareSurveyModal = ({
}
}, [open, modalView]);
// Ensure active tab is not disabled - if it is, switch to default
useEffect(() => {
const activeTab = linkTabs.find((tab) => tab.id === activeId);
if (activeTab?.disabled) {
setActiveId(getDefaultActiveId());
}
}, [activeId, linkTabs, getDefaultActiveId]);
const handleOpenChange = (open: boolean) => {
setOpen(open);
if (!open) {
setShowView("start");
setActiveId(ShareViaType.ANON_LINKS);
setActiveId(getDefaultActiveId());
}
};

View File

@@ -150,13 +150,13 @@ export const LinkSettingsTab = ({ isReadOnly, locale }: LinkSettingsTabProps) =>
name: "title",
label: t("environments.surveys.share.link_settings.link_title"),
description: t("environments.surveys.share.link_settings.link_title_description"),
placeholder: t("environments.surveys.share.link_settings.link_title_placeholder"),
placeholder: survey.name,
},
{
name: "description",
label: t("environments.surveys.share.link_settings.link_description"),
description: t("environments.surveys.share.link_settings.link_description_description"),
placeholder: t("environments.surveys.share.link_settings.link_description_placeholder"),
placeholder: "Please complete this survey.",
},
];

View File

@@ -34,6 +34,7 @@ interface ShareViewProps {
componentProps: any;
title: string;
description?: string;
disabled?: boolean;
}>;
activeId: ShareViaType | ShareSettingsType;
setActiveId: React.Dispatch<React.SetStateAction<ShareViaType | ShareSettingsType>>;
@@ -109,12 +110,13 @@ export const ShareView = ({ tabs, activeId, setActiveId }: ShareViewProps) => {
onClick={() => setActiveId(tab.id)}
className={cn(
"flex w-full justify-start rounded-md p-2 text-slate-600 hover:bg-slate-100 hover:text-slate-900",
tab.id === activeId
tab.id === activeId && !tab.disabled
? "bg-slate-100 font-medium text-slate-900"
: "text-slate-700"
)}
tooltip={tab.label}
isActive={tab.id === activeId}>
isActive={tab.id === activeId}
disabled={tab.disabled}>
<tab.icon className="h-4 w-4 text-slate-700" />
<span>{tab.label}</span>
</SidebarMenuButton>
@@ -136,9 +138,10 @@ export const ShareView = ({ tabs, activeId, setActiveId }: ShareViewProps) => {
<Button
variant="ghost"
onClick={() => setActiveId(tab.id)}
disabled={tab.disabled}
className={cn(
"rounded-md px-4 py-2",
tab.id === activeId
tab.id === activeId && !tab.disabled
? "bg-white text-slate-900 shadow-sm hover:bg-white"
: "border-transparent text-slate-700 hover:text-slate-900"
)}>

View File

@@ -92,7 +92,7 @@ describe("contact-survey page", () => {
params: Promise.resolve({ jwt: "token" }),
searchParams: Promise.resolve({}),
});
expect(meta).toEqual({ title: "Survey", description: "Complete this survey" });
expect(meta).toEqual({ title: "Survey", description: "Please complete this survey." });
});
test("generateMetadata returns default when verify throws", async () => {
@@ -103,7 +103,7 @@ describe("contact-survey page", () => {
params: Promise.resolve({ jwt: "token" }),
searchParams: Promise.resolve({}),
});
expect(meta).toEqual({ title: "Survey", description: "Complete this survey" });
expect(meta).toEqual({ title: "Survey", description: "Please complete this survey." });
});
test("generateMetadata returns basic metadata when token valid", async () => {

View File

@@ -31,7 +31,7 @@ export const generateMetadata = async (props: ContactSurveyPageProps): Promise<M
if (!result.ok) {
return {
title: "Survey",
description: "Complete this survey",
description: "Please complete this survey.",
};
}
const { surveyId } = result.data;
@@ -40,7 +40,7 @@ export const generateMetadata = async (props: ContactSurveyPageProps): Promise<M
// If the token is invalid, we'll return generic metadata
return {
title: "Survey",
description: "Complete this survey",
description: "Please complete this survey.",
};
}
};

View File

@@ -77,7 +77,7 @@ describe("Metadata Utils", () => {
expect(getSurvey).toHaveBeenCalledWith(mockSurveyId);
expect(result).toEqual({
title: "Survey",
description: "Complete this survey",
description: "Please complete this survey.",
survey: null,
ogImage: undefined,
});
@@ -108,10 +108,9 @@ describe("Metadata Utils", () => {
const result = await getBasicSurveyMetadata(mockSurveyId);
expect(getSurvey).toHaveBeenCalledWith(mockSurveyId);
expect(getProjectByEnvironmentId).toHaveBeenCalledWith(mockEnvironmentId);
expect(result).toEqual({
title: "Welcome Headline | Test Project",
description: "Complete this survey",
title: "Welcome Headline",
description: "Please complete this survey.",
survey: mockSurvey,
ogImage: undefined,
});
@@ -129,13 +128,12 @@ describe("Metadata Utils", () => {
} as TSurvey;
vi.mocked(getSurvey).mockResolvedValue(mockSurvey);
vi.mocked(getProjectByEnvironmentId).mockResolvedValue({ name: "Test Project" } as any);
const result = await getBasicSurveyMetadata(mockSurveyId);
expect(result).toEqual({
title: "Test Survey | Test Project",
description: "Complete this survey",
title: "Test Survey",
description: "Please complete this survey.",
survey: mockSurvey,
ogImage: undefined,
});

View File

@@ -1,8 +1,8 @@
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getPublicDomain } from "@/lib/getPublicUrl";
import { getLocalizedValue } from "@/lib/i18n/utils";
import { COLOR_DEFAULTS } from "@/lib/styling/constants";
import { getSurvey } from "@/modules/survey/lib/survey";
import { getProjectByEnvironmentId } from "@/modules/survey/link/lib/project";
import { Metadata } from "next";
type TBasicSurveyMetadata = {
@@ -12,22 +12,16 @@ type TBasicSurveyMetadata = {
ogImage?: string;
};
/**
* Utility function to encode name for URL usage
*/
export const getNameForURL = (url: string) => url.replace(/ /g, "%20");
export const getNameForURL = (value: string) => encodeURIComponent(value);
/**
* Utility function to encode brand color for URL usage
*/
export const getBrandColorForURL = (url: string) => url.replace(/#/g, "%23");
export const getBrandColorForURL = (value: string) => encodeURIComponent(value);
/**
* Get basic survey metadata (title and description) based on link metadata, welcome card or survey name
*/
export const getBasicSurveyMetadata = async (
surveyId: string,
languageCode?: string
languageCode = "default"
): Promise<TBasicSurveyMetadata> => {
const survey = await getSurvey(surveyId);
@@ -35,7 +29,7 @@ export const getBasicSurveyMetadata = async (
if (!survey) {
return {
title: "Survey",
description: "Complete this survey",
description: "Please complete this survey.",
survey: null,
ogImage: undefined,
};
@@ -43,38 +37,33 @@ export const getBasicSurveyMetadata = async (
const metadata = survey.metadata;
const welcomeCard = survey.welcomeCard;
const useDefaultLanguageCode =
languageCode === "default" ||
survey.languages.find((lang) => lang.language.code === languageCode)?.default;
// Determine language code to use for metadata
const langCode = languageCode || "default";
const langCode = useDefaultLanguageCode ? "default" : languageCode;
// Set title - priority: custom link metadata > welcome card > survey name
let title = "Survey";
if (metadata.title?.[langCode]) {
title = metadata.title[langCode];
} else if (welcomeCard.enabled && welcomeCard.headline?.default) {
title = welcomeCard.headline.default;
} else {
title = survey.name;
}
const titleFromMetadata = metadata?.title ? getLocalizedValue(metadata.title, langCode) || "" : undefined;
const titleFromWelcome =
welcomeCard?.enabled && welcomeCard.headline
? getLocalizedValue(welcomeCard.headline, langCode) || ""
: undefined;
let title = titleFromMetadata || titleFromWelcome || survey.name;
// Set description - priority: custom link metadata > welcome card > default
let description = "Complete this survey";
if (metadata.description?.[langCode]) {
description = metadata.description[langCode];
}
const descriptionFromMetadata = metadata?.description
? getLocalizedValue(metadata.description, langCode) || ""
: undefined;
let description = descriptionFromMetadata || "Please complete this survey.";
// Get OG image from link metadata if available
const { ogImage } = metadata;
// Add product name in title if it's Formbricks cloud and not using custom metadata
if (!metadata.title?.[langCode]) {
if (!titleFromMetadata) {
if (IS_FORMBRICKS_CLOUD) {
title = `${title} | Formbricks`;
} else {
const project = await getProjectByEnvironmentId(survey.environmentId);
if (project) {
title = `${title} | ${project.name}`;
}
}
}
@@ -89,10 +78,13 @@ export const getBasicSurveyMetadata = async (
/**
* Generate Open Graph metadata for survey
*/
export const getSurveyOpenGraphMetadata = (surveyId: string, surveyName: string): Metadata => {
const brandColor = getBrandColorForURL(COLOR_DEFAULTS.brandColor); // Default color
export const getSurveyOpenGraphMetadata = (
surveyId: string,
surveyName: string,
surveyBrandColor?: string
): Metadata => {
const encodedName = getNameForURL(surveyName);
const brandColor = getBrandColorForURL(surveyBrandColor ?? COLOR_DEFAULTS.brandColor);
const ogImgURL = `/api/v1/client/og?brandColor=${brandColor}&name=${encodedName}`;
return {

View File

@@ -20,7 +20,7 @@ vi.mock("./lib/metadata-utils", () => ({
describe("getMetadataForLinkSurvey", () => {
const mockSurveyId = "survey-123";
const mockSurveyName = "Test Survey";
const mockDescription = "Complete this survey";
const mockDescription = "Please complete this survey.";
const mockOgImageUrl = "https://example.com/custom-image.png";
beforeEach(() => {
@@ -60,7 +60,7 @@ describe("getMetadataForLinkSurvey", () => {
expect(getSurveyMetadata).toHaveBeenCalledWith(mockSurveyId);
expect(getBasicSurveyMetadata).toHaveBeenCalledWith(mockSurveyId, undefined);
expect(getSurveyOpenGraphMetadata).toHaveBeenCalledWith(mockSurveyId, mockSurveyName);
expect(getSurveyOpenGraphMetadata).toHaveBeenCalledWith(mockSurveyId, mockSurveyName, undefined);
expect(result).toEqual({
title: mockSurveyName,

View File

@@ -15,9 +15,10 @@ export const getMetadataForLinkSurvey = async (
// Get enhanced metadata that includes custom link metadata
const { title, description, ogImage } = await getBasicSurveyMetadata(surveyId, languageCode);
const surveyBrandColor = survey.styling?.brandColor?.light;
// Use the shared function for creating the base metadata but override with custom data
const baseMetadata = getSurveyOpenGraphMetadata(survey.id, title);
const baseMetadata = getSurveyOpenGraphMetadata(survey.id, title, surveyBrandColor);
// Override with the custom image URL
if (baseMetadata.openGraph) {

View File

@@ -765,7 +765,7 @@ export function Survey({
<LanguageSwitch
surveyLanguages={localSurvey.languages}
setSelectedLanguageCode={setselectedLanguage}
hoverColor={styling.inputColor?.light ?? "#000000"}
hoverColor={styling.inputColor?.light ?? "#f8fafc"}
borderRadius={styling.roundness ?? 8}
/>
)}
@@ -776,7 +776,7 @@ export function Survey({
{isCloseButtonVisible && (
<SurveyCloseButton
onClose={onClose}
hoverColor={styling.inputColor?.light ?? "#000000"}
hoverColor={styling.inputColor?.light ?? "#f8fafc"}
borderRadius={styling.roundness ?? 8}
/>
)}