Compare commits

..

1 Commits

Author SHA1 Message Date
Matthias Nannt
fe15f6b7bd fix(backport): github release action fix 2025-08-15 13:21:14 +02:00
21 changed files with 77 additions and 3266 deletions

View File

@@ -1,99 +0,0 @@
name: Build & Push Docker to ECR
on:
workflow_dispatch:
inputs:
image_tag:
description: "Image tag to push (e.g., v3.16.1)"
required: true
default: "v3.16.1"
permissions:
contents: read
id-token: write
env:
ECR_REGION: ${{ vars.ECR_REGION }}
# ECR settings are sourced from repository/environment variables for portability across envs/forks
ECR_REGISTRY: ${{ vars.ECR_REGISTRY }}
ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}
DOCKERFILE: apps/web/Dockerfile
CONTEXT: .
jobs:
build-and-push:
name: Build and Push
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate image tag input
shell: bash
env:
IMAGE_TAG: ${{ inputs.image_tag }}
run: |
set -euo pipefail
if [[ -z "${IMAGE_TAG}" ]]; then
echo "❌ Image tag is required (non-empty)."
exit 1
fi
if (( ${#IMAGE_TAG} > 128 )); then
echo "❌ Image tag must be at most 128 characters."
exit 1
fi
if [[ ! "${IMAGE_TAG}" =~ ^[a-z0-9._-]+$ ]]; then
echo "❌ Image tag may only contain lowercase letters, digits, '.', '_' and '-'."
exit 1
fi
if [[ "${IMAGE_TAG}" =~ ^[.-] || "${IMAGE_TAG}" =~ [.-]$ ]]; then
echo "❌ Image tag must not start or end with '.' or '-'."
exit 1
fi
- name: Validate required variables
shell: bash
env:
ECR_REGISTRY: ${{ env.ECR_REGISTRY }}
ECR_REPOSITORY: ${{ env.ECR_REPOSITORY }}
ECR_REGION: ${{ env.ECR_REGION }}
run: |
set -euo pipefail
if [[ -z "${ECR_REGISTRY}" || -z "${ECR_REPOSITORY}" || -z "${ECR_REGION}" ]]; then
echo "ECR_REGION, ECR_REGISTRY and ECR_REPOSITORY must be set via repository or environment variables (Settings → Variables)."
exit 1
fi
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a
with:
role-to-assume: ${{ secrets.AWS_ECR_PUSH_ROLE_ARN }}
aws-region: ${{ env.ECR_REGION }}
- name: Log in to Amazon ECR
uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076
- name: Set up Depot CLI
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
- name: Build and push image (Depot remote builder)
uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0
with:
project: tw0fqmsx3c
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: ${{ env.CONTEXT }}
file: ${{ env.DOCKERFILE }}
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ inputs.image_tag }}
${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:latest
secrets: |
database_url=${{ secrets.DUMMY_DATABASE_URL }}
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}

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: survey.name,
placeholder: t("environments.surveys.share.link_settings.link_title_placeholder"),
},
{
name: "description",
label: t("environments.surveys.share.link_settings.link_description"),
description: t("environments.surveys.share.link_settings.link_description_description"),
placeholder: "Please complete this survey.",
placeholder: t("environments.surveys.share.link_settings.link_description_placeholder"),
},
];

View File

