mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-22 06:00:51 -06:00
Compare commits
7 Commits
fix/v2-api
...
fix/docker
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04bbb129e5 | ||
|
|
0bcd85d658 | ||
|
|
f59df49588 | ||
|
|
f08fabfb13 | ||
|
|
ee8af9dd74 | ||
|
|
1091b40bd1 | ||
|
|
87a2d727ed |
99
.github/workflows/build-and-push-ecr.yml
vendored
Normal file
99
.github/workflows/build-and-push-ecr.yml
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
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 }}
|
||||
@@ -37,7 +37,7 @@ on:
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
helmfile-deploy:
|
||||
|
||||
17
.github/workflows/formbricks-release.yml
vendored
17
.github/workflows/formbricks-release.yml
vendored
@@ -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' }}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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.",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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"
|
||||
)}>
|
||||
|
||||
@@ -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,6 +109,7 @@ describe("surveyLogic", () => {
|
||||
languages: [],
|
||||
triggers: [],
|
||||
segment: null,
|
||||
recaptcha: null,
|
||||
};
|
||||
|
||||
const simpleGroup = (): TConditionGroup => ({
|
||||
@@ -175,7 +176,8 @@ describe("surveyLogic", () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
removeCondition(group, "c");
|
||||
const result = removeCondition(group, "c");
|
||||
expect(result).toBe(true);
|
||||
expect(group.conditions).toHaveLength(0);
|
||||
});
|
||||
|
||||
@@ -433,6 +435,8 @@ 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")
|
||||
@@ -510,7 +514,8 @@ describe("surveyLogic", () => {
|
||||
expect(group.conditions.length).toBe(2);
|
||||
toggleGroupConnector(group, "notfound");
|
||||
expect(group.connector).toBe("and");
|
||||
removeCondition(group, "notfound");
|
||||
const result = removeCondition(group, "notfound");
|
||||
expect(result).toBe(false);
|
||||
expect(group.conditions.length).toBe(2);
|
||||
duplicateCondition(group, "notfound");
|
||||
expect(group.conditions.length).toBe(2);
|
||||
@@ -520,6 +525,192 @@ 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", () => {
|
||||
|
||||
@@ -94,21 +94,48 @@ export const toggleGroupConnector = (group: TConditionGroup, resourceId: string)
|
||||
}
|
||||
};
|
||||
|
||||
export const removeCondition = (group: TConditionGroup, resourceId: string) => {
|
||||
for (let i = 0; i < group.conditions.length; i++) {
|
||||
export const removeCondition = (group: TConditionGroup, resourceId: string): boolean => {
|
||||
for (let i = group.conditions.length - 1; i >= 0; i--) {
|
||||
const item = group.conditions[i];
|
||||
|
||||
if (item.id === resourceId) {
|
||||
group.conditions.splice(i, 1);
|
||||
return;
|
||||
cleanupGroup(group);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isConditionGroup(item)) {
|
||||
removeCondition(item, resourceId);
|
||||
if (isConditionGroup(item) && removeCondition(item, resourceId)) {
|
||||
cleanupGroup(group);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
deleteEmptyGroups(group);
|
||||
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);
|
||||
};
|
||||
|
||||
export const duplicateCondition = (group: TConditionGroup, resourceId: string) => {
|
||||
@@ -130,18 +157,6 @@ 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];
|
||||
@@ -670,8 +685,9 @@ 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;
|
||||
}
|
||||
|
||||
@@ -141,7 +141,6 @@
|
||||
"apply_filters": "Filter anwenden",
|
||||
"are_you_sure": "Bist Du sicher?",
|
||||
"attributes": "Attribute",
|
||||
"avatar": "Avatar",
|
||||
"back": "Zurück",
|
||||
"billing": "Abrechnung",
|
||||
"booked": "Gebucht",
|
||||
@@ -748,6 +747,7 @@
|
||||
"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",
|
||||
@@ -1111,9 +1111,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"account_deletion_consequences_warning": "Was passiert, wenn Du das Konto löschst",
|
||||
"avatar_update_failed": "Aktualisierung des Avatars fehlgeschlagen. Bitte versuche es erneut.",
|
||||
"backup_code": "Backup-Code",
|
||||
"change_image": "Bild ändern",
|
||||
"confirm_delete_account": "Lösche dein Konto mit all deinen persönlichen Informationen und Daten",
|
||||
"confirm_delete_my_account": "Konto löschen",
|
||||
"confirm_your_current_password_to_get_started": "Bestätige dein aktuelles Passwort, um loszulegen.",
|
||||
@@ -1124,17 +1122,13 @@
|
||||
"email_change_initiated": "Deine Anfrage zur Änderung der E-Mail wurde eingeleitet.",
|
||||
"enable_two_factor_authentication": "Zwei-Faktor-Authentifizierung aktivieren",
|
||||
"enter_the_code_from_your_authenticator_app_below": "Gib den Code aus deiner Authentifizierungs-App unten ein.",
|
||||
"file_size_must_be_less_than_10mb": "Dateigröße muss weniger als 10MB sein.",
|
||||
"invalid_file_type": "Ungültiger Dateityp. Nur JPEG-, PNG- und WEBP-Dateien sind erlaubt.",
|
||||
"lost_access": "Zugriff verloren",
|
||||
"or_enter_the_following_code_manually": "Oder gib den folgenden Code manuell ein:",
|
||||
"organization_identification": "Hilf deiner Organisation, Dich auf Formbricks zu identifizieren",
|
||||
"organizations_delete_message": "Du bist der einzige Besitzer dieser Organisationen, also werden sie <b>auch gelöscht.</b>",
|
||||
"permanent_removal_of_all_of_your_personal_information_and_data": "Dauerhafte Entfernung all deiner persönlichen Informationen und Daten",
|
||||
"personal_information": "Persönliche Informationen",
|
||||
"please_enter_email_to_confirm_account_deletion": "Bitte gib {email} in das folgende Feld ein, um die endgültige Löschung deines Kontos zu bestätigen:",
|
||||
"profile_updated_successfully": "Dein Profil wurde erfolgreich aktualisiert",
|
||||
"remove_image": "Bild entfernen",
|
||||
"save_the_following_backup_codes_in_a_safe_place": "Speichere die folgenden Backup-Codes an einem sicheren Ort.",
|
||||
"scan_the_qr_code_below_with_your_authenticator_app": "Scanne den QR-Code unten mit deiner Authentifizierungs-App.",
|
||||
"security_description": "Verwalte dein Passwort und andere Sicherheitseinstellungen wie Zwei-Faktor-Authentifizierung (2FA).",
|
||||
@@ -1144,10 +1138,8 @@
|
||||
"two_factor_code": "Zwei-Faktor-Code",
|
||||
"unlock_two_factor_authentication": "Zwei-Faktor-Authentifizierung mit einem höheren Plan freischalten",
|
||||
"update_personal_info": "Persönliche Daten aktualisieren",
|
||||
"upload_image": "Bild hochladen",
|
||||
"warning_cannot_delete_account": "Du bist der einzige Besitzer dieser Organisation. Bitte übertrage das Eigentum zuerst an ein anderes Mitglied.",
|
||||
"warning_cannot_undo": "Das kann nicht rückgängig gemacht werden",
|
||||
"you_must_select_a_file": "Du musst eine Datei auswählen."
|
||||
"warning_cannot_undo": "Das kann nicht rückgängig gemacht werden"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Füge Mitglieder zum Team hinzu und bestimme ihre Rolle.",
|
||||
@@ -1715,10 +1707,8 @@
|
||||
"language_help_text": "Die Meta-Daten werden basierend auf dem `lang` Wert in der URL geladen.",
|
||||
"link_description": "Linkbeschreibung",
|
||||
"link_description_description": "Beschreibung mit 55-200 Zeichen funktionieren am besten.",
|
||||
"link_description_placeholder": "Hilf uns, indem du deine Gedanken teilst.",
|
||||
"link_title": "Linktitel",
|
||||
"link_title_description": "Kurze Titel funktionieren am besten als Meta-Titel.",
|
||||
"link_title_placeholder": "Kundenfeedback-Umfrage",
|
||||
"preview_image": "Vorschaubild",
|
||||
"preview_image_description": "Querformatige Bilder mit kleiner Dateigröße (<4MB) funktionieren am besten.",
|
||||
"title": "Link-Einstellungen"
|
||||
|
||||
@@ -141,7 +141,6 @@
|
||||
"apply_filters": "Apply filters",
|
||||
"are_you_sure": "Are you sure?",
|
||||
"attributes": "Attributes",
|
||||
"avatar": "Avatar",
|
||||
"back": "Back",
|
||||
"billing": "Billing",
|
||||
"booked": "Booked",
|
||||
@@ -748,6 +747,7 @@
|
||||
"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",
|
||||
@@ -1111,9 +1111,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"account_deletion_consequences_warning": "Account deletion consequences",
|
||||
"avatar_update_failed": "Avatar update failed. Please try again.",
|
||||
"backup_code": "Backup Code",
|
||||
"change_image": "Change image",
|
||||
"confirm_delete_account": "Delete your account with all of your personal information and data",
|
||||
"confirm_delete_my_account": "Delete My Account",
|
||||
"confirm_your_current_password_to_get_started": "Confirm your current password to get started.",
|
||||
@@ -1124,17 +1122,13 @@
|
||||
"email_change_initiated": "Your email change request has been initiated.",
|
||||
"enable_two_factor_authentication": "Enable two factor authentication",
|
||||
"enter_the_code_from_your_authenticator_app_below": "Enter the code from your authenticator app below.",
|
||||
"file_size_must_be_less_than_10mb": "File size must be less than 10MB.",
|
||||
"invalid_file_type": "Invalid file type. Only JPEG, PNG, and WEBP files are allowed.",
|
||||
"lost_access": "Lost access",
|
||||
"or_enter_the_following_code_manually": "Or enter the following code manually:",
|
||||
"organization_identification": "Assist your organization in identifying you on Formbricks",
|
||||
"organizations_delete_message": "You are the only owner of these organizations, so they <b>will be deleted as well.</b>",
|
||||
"permanent_removal_of_all_of_your_personal_information_and_data": "Permanent removal of all of your personal information and data",
|
||||
"personal_information": "Personal information",
|
||||
"please_enter_email_to_confirm_account_deletion": "Please enter {email} in the following field to confirm the definitive deletion of your account:",
|
||||
"profile_updated_successfully": "Your profile was updated successfully",
|
||||
"remove_image": "Remove image",
|
||||
"save_the_following_backup_codes_in_a_safe_place": "Save the following backup codes in a safe place.",
|
||||
"scan_the_qr_code_below_with_your_authenticator_app": "Scan the QR code below with your authenticator app.",
|
||||
"security_description": "Manage your password and other security settings like two-factor authentication (2FA).",
|
||||
@@ -1144,10 +1138,8 @@
|
||||
"two_factor_code": "Two-Factor Code",
|
||||
"unlock_two_factor_authentication": "Unlock two-factor authentication with a higher plan",
|
||||
"update_personal_info": "Update your personal information",
|
||||
"upload_image": "Upload image",
|
||||
"warning_cannot_delete_account": "You are the only owner of this organization. Please transfer ownership to another member first.",
|
||||
"warning_cannot_undo": "This cannot be undone",
|
||||
"you_must_select_a_file": "You must select a file."
|
||||
"warning_cannot_undo": "This cannot be undone"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Add members to the team and determine their role.",
|
||||
@@ -1715,10 +1707,8 @@
|
||||
"language_help_text": "The meta data is loaded based on the `lang` value in the URL.",
|
||||
"link_description": "Link description",
|
||||
"link_description_description": "Descriptions between 55-200 characters perform best.",
|
||||
"link_description_placeholder": "Help us improve by sharing your thoughts.",
|
||||
"link_title": "Link title",
|
||||
"link_title_description": "Short titles perform best as Meta Titles.",
|
||||
"link_title_placeholder": "Customer Feedback Survey",
|
||||
"preview_image": "Preview image",
|
||||
"preview_image_description": "Landscape images with small file sizes (<4MB) perform best.",
|
||||
"title": "Link settings"
|
||||
|
||||
@@ -141,7 +141,6 @@
|
||||
"apply_filters": "Appliquer des filtres",
|
||||
"are_you_sure": "Es-tu sûr ?",
|
||||
"attributes": "Attributs",
|
||||
"avatar": "Avatar",
|
||||
"back": "Retour",
|
||||
"billing": "Facturation",
|
||||
"booked": "Réservé",
|
||||
@@ -748,6 +747,7 @@
|
||||
"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",
|
||||
@@ -1111,9 +1111,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"account_deletion_consequences_warning": "Conséquences de la suppression de compte",
|
||||
"avatar_update_failed": "La mise à jour de l'avatar a échoué. Veuillez réessayer.",
|
||||
"backup_code": "Code de sauvegarde",
|
||||
"change_image": "Changer l'image",
|
||||
"confirm_delete_account": "Supprimez votre compte avec toutes vos informations personnelles et données.",
|
||||
"confirm_delete_my_account": "Supprimer mon compte",
|
||||
"confirm_your_current_password_to_get_started": "Confirmez votre mot de passe actuel pour commencer.",
|
||||
@@ -1124,17 +1122,13 @@
|
||||
"email_change_initiated": "Votre demande de changement d'email a été initiée.",
|
||||
"enable_two_factor_authentication": "Activer l'authentification à deux facteurs",
|
||||
"enter_the_code_from_your_authenticator_app_below": "Entrez le code de votre application d'authentification ci-dessous.",
|
||||
"file_size_must_be_less_than_10mb": "La taille du fichier doit être inférieure à 10 Mo.",
|
||||
"invalid_file_type": "Type de fichier invalide. Seuls les fichiers JPEG, PNG et WEBP sont autorisés.",
|
||||
"lost_access": "Accès perdu",
|
||||
"or_enter_the_following_code_manually": "Ou entrez le code suivant manuellement :",
|
||||
"organization_identification": "Aidez votre organisation à vous identifier sur Formbricks",
|
||||
"organizations_delete_message": "Tu es le seul propriétaire de ces organisations, elles <b>seront aussi supprimées.</b>",
|
||||
"permanent_removal_of_all_of_your_personal_information_and_data": "Suppression permanente de toutes vos informations et données personnelles.",
|
||||
"personal_information": "Informations personnelles",
|
||||
"please_enter_email_to_confirm_account_deletion": "Veuillez entrer {email} dans le champ suivant pour confirmer la suppression définitive de votre compte :",
|
||||
"profile_updated_successfully": "Votre profil a été mis à jour avec succès.",
|
||||
"remove_image": "Supprimer l'image",
|
||||
"save_the_following_backup_codes_in_a_safe_place": "Enregistrez les codes de sauvegarde suivants dans un endroit sûr.",
|
||||
"scan_the_qr_code_below_with_your_authenticator_app": "Scannez le code QR ci-dessous avec votre application d'authentification.",
|
||||
"security_description": "Gérez votre mot de passe et d'autres paramètres de sécurité comme l'authentification à deux facteurs (2FA).",
|
||||
@@ -1144,10 +1138,8 @@
|
||||
"two_factor_code": "Code à deux facteurs",
|
||||
"unlock_two_factor_authentication": "Débloquez l'authentification à deux facteurs avec une offre supérieure",
|
||||
"update_personal_info": "Mettez à jour vos informations personnelles",
|
||||
"upload_image": "Télécharger l'image",
|
||||
"warning_cannot_delete_account": "Tu es le seul propriétaire de cette organisation. Transfère la propriété à un autre membre d'abord.",
|
||||
"warning_cannot_undo": "Ceci ne peut pas être annulé",
|
||||
"you_must_select_a_file": "Vous devez sélectionner un fichier."
|
||||
"warning_cannot_undo": "Ceci ne peut pas être annulé"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Ajoutez des membres à l'équipe et déterminez leur rôle.",
|
||||
@@ -1715,10 +1707,8 @@
|
||||
"language_help_text": "Les métadonnées sont chargées en fonction de la valeur « lang » dans l'URL.",
|
||||
"link_description": "Description du lien",
|
||||
"link_description_description": "« Les descriptions entre 55 et 200 caractères donnent les meilleurs résultats. »",
|
||||
"link_description_placeholder": "Aidez-nous à nous améliorer en partageant vos pensées.",
|
||||
"link_title": "Titre du lien",
|
||||
"link_title_description": "Les titres courts fonctionnent mieux comme titres méta.",
|
||||
"link_title_placeholder": "Sondage de Retour Clients",
|
||||
"preview_image": "Aperçu de l'image",
|
||||
"preview_image_description": "Les images en paysage avec de petites tailles de fichier (<4MB) fonctionnent le mieux.",
|
||||
"title": "Paramètres de lien"
|
||||
|
||||
2865
apps/web/locales/ja-JP.json
Normal file
2865
apps/web/locales/ja-JP.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -141,7 +141,6 @@
|
||||
"apply_filters": "Aplicar filtros",
|
||||
"are_you_sure": "Certeza?",
|
||||
"attributes": "atributos",
|
||||
"avatar": "Avatar",
|
||||
"back": "Voltar",
|
||||
"billing": "Faturamento",
|
||||
"booked": "Reservado",
|
||||
@@ -748,6 +747,7 @@
|
||||
"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",
|
||||
@@ -1111,9 +1111,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"account_deletion_consequences_warning": "Consequências da exclusão da conta",
|
||||
"avatar_update_failed": "Falha ao atualizar o avatar. Por favor, tente novamente.",
|
||||
"backup_code": "Código de Backup",
|
||||
"change_image": "Mudar imagem",
|
||||
"confirm_delete_account": "Apague sua conta com todas as suas informações pessoais e dados",
|
||||
"confirm_delete_my_account": "Excluir Minha Conta",
|
||||
"confirm_your_current_password_to_get_started": "Confirme sua senha atual para começar.",
|
||||
@@ -1124,17 +1122,13 @@
|
||||
"email_change_initiated": "Sua solicitação de alteração de e-mail foi iniciada.",
|
||||
"enable_two_factor_authentication": "Ativar autenticação de dois fatores",
|
||||
"enter_the_code_from_your_authenticator_app_below": "Digite o código do seu app autenticador abaixo.",
|
||||
"file_size_must_be_less_than_10mb": "O tamanho do arquivo deve ser menor que 10MB.",
|
||||
"invalid_file_type": "Tipo de arquivo inválido. Só são permitidos arquivos JPEG, PNG e WEBP.",
|
||||
"lost_access": "Perdi o acesso",
|
||||
"or_enter_the_following_code_manually": "Ou insira o seguinte código manualmente:",
|
||||
"organization_identification": "Ajude sua organização a te identificar no Formbricks",
|
||||
"organizations_delete_message": "Você é o único dono dessas organizações, então elas <b>também serão apagadas.</b>",
|
||||
"permanent_removal_of_all_of_your_personal_information_and_data": "Remoção permanente de todas as suas informações e dados pessoais",
|
||||
"personal_information": "Informações pessoais",
|
||||
"please_enter_email_to_confirm_account_deletion": "Por favor, insira {email} no campo abaixo para confirmar a exclusão definitiva da sua conta:",
|
||||
"profile_updated_successfully": "Seu perfil foi atualizado com sucesso",
|
||||
"remove_image": "Remover imagem",
|
||||
"save_the_following_backup_codes_in_a_safe_place": "Guarde os seguintes códigos de backup em um lugar seguro.",
|
||||
"scan_the_qr_code_below_with_your_authenticator_app": "Escaneie o código QR abaixo com seu app autenticador.",
|
||||
"security_description": "Gerencie sua senha e outras configurações de segurança como a autenticação de dois fatores (2FA).",
|
||||
@@ -1144,10 +1138,8 @@
|
||||
"two_factor_code": "Código de Dois Fatores",
|
||||
"unlock_two_factor_authentication": "Desbloqueia a autenticação de dois fatores com um plano melhor",
|
||||
"update_personal_info": "Atualize suas informações pessoais",
|
||||
"upload_image": "Enviar imagem",
|
||||
"warning_cannot_delete_account": "Você é o único dono desta organização. Transfere a propriedade para outra pessoa primeiro.",
|
||||
"warning_cannot_undo": "Isso não pode ser desfeito",
|
||||
"you_must_select_a_file": "Você tem que selecionar um arquivo."
|
||||
"warning_cannot_undo": "Isso não pode ser desfeito"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Adicione membros à equipe e determine sua função.",
|
||||
@@ -1715,10 +1707,8 @@
|
||||
"language_help_text": "Os metadados são carregados com base no valor `lang` na URL.",
|
||||
"link_description": "Descrição do link",
|
||||
"link_description_description": "\"Descrições entre 55-200 caracteres têm um melhor desempenho.\"",
|
||||
"link_description_placeholder": "Ajude-nos a melhorar compartilhando suas opiniões.",
|
||||
"link_title": "Título do link",
|
||||
"link_title_description": "Títulos curtos têm melhor desempenho como Meta Títulos.",
|
||||
"link_title_placeholder": "Pesquisa de Feedback do Cliente",
|
||||
"preview_image": "Imagem de prévia",
|
||||
"preview_image_description": "Imagens em paisagem com tamanhos de arquivo pequenos (<4MB) têm o melhor desempenho.",
|
||||
"title": "Configurações de link"
|
||||
|
||||
@@ -141,7 +141,6 @@
|
||||
"apply_filters": "Aplicar filtros",
|
||||
"are_you_sure": "Tem a certeza?",
|
||||
"attributes": "Atributos",
|
||||
"avatar": "Avatar",
|
||||
"back": "Voltar",
|
||||
"billing": "Faturação",
|
||||
"booked": "Reservado",
|
||||
@@ -748,6 +747,7 @@
|
||||
"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",
|
||||
@@ -1111,9 +1111,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"account_deletion_consequences_warning": "Consequências da eliminação da conta",
|
||||
"avatar_update_failed": "Falha na atualização do avatar. Por favor, tente novamente.",
|
||||
"backup_code": "Código de Backup",
|
||||
"change_image": "Alterar imagem",
|
||||
"confirm_delete_account": "Eliminar a sua conta com todas as suas informações e dados pessoais",
|
||||
"confirm_delete_my_account": "Eliminar a Minha Conta",
|
||||
"confirm_your_current_password_to_get_started": "Confirme a sua palavra-passe atual para começar.",
|
||||
@@ -1124,17 +1122,13 @@
|
||||
"email_change_initiated": "O seu pedido de alteração de email foi iniciado.",
|
||||
"enable_two_factor_authentication": "Ativar autenticação de dois fatores",
|
||||
"enter_the_code_from_your_authenticator_app_below": "Introduza o código da sua aplicação de autenticação abaixo.",
|
||||
"file_size_must_be_less_than_10mb": "O tamanho do ficheiro deve ser inferior a 10MB.",
|
||||
"invalid_file_type": "Tipo de ficheiro inválido. Apenas são permitidos ficheiros JPEG, PNG e WEBP.",
|
||||
"lost_access": "Perdeu o acesso",
|
||||
"or_enter_the_following_code_manually": "Ou insira o seguinte código manualmente:",
|
||||
"organization_identification": "Ajude a sua organização a identificá-lo no Formbricks",
|
||||
"organizations_delete_message": "É o único proprietário destas organizações, por isso <b>também serão eliminadas.</b>",
|
||||
"permanent_removal_of_all_of_your_personal_information_and_data": "Remoção permanente de todas as suas informações e dados pessoais",
|
||||
"personal_information": "Informações pessoais",
|
||||
"please_enter_email_to_confirm_account_deletion": "Por favor, insira {email} no campo seguinte para confirmar a eliminação definitiva da sua conta:",
|
||||
"profile_updated_successfully": "O seu perfil foi atualizado com sucesso",
|
||||
"remove_image": "Remover imagem",
|
||||
"save_the_following_backup_codes_in_a_safe_place": "Guarde os seguintes códigos de backup num local seguro.",
|
||||
"scan_the_qr_code_below_with_your_authenticator_app": "Digitalize o código QR abaixo com a sua aplicação de autenticação.",
|
||||
"security_description": "Gerir a sua palavra-passe e outras definições de segurança, como a autenticação de dois fatores (2FA).",
|
||||
@@ -1144,10 +1138,8 @@
|
||||
"two_factor_code": "Código de Dois Fatores",
|
||||
"unlock_two_factor_authentication": "Desbloqueie a autenticação de dois fatores com um plano superior",
|
||||
"update_personal_info": "Atualize as suas informações pessoais",
|
||||
"upload_image": "Carregar imagem",
|
||||
"warning_cannot_delete_account": "É o único proprietário desta organização. Transfira a propriedade para outro membro primeiro.",
|
||||
"warning_cannot_undo": "Isto não pode ser desfeito",
|
||||
"you_must_select_a_file": "Deve selecionar um ficheiro."
|
||||
"warning_cannot_undo": "Isto não pode ser desfeito"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Adicionar membros à equipa e determinar o seu papel.",
|
||||
@@ -1715,10 +1707,8 @@
|
||||
"language_help_text": "Os metadados são carregados com base no valor `lang` no URL.",
|
||||
"link_description": "Descrição do link",
|
||||
"link_description_description": "Descrições entre 55 a 200 caracteres têm melhor desempenho.",
|
||||
"link_description_placeholder": "Ajude-nos a melhorar compartilhando suas opiniões.",
|
||||
"link_title": "Título do Link",
|
||||
"link_title_description": "Títulos curtos têm melhor desempenho como Meta Titles.",
|
||||
"link_title_placeholder": "Inquérito de Feedback do Cliente",
|
||||
"preview_image": "Pré-visualização da imagem",
|
||||
"preview_image_description": "Imagens de paisagem com tamanhos pequenos (<4MB) apresentam melhor desempenho.",
|
||||
"title": "Definições de ligação"
|
||||
|
||||
@@ -141,7 +141,6 @@
|
||||
"apply_filters": "Aplică filtre",
|
||||
"are_you_sure": "Ești sigur?",
|
||||
"attributes": "Atribute",
|
||||
"avatar": "Avatar",
|
||||
"back": "Înapoi",
|
||||
"billing": "Facturare",
|
||||
"booked": "Rezervat",
|
||||
@@ -748,6 +747,7 @@
|
||||
"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",
|
||||
@@ -1111,9 +1111,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"account_deletion_consequences_warning": "Consecințele ștergerii contului",
|
||||
"avatar_update_failed": "Actualizarea avatarului a eșuat. Vă rugăm să încercați din nou.",
|
||||
"backup_code": "Cod de rezervă",
|
||||
"change_image": "Schimbă imaginea",
|
||||
"confirm_delete_account": "Șterge contul tău cu toate informațiile personale și datele tale",
|
||||
"confirm_delete_my_account": "Șterge Contul Meu",
|
||||
"confirm_your_current_password_to_get_started": "Confirmaţi parola curentă pentru a începe.",
|
||||
@@ -1124,17 +1122,13 @@
|
||||
"email_change_initiated": "Cererea dvs. de schimbare a e-mailului a fost inițiată.",
|
||||
"enable_two_factor_authentication": "Activează autentificarea în doi pași",
|
||||
"enter_the_code_from_your_authenticator_app_below": "Introduceți codul din aplicația dvs. de autentificare mai jos.",
|
||||
"file_size_must_be_less_than_10mb": "Dimensiunea fișierului trebuie să fie mai mică de 10MB.",
|
||||
"invalid_file_type": "Tip de fișier invalid. Sunt permise numai fișiere JPEG, PNG și WEBP.",
|
||||
"lost_access": "Acces pierdut",
|
||||
"or_enter_the_following_code_manually": "Sau introduceți manual următorul cod:",
|
||||
"organization_identification": "Ajutați organizația să vă identifice pe Formbricks",
|
||||
"organizations_delete_message": "Ești singurul proprietar al acestor organizații, deci ele <b>vor fi șterse și ele.</b>",
|
||||
"permanent_removal_of_all_of_your_personal_information_and_data": "Ștergerea permanentă a tuturor informațiilor și datelor tale personale",
|
||||
"personal_information": "Informații personale",
|
||||
"please_enter_email_to_confirm_account_deletion": "Vă rugăm să introduceți {email} în câmpul următor pentru a confirma ștergerea definitivă a contului dumneavoastră:",
|
||||
"profile_updated_successfully": "Profilul dvs. a fost actualizat cu succes",
|
||||
"remove_image": "Șterge imaginea",
|
||||
"save_the_following_backup_codes_in_a_safe_place": "Salvează următoarele coduri de rezervă într-un loc sigur.",
|
||||
"scan_the_qr_code_below_with_your_authenticator_app": "Scanați codul QR de mai jos cu aplicația dvs. de autentificare.",
|
||||
"security_description": "Gestionează parola și alte setări de securitate, precum autentificarea în doi pași (2FA).",
|
||||
@@ -1144,10 +1138,8 @@
|
||||
"two_factor_code": "Codul cu doi factori",
|
||||
"unlock_two_factor_authentication": "Deblocați autentificarea în doi pași cu un plan superior",
|
||||
"update_personal_info": "Actualizează informațiile tale personale",
|
||||
"upload_image": "Încărcați imagine",
|
||||
"warning_cannot_delete_account": "Ești singurul proprietar al acestei organizații. Te rugăm să transferi proprietatea către un alt membru mai întâi.",
|
||||
"warning_cannot_undo": "Aceasta nu poate fi anulată",
|
||||
"you_must_select_a_file": "Trebuie să selectați un fișier."
|
||||
"warning_cannot_undo": "Aceasta nu poate fi anulată"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Adaugă membri în echipă și stabilește rolul lor.",
|
||||
@@ -1715,10 +1707,8 @@
|
||||
"language_help_text": "Meta datele sunt încărcate pe baza valorii `lang` din URL.",
|
||||
"link_description": "Descriere legătură",
|
||||
"link_description_description": "Descrierile între 55-200 de caractere au cele mai bune performanțe.",
|
||||
"link_description_placeholder": "Ajutați-ne să ne îmbunătățim împărtășindu-vă gândurile.",
|
||||
"link_title": "Titlu link",
|
||||
"link_title_description": "Titlurile scurte funcționează cel mai bine ca Meta Title-uri.",
|
||||
"link_title_placeholder": "Chestionar de feedback al clienților",
|
||||
"preview_image": "Previzualizare imagine",
|
||||
"preview_image_description": "Imaginile panoramice cu dimensiuni de fișier mici (<4MB) au cel mai bun randament.",
|
||||
"title": "Setări link"
|
||||
|
||||
@@ -141,7 +141,6 @@
|
||||
"apply_filters": "套用篩選器",
|
||||
"are_you_sure": "您確定嗎?",
|
||||
"attributes": "屬性",
|
||||
"avatar": "頭像",
|
||||
"back": "返回",
|
||||
"billing": "帳單",
|
||||
"booked": "已預訂",
|
||||
@@ -748,6 +747,7 @@
|
||||
"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": "找不到環境權限",
|
||||
@@ -1111,9 +1111,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"account_deletion_consequences_warning": "帳戶刪除後果",
|
||||
"avatar_update_failed": "頭像更新失敗。請再試一次。",
|
||||
"backup_code": "備份碼",
|
||||
"change_image": "變更圖片",
|
||||
"confirm_delete_account": "刪除您的帳戶以及您的所有個人資訊和資料",
|
||||
"confirm_delete_my_account": "刪除我的帳戶",
|
||||
"confirm_your_current_password_to_get_started": "確認您目前的密碼以開始使用。",
|
||||
@@ -1124,17 +1122,13 @@
|
||||
"email_change_initiated": "您的 email 更改請求已啟動。",
|
||||
"enable_two_factor_authentication": "啟用雙重驗證",
|
||||
"enter_the_code_from_your_authenticator_app_below": "在下方輸入您驗證器應用程式中的程式碼。",
|
||||
"file_size_must_be_less_than_10mb": "檔案大小必須小於 10MB。",
|
||||
"invalid_file_type": "無效的檔案類型。僅允許 JPEG、PNG 和 WEBP 檔案。",
|
||||
"lost_access": "無法存取",
|
||||
"or_enter_the_following_code_manually": "或手動輸入下列程式碼:",
|
||||
"organization_identification": "協助您的組織在 Formbricks 上識別您",
|
||||
"organizations_delete_message": "您是這些組織的唯一擁有者,因此它們也 <b>將被刪除。</b>",
|
||||
"permanent_removal_of_all_of_your_personal_information_and_data": "永久移除您的所有個人資訊和資料",
|
||||
"personal_information": "個人資訊",
|
||||
"please_enter_email_to_confirm_account_deletion": "請在以下欄位中輸入 '{'email'}' 以確認永久刪除您的帳戶:",
|
||||
"profile_updated_successfully": "您的個人資料已成功更新",
|
||||
"remove_image": "移除圖片",
|
||||
"save_the_following_backup_codes_in_a_safe_place": "將下列備份碼儲存在安全的地方。",
|
||||
"scan_the_qr_code_below_with_your_authenticator_app": "使用您的驗證器應用程式掃描下方的 QR 碼。",
|
||||
"security_description": "管理您的密碼和其他安全性設定,例如雙重驗證 (2FA)。",
|
||||
@@ -1144,10 +1138,8 @@
|
||||
"two_factor_code": "雙重驗證碼",
|
||||
"unlock_two_factor_authentication": "使用更高等級的方案解鎖雙重驗證",
|
||||
"update_personal_info": "更新您的個人資訊",
|
||||
"upload_image": "上傳圖片",
|
||||
"warning_cannot_delete_account": "您是此組織的唯一擁有者。請先將所有權轉讓給其他成員。",
|
||||
"warning_cannot_undo": "此操作無法復原",
|
||||
"you_must_select_a_file": "您必須選取檔案。"
|
||||
"warning_cannot_undo": "此操作無法復原"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "將成員新增至團隊並確定其角色。",
|
||||
@@ -1715,10 +1707,8 @@
|
||||
"language_help_text": "中 繼資料 會 根據 URL 中 的 `lang` 值 載入。",
|
||||
"link_description": "連結描述",
|
||||
"link_description_description": "描述在 55 - 200 個字符之間的表現最好。",
|
||||
"link_description_placeholder": "幫助 我們 改善 , 分享 您 的 想法 。",
|
||||
"link_title": "連結標題",
|
||||
"link_title_description": "短 標題 在 Meta Titles 中表現最佳。",
|
||||
"link_title_placeholder": "顧客 回饋 調查",
|
||||
"preview_image": "預覽 圖片",
|
||||
"preview_image_description": "景觀 圖片 檔案 大小 小於 4MB 效果 最佳。",
|
||||
"title": "連結 設定"
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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.",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -233,12 +233,31 @@ describe("ConditionsEditor", () => {
|
||||
expect(mockCallbacks.onDuplicateCondition).toHaveBeenCalledWith("cond1");
|
||||
});
|
||||
|
||||
test("calls onCreateGroup from the dropdown menu", async () => {
|
||||
test("calls onCreateGroup from the dropdown menu when enabled", 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");
|
||||
await user.click(createGroupButton);
|
||||
expect(mockCallbacks.onCreateGroup).toHaveBeenCalledWith("cond1");
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
test("calls onToggleGroupConnector when the connector is changed", async () => {
|
||||
|
||||
@@ -233,7 +233,8 @@ export function ConditionsEditor({ conditions, config, callbacks, depth = 0 }: C
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => callbacks.onCreateGroup(condition.id)}
|
||||
icon={<WorkflowIcon className="h-4 w-4" />}>
|
||||
icon={<WorkflowIcon className="h-4 w-4" />}
|
||||
disabled={conditions.conditions.length <= 1}>
|
||||
{t("environments.surveys.edit.create_group")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -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/main/docker/docker-compose.yml
|
||||
curl -o docker-compose.yml https://raw.githubusercontent.com/formbricks/formbricks/stable/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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user