@@ -1,12 +1,12 @@
import { describe, expect, test, vi } from "vitest";
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
import { TResponseData, TResponseVariables } from "@formbricks/types/responses";
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import {
TConditionGroup,
TSingleCondition,
TSurveyLogic,
TSurveyLogicAction,
TSurveyQuestionTypeEnum,
} from "@formbricks/types/surveys/types";
import {
addConditionBelow,
@@ -109,7 +109,6 @@ describe("surveyLogic", () => {
languages: [],
triggers: [],
segment: null,
recaptcha: null,
};
const simpleGroup = (): TConditionGroup => ({
@@ -176,8 +175,7 @@ describe("surveyLogic", () => {
},
],
};
const result = removeCondition(group, "c");
expect(result).toBe(true);
removeCondition(group, "c");
expect(group.conditions).toHaveLength(0);
});
@@ -435,8 +433,6 @@ describe("surveyLogic", () => {
)
).toBe(true);
expect(evaluateLogic(mockSurvey, { f: "foo" }, vars, group(baseCond("isSet")), "en")).toBe(true);
expect(evaluateLogic(mockSurvey, { f: "foo" }, vars, group(baseCond("isNotEmpty")), "en")).toBe(true);
expect(evaluateLogic(mockSurvey, { f: "" }, vars, group(baseCond("isNotSet")), "en")).toBe(true);
expect(evaluateLogic(mockSurvey, { f: "" }, vars, group(baseCond("isEmpty")), "en")).toBe(true);
expect(
evaluateLogic(mockSurvey, { f: "foo" }, vars, group({ ...baseCond("isAnyOf", ["foo", "bar"]) }), "en")
@@ -514,8 +510,7 @@ describe("surveyLogic", () => {
expect(group.conditions.length).toBe(2);
toggleGroupConnector(group, "notfound");
expect(group.connector).toBe("and");
const result = removeCondition(group, "notfound");
expect(result).toBe(false);
removeCondition(group, "notfound");
expect(group.conditions.length).toBe(2);
duplicateCondition(group, "notfound");
expect(group.conditions.length).toBe(2);
@@ -525,192 +520,6 @@ describe("surveyLogic", () => {
expect(group.conditions.length).toBe(2);
});
test("removeCondition returns false when condition not found in nested groups", () => {
const nestedGroup: TConditionGroup = {
id: "nested",
connector: "and",
conditions: [
{
id: "nestedC1",
leftOperand: { type: "hiddenField", value: "nf1" },
operator: "equals",
rightOperand: { type: "static", value: "nv1" },
},
],
};
const group: TConditionGroup = {
id: "parent",
connector: "and",
conditions: [nestedGroup],
};
const result = removeCondition(group, "nonexistent");
expect(result).toBe(false);
expect(group.conditions).toHaveLength(1);
});
test("removeCondition successfully removes from nested groups and cleans up", () => {
const nestedGroup: TConditionGroup = {
id: "nested",
connector: "and",
conditions: [
{
id: "nestedC1",
leftOperand: { type: "hiddenField", value: "nf1" },
operator: "equals",
rightOperand: { type: "static", value: "nv1" },
},
{
id: "nestedC2",
leftOperand: { type: "hiddenField", value: "nf2" },
operator: "equals",
rightOperand: { type: "static", value: "nv2" },
},
],
};
const otherCondition: TSingleCondition = {
id: "otherCondition",
leftOperand: { type: "hiddenField", value: "other" },
operator: "equals",
rightOperand: { type: "static", value: "value" },
};
const group: TConditionGroup = {
id: "parent",
connector: "and",
conditions: [nestedGroup, otherCondition],
};
const result = removeCondition(group, "nestedC1");
expect(result).toBe(true);
expect(group.conditions).toHaveLength(2);
expect((group.conditions[0] as TConditionGroup).conditions).toHaveLength(1);
expect((group.conditions[0] as TConditionGroup).conditions[0].id).toBe("nestedC2");
expect(group.conditions[1].id).toBe("otherCondition");
});
test("removeCondition flattens group when nested group has only one condition left", () => {
const deeplyNestedGroup: TConditionGroup = {
id: "deepNested",
connector: "or",
conditions: [
{
id: "deepC1",
leftOperand: { type: "hiddenField", value: "df1" },
operator: "equals",
rightOperand: { type: "static", value: "dv1" },
},
],
};
const nestedGroup: TConditionGroup = {
id: "nested",
connector: "and",
conditions: [
{
id: "nestedC1",
leftOperand: { type: "hiddenField", value: "nf1" },
operator: "equals",
rightOperand: { type: "static", value: "nv1" },
},
deeplyNestedGroup,
],
};
const otherCondition: TSingleCondition = {
id: "otherCondition",
leftOperand: { type: "hiddenField", value: "other" },
operator: "equals",
rightOperand: { type: "static", value: "value" },
};
const group: TConditionGroup = {
id: "parent",
connector: "and",
conditions: [nestedGroup, otherCondition],
};
// Remove the regular condition, leaving only the deeply nested group in the nested group
const result = removeCondition(group, "nestedC1");
expect(result).toBe(true);
// The parent group should still have 2 conditions: the nested group and the other condition
expect(group.conditions).toHaveLength(2);
// The nested group should still be there but now contain only the deeply nested group
expect(group.conditions[0].id).toBe("nested");
expect((group.conditions[0] as TConditionGroup).conditions).toHaveLength(1);
// The nested group should contain the flattened content from the deeply nested group
expect((group.conditions[0] as TConditionGroup).conditions[0].id).toBe("deepC1");
expect(group.conditions[1].id).toBe("otherCondition");
});
test("removeCondition removes empty groups after cleanup", () => {
const emptyNestedGroup: TConditionGroup = {
id: "emptyNested",
connector: "and",
conditions: [
{
id: "toBeRemoved",
leftOperand: { type: "hiddenField", value: "f1" },
operator: "equals",
rightOperand: { type: "static", value: "v1" },
},
],
};
const group: TConditionGroup = {
id: "parent",
connector: "and",
conditions: [
emptyNestedGroup,
{
id: "keepThis",
leftOperand: { type: "hiddenField", value: "f2" },
operator: "equals",
rightOperand: { type: "static", value: "v2" },
},
],
};
// Remove the only condition from the nested group
const result = removeCondition(group, "toBeRemoved");
expect(result).toBe(true);
// The empty nested group should be removed, leaving only the other condition
expect(group.conditions).toHaveLength(1);
expect(group.conditions[0].id).toBe("keepThis");
});
test("deleteEmptyGroups with complex nested structure", () => {
const deepEmptyGroup: TConditionGroup = { id: "deepEmpty", connector: "and", conditions: [] };
const middleGroup: TConditionGroup = {
id: "middle",
connector: "or",
conditions: [deepEmptyGroup],
};
const topGroup: TConditionGroup = {
id: "top",
connector: "and",
conditions: [
middleGroup,
{
id: "validCondition",
leftOperand: { type: "hiddenField", value: "f" },
operator: "equals",
rightOperand: { type: "static", value: "v" },
},
],
};
deleteEmptyGroups(topGroup);
// Should remove the nested empty groups and keep only the valid condition
expect(topGroup.conditions).toHaveLength(1);
expect(topGroup.conditions[0].id).toBe("validCondition");
});
// Additional tests for complete coverage
test("addConditionBelow with nested group correctly adds condition", () => {

View File

@@ -94,48 +94,21 @@ export const toggleGroupConnector = (group: TConditionGroup, resourceId: string)
}
};
export const removeCondition = (group: TConditionGroup, resourceId: string): boolean => {
for (let i = group.conditions.length - 1; i >= 0; i--) {
export const removeCondition = (group: TConditionGroup, resourceId: string) => {
for (let i = 0; i < group.conditions.length; i++) {
const item = group.conditions[i];
if (item.id === resourceId) {
group.conditions.splice(i, 1);
cleanupGroup(group);
return true;
return;
}
if (isConditionGroup(item) && removeCondition(item, resourceId)) {
cleanupGroup(group);
return true;
if (isConditionGroup(item)) {
removeCondition(item, resourceId);
}
}
return false;
};
const cleanupGroup = (group: TConditionGroup) => {
// Remove empty condition groups first
for (let i = group.conditions.length - 1; i >= 0; i--) {
const condition = group.conditions[i];
if (isConditionGroup(condition)) {
cleanupGroup(condition);
// Remove if empty after cleanup
if (condition.conditions.length === 0) {
group.conditions.splice(i, 1);
}
}
}
// Flatten if group has only one condition and it's a condition group
if (group.conditions.length === 1 && isConditionGroup(group.conditions[0])) {
group.connector = group.conditions[0].connector || "and";
group.conditions = group.conditions[0].conditions;
}
};
export const deleteEmptyGroups = (group: TConditionGroup) => {
cleanupGroup(group);
deleteEmptyGroups(group);
};
export const duplicateCondition = (group: TConditionGroup, resourceId: string) => {
@@ -157,6 +130,18 @@ export const duplicateCondition = (group: TConditionGroup, resourceId: string) =
}
};
export const deleteEmptyGroups = (group: TConditionGroup) => {
for (let i = 0; i < group.conditions.length; i++) {
const resource = group.conditions[i];
if (isConditionGroup(resource) && resource.conditions.length === 0) {
group.conditions.splice(i, 1);
} else if (isConditionGroup(resource)) {
deleteEmptyGroups(resource);
}
}
};
export const createGroupFromResource = (group: TConditionGroup, resourceId: string) => {
for (let i = 0; i < group.conditions.length; i++) {
const item = group.conditions[i];
@@ -685,9 +670,8 @@ const performCalculation = (
if (typeof val === "number" || typeof val === "string") {
if (variable.type === "number" && !isNaN(Number(val))) {
operandValue = Number(val);
} else {
operandValue = val;
}
operandValue = val;
}
break;
}

View File

@@ -747,7 +747,6 @@
"api_key_label": "API-Schlüssel Label",
"api_key_security_warning": "Aus Sicherheitsgründen wird der API-Schlüssel nur einmal nach der Erstellung angezeigt. Bitte kopiere ihn sofort an einen sicheren Ort.",
"api_key_updated": "API-Schlüssel aktualisiert",
"delete_permission": "Berechtigung löschen",
"duplicate_access": "Doppelter Projektzugriff nicht erlaubt",
"no_api_keys_yet": "Du hast noch keine API-Schlüssel",
"no_env_permissions_found": "Keine Umgebungsberechtigungen gefunden",

View File

@@ -747,7 +747,6 @@
"api_key_label": "API Key Label",
"api_key_security_warning": "For security reasons, the API key will only be shown once after creation. Please copy it to your destination right away.",
"api_key_updated": "API Key updated",
"delete_permission": "Delete permission",
"duplicate_access": "Duplicate project access not allowed",
"no_api_keys_yet": "You don't have any API keys yet",
"no_env_permissions_found": "No environment permissions found",

View File

@@ -747,7 +747,6 @@
"api_key_label": "Étiquette de clé API",
"api_key_security_warning": "Pour des raisons de sécurité, la clé API ne sera affichée qu'une seule fois après sa création. Veuillez la copier immédiatement à votre destination.",
"api_key_updated": "Clé API mise à jour",
"delete_permission": "Supprimer une permission",
"duplicate_access": "L'accès en double au projet n'est pas autorisé",
"no_api_keys_yet": "Vous n'avez pas encore de clés API.",
"no_env_permissions_found": "Aucune autorisation d'environnement trouvée",

File diff suppressed because it is too large Load Diff

View File

@@ -747,7 +747,6 @@
"api_key_label": "Rótulo da Chave API",
"api_key_security_warning": "Por motivos de segurança, a chave da API será mostrada apenas uma vez após a criação. Por favor, copie-a para o seu destino imediatamente.",
"api_key_updated": "Chave de API atualizada",
"delete_permission": "Remover permissão",
"duplicate_access": "Acesso duplicado ao projeto não permitido",
"no_api_keys_yet": "Você ainda não tem nenhuma chave de API",
"no_env_permissions_found": "Nenhuma permissão de ambiente encontrada",

View File

@@ -747,7 +747,6 @@
"api_key_label": "Etiqueta da Chave API",
"api_key_security_warning": "Por razões de segurança, a chave API será mostrada apenas uma vez após a criação. Por favor, copie-a para o seu destino imediatamente.",
"api_key_updated": "Chave API atualizada",
"delete_permission": "Eliminar permissão",
"duplicate_access": "Acesso duplicado ao projeto não permitido",
"no_api_keys_yet": "Ainda não tem nenhuma chave API",
"no_env_permissions_found": "Nenhuma permissão de ambiente encontrada",

View File

@@ -747,7 +747,6 @@
"api_key_label": "Etichetă Cheie API",
"api_key_security_warning": "Din motive de securitate, cheia API va fi afișată o singură dată după creare. Vă rugăm să o copiați imediat la destinație.",
"api_key_updated": "Cheie API actualizată",
"delete_permission": "Șterge permisiunea",
"duplicate_access": "Accesul dublu la proiect nu este permis",
"no_api_keys_yet": "Nu aveți încă chei API",
"no_env_permissions_found": "Nu s-au găsit permisiuni pentru mediu",

View File

@@ -747,7 +747,6 @@
"api_key_label": "API 金鑰標籤",
"api_key_security_warning": "為安全起見API 金鑰僅在建立後顯示一次。請立即將其複製到您的目的地。",
"api_key_updated": "API 金鑰已更新",
"delete_permission": "刪除 權限",
"duplicate_access": "不允許重複的 project 存取",
"no_api_keys_yet": "您還沒有任何 API 金鑰",
"no_env_permissions_found": "找不到環境權限",

View File

@@ -92,7 +92,7 @@ describe("contact-survey page", () => {
params: Promise.resolve({ jwt: "token" }),
searchParams: Promise.resolve({}),
});
expect(meta).toEqual({ title: "Survey", description: "Please complete this survey." });
expect(meta).toEqual({ title: "Survey", description: "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: "Please complete this survey." });
expect(meta).toEqual({ title: "Survey", description: "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: "Please complete this survey.",
description: "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: "Please complete this survey.",
description: "Complete this survey",
};
}
};

View File

@@ -77,7 +77,7 @@ describe("Metadata Utils", () => {
expect(getSurvey).toHaveBeenCalledWith(mockSurveyId);
expect(result).toEqual({
title: "Survey",
description: "Please complete this survey.",
description: "Complete this survey",
survey: null,
ogImage: undefined,
});
@@ -108,9 +108,10 @@ describe("Metadata Utils", () => {
const result = await getBasicSurveyMetadata(mockSurveyId);
expect(getSurvey).toHaveBeenCalledWith(mockSurveyId);
expect(getProjectByEnvironmentId).toHaveBeenCalledWith(mockEnvironmentId);
expect(result).toEqual({
title: "Welcome Headline",
description: "Please complete this survey.",
title: "Welcome Headline | Test Project",
description: "Complete this survey",
survey: mockSurvey,
ogImage: undefined,
});
@@ -128,12 +129,13 @@ 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",
description: "Please complete this survey.",
title: "Test Survey | Test Project",
description: "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,16 +12,22 @@ type TBasicSurveyMetadata = {
ogImage?: string;
};
export const getNameForURL = (value: string) => encodeURIComponent(value);
/**
* Utility function to encode name for URL usage
*/
export const getNameForURL = (url: string) => url.replace(/ /g, "%20");
export const getBrandColorForURL = (value: string) => encodeURIComponent(value);
/**
* Utility function to encode brand color for URL usage
*/
export const getBrandColorForURL = (url: string) => url.replace(/#/g, "%23");
/**
* Get basic survey metadata (title and description) based on link metadata, welcome card or survey name
*/
export const getBasicSurveyMetadata = async (
surveyId: string,
languageCode = "default"
languageCode?: string
): Promise<TBasicSurveyMetadata> => {
const survey = await getSurvey(surveyId);
@@ -29,7 +35,7 @@ export const getBasicSurveyMetadata = async (
if (!survey) {
return {
title: "Survey",
description: "Please complete this survey.",
description: "Complete this survey",
survey: null,
ogImage: undefined,
};
@@ -37,33 +43,38 @@ 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 = useDefaultLanguageCode ? "default" : languageCode;
const langCode = languageCode || "default";
// Set title - priority: custom link metadata > welcome card > 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;
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;
}
// Set description - priority: custom link metadata > welcome card > default
const descriptionFromMetadata = metadata?.description
? getLocalizedValue(metadata.description, langCode) || ""
: undefined;
let description = descriptionFromMetadata || "Please complete this survey.";
let description = "Complete this survey";
if (metadata.description?.[langCode]) {
description = metadata.description[langCode];
}
// Get OG image from link metadata if available
const { ogImage } = metadata;
if (!titleFromMetadata) {
// Add product name in title if it's Formbricks cloud and not using custom metadata
if (!metadata.title?.[langCode]) {
if (IS_FORMBRICKS_CLOUD) {
title = `${title} | Formbricks`;
} else {
const project = await getProjectByEnvironmentId(survey.environmentId);
if (project) {
title = `${title} | ${project.name}`;
}
}
}
@@ -78,13 +89,10 @@ export const getBasicSurveyMetadata = async (
/**
* Generate Open Graph metadata for survey
*/
export const getSurveyOpenGraphMetadata = (
surveyId: string,
surveyName: string,
surveyBrandColor?: string
): Metadata => {
export const getSurveyOpenGraphMetadata = (surveyId: string, surveyName: string): Metadata => {
const brandColor = getBrandColorForURL(COLOR_DEFAULTS.brandColor); // Default color
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 = "Please complete this survey.";
const mockDescription = "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, undefined);
expect(getSurveyOpenGraphMetadata).toHaveBeenCalledWith(mockSurveyId, mockSurveyName);
expect(result).toEqual({
title: mockSurveyName,

View File

@@ -15,10 +15,9 @@ 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, surveyBrandColor);
const baseMetadata = getSurveyOpenGraphMetadata(survey.id, title);
// Override with the custom image URL
if (baseMetadata.openGraph) {

View File

@@ -233,31 +233,12 @@ describe("ConditionsEditor", () => {
expect(mockCallbacks.onDuplicateCondition).toHaveBeenCalledWith("cond1");
});
test("calls onCreateGroup from the dropdown menu when enabled", async () => {
test("calls onCreateGroup from the dropdown menu", async () => {
const user = userEvent.setup();
render(
<ConditionsEditor conditions={multipleConditions} config={mockConfig} callbacks={mockCallbacks} />
);
const createGroupButtons = screen.getAllByText("environments.surveys.edit.create_group");
await user.click(createGroupButtons[0]); // Click the first one
expect(mockCallbacks.onCreateGroup).toHaveBeenCalledWith("cond1");
});
test("disables the 'Create Group' button when there's only one condition", () => {
render(<ConditionsEditor conditions={singleCondition} config={mockConfig} callbacks={mockCallbacks} />);
const createGroupButton = screen.getByText("environments.surveys.edit.create_group");
expect(createGroupButton).toBeDisabled();
});
test("enables the 'Create Group' button when there are multiple conditions", () => {
render(
<ConditionsEditor conditions={multipleConditions} config={mockConfig} callbacks={mockCallbacks} />
);
const createGroupButtons = screen.getAllByText("environments.surveys.edit.create_group");
// Both buttons should be enabled since the main group has multiple conditions
createGroupButtons.forEach((button) => {
expect(button).not.toBeDisabled();
});
await user.click(createGroupButton);
expect(mockCallbacks.onCreateGroup).toHaveBeenCalledWith("cond1");
});
test("calls onToggleGroupConnector when the connector is changed", async () => {

View File

@@ -233,8 +233,7 @@ export function ConditionsEditor({ conditions, config, callbacks, depth = 0 }: C
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => callbacks.onCreateGroup(condition.id)}
icon={<WorkflowIcon className="h-4 w-4" />}
disabled={conditions.conditions.length <= 1}>
icon={<WorkflowIcon className="h-4 w-4" />}>
{t("environments.surveys.edit.create_group")}
</DropdownMenuItem>
</DropdownMenuContent>

View File

@@ -256,7 +256,7 @@ EOT
fi
echo "📥 Downloading docker-compose.yml from Formbricks GitHub repository..."
curl -o docker-compose.yml https://raw.githubusercontent.com/formbricks/formbricks/stable/docker/docker-compose.yml
curl -o docker-compose.yml https://raw.githubusercontent.com/formbricks/formbricks/main/docker/docker-compose.yml
echo "🚙 Updating docker-compose.yml with your custom inputs..."
sed -i "/WEBAPP_URL:/s|WEBAPP_URL:.*|WEBAPP_URL: \"https://$domain_name\"|" docker-compose.yml