mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-04 10:30:00 -06:00
Merge branch 'main' of https://github.com/formbricks/formbricks into feature-formal-wording
This commit is contained in:
@@ -168,6 +168,9 @@ SLACK_CLIENT_SECRET=
|
||||
# Enterprise License Key
|
||||
ENTERPRISE_LICENSE_KEY=
|
||||
|
||||
# Internal Environment (production, staging) - used for internal staging environment
|
||||
# ENVIRONMENT=production
|
||||
|
||||
# Automatically assign new users to a specific organization and role within that organization
|
||||
# Insert an existing organization id or generate a valid CUID for a new one at https://www.getuniqueid.com/cuid (e.g. cjld2cjxh0000qzrmn831i7rn)
|
||||
# (Role Management is an Enterprise feature)
|
||||
|
||||
@@ -213,6 +213,7 @@ export const SurveyAnalysisCTA = ({
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
isReadOnly={isReadOnly}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
projectCustomScripts={project.customHeadScripts}
|
||||
/>
|
||||
)}
|
||||
<SuccessMessage environment={environment} survey={survey} />
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||
import {
|
||||
Code2Icon,
|
||||
CodeIcon,
|
||||
Link2Icon,
|
||||
MailIcon,
|
||||
QrCodeIcon,
|
||||
@@ -18,6 +19,7 @@ import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { AnonymousLinksTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/anonymous-links-tab";
|
||||
import { AppTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab";
|
||||
import { CustomHtmlTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/custom-html-tab";
|
||||
import { DynamicPopupTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab";
|
||||
import { EmailTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/email-tab";
|
||||
import { LinkSettingsTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/link-settings-tab";
|
||||
@@ -51,6 +53,7 @@ interface ShareSurveyModalProps {
|
||||
isFormbricksCloud: boolean;
|
||||
isReadOnly: boolean;
|
||||
isStorageConfigured: boolean;
|
||||
projectCustomScripts?: string | null;
|
||||
}
|
||||
|
||||
export const ShareSurveyModal = ({
|
||||
@@ -65,6 +68,7 @@ export const ShareSurveyModal = ({
|
||||
isFormbricksCloud,
|
||||
isReadOnly,
|
||||
isStorageConfigured,
|
||||
projectCustomScripts,
|
||||
}: ShareSurveyModalProps) => {
|
||||
const environmentId = survey.environmentId;
|
||||
const [surveyUrl, setSurveyUrl] = useState<string>(getSurveyUrl(survey, publicDomain, "default"));
|
||||
@@ -191,9 +195,24 @@ export const ShareSurveyModal = ({
|
||||
componentType: PrettyUrlTab,
|
||||
componentProps: { publicDomain, isReadOnly },
|
||||
},
|
||||
{
|
||||
id: ShareSettingsType.CUSTOM_HTML,
|
||||
type: LinkTabsType.SHARE_SETTING,
|
||||
label: t("environments.surveys.share.custom_html.nav_title"),
|
||||
icon: CodeIcon,
|
||||
title: t("environments.surveys.share.custom_html.nav_title"),
|
||||
description: t("environments.surveys.share.custom_html.description"),
|
||||
componentType: CustomHtmlTab,
|
||||
componentProps: { projectCustomScripts, isReadOnly },
|
||||
},
|
||||
];
|
||||
|
||||
return isFormbricksCloud ? tabs.filter((tab) => tab.id !== ShareSettingsType.PRETTY_URL) : tabs;
|
||||
// Filter out tabs that should not be shown on Formbricks Cloud
|
||||
return isFormbricksCloud
|
||||
? tabs.filter(
|
||||
(tab) => tab.id !== ShareSettingsType.PRETTY_URL && tab.id !== ShareSettingsType.CUSTOM_HTML
|
||||
)
|
||||
: tabs;
|
||||
}, [
|
||||
t,
|
||||
survey,
|
||||
@@ -207,6 +226,7 @@ export const ShareSurveyModal = ({
|
||||
isFormbricksCloud,
|
||||
email,
|
||||
isStorageConfigured,
|
||||
projectCustomScripts,
|
||||
]);
|
||||
|
||||
const getDefaultActiveId = useCallback(() => {
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
"use client";
|
||||
|
||||
import { AlertTriangleIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { useSurvey } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/context/survey-context";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { updateSurveyAction } from "@/modules/survey/editor/actions";
|
||||
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormProvider,
|
||||
} from "@/modules/ui/components/form";
|
||||
import { TabToggle } from "@/modules/ui/components/tab-toggle";
|
||||
|
||||
interface CustomHtmlTabProps {
|
||||
projectCustomScripts: string | null | undefined;
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
interface CustomHtmlFormData {
|
||||
customHeadScripts: string;
|
||||
customHeadScriptsMode: TSurvey["customHeadScriptsMode"];
|
||||
}
|
||||
|
||||
export const CustomHtmlTab = ({ projectCustomScripts, isReadOnly }: CustomHtmlTabProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { survey } = useSurvey();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const form = useForm<CustomHtmlFormData>({
|
||||
defaultValues: {
|
||||
customHeadScripts: survey.customHeadScripts ?? "",
|
||||
customHeadScriptsMode: survey.customHeadScriptsMode ?? "add",
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
reset,
|
||||
formState: { isDirty },
|
||||
} = form;
|
||||
|
||||
const scriptsMode = watch("customHeadScriptsMode");
|
||||
|
||||
const onSubmit = async (data: CustomHtmlFormData) => {
|
||||
if (isSaving || isReadOnly) return;
|
||||
|
||||
setIsSaving(true);
|
||||
|
||||
const updatedSurvey: TSurvey = {
|
||||
...survey,
|
||||
customHeadScripts: data.customHeadScripts || null,
|
||||
customHeadScriptsMode: data.customHeadScriptsMode,
|
||||
};
|
||||
|
||||
const result = await updateSurveyAction(updatedSurvey);
|
||||
|
||||
if (result?.data) {
|
||||
toast.success(t("environments.surveys.share.custom_html.saved_successfully"));
|
||||
reset(data);
|
||||
} else {
|
||||
toast.error(t("common.something_went_wrong_please_try_again"));
|
||||
}
|
||||
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-1">
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||
{/* Mode Toggle */}
|
||||
<div className="space-y-2">
|
||||
<FormLabel>{t("environments.surveys.share.custom_html.script_mode")}</FormLabel>
|
||||
<TabToggle
|
||||
id="custom-scripts-mode"
|
||||
options={[
|
||||
{ value: "add", label: t("environments.surveys.share.custom_html.add_to_workspace") },
|
||||
{ value: "replace", label: t("environments.surveys.share.custom_html.replace_workspace") },
|
||||
]}
|
||||
defaultSelected={scriptsMode ?? "add"}
|
||||
onChange={(value) => setValue("customHeadScriptsMode", value, { shouldDirty: true })}
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
<p className="text-sm text-slate-500">
|
||||
{scriptsMode === "add"
|
||||
? t("environments.surveys.share.custom_html.add_mode_description")
|
||||
: t("environments.surveys.share.custom_html.replace_mode_description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Workspace Scripts Preview */}
|
||||
{projectCustomScripts && (
|
||||
<div className={scriptsMode === "replace" ? "opacity-50" : ""}>
|
||||
<FormLabel>{t("environments.surveys.share.custom_html.workspace_scripts_label")}</FormLabel>
|
||||
<div className="mt-2 max-h-32 overflow-auto rounded-md border border-slate-200 bg-slate-50 p-3">
|
||||
<pre className="font-mono text-xs whitespace-pre-wrap text-slate-600">
|
||||
{projectCustomScripts}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!projectCustomScripts && (
|
||||
<div className="rounded-md border border-slate-200 bg-slate-50 p-3">
|
||||
<p className="text-sm text-slate-500">
|
||||
{t("environments.surveys.share.custom_html.no_workspace_scripts")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Survey Scripts */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="customHeadScripts"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("environments.surveys.share.custom_html.survey_scripts_label")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("environments.surveys.share.custom_html.survey_scripts_description")}
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<textarea
|
||||
rows={8}
|
||||
placeholder={t("environments.surveys.share.custom_html.placeholder")}
|
||||
className={cn(
|
||||
"focus:border-brand-dark flex w-full rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs text-slate-800 placeholder:text-slate-400 focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
)}
|
||||
{...field}
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Save Button */}
|
||||
<Button type="submit" disabled={isSaving || isReadOnly || !isDirty}>
|
||||
{isSaving ? t("common.saving") : t("common.save")}
|
||||
</Button>
|
||||
{/* Security Warning */}
|
||||
<Alert variant="warning" className="flex items-start gap-2">
|
||||
<AlertTriangleIcon className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<AlertDescription>
|
||||
{t("environments.surveys.share.custom_html.security_warning")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -13,6 +13,7 @@ export enum ShareViaType {
|
||||
export enum ShareSettingsType {
|
||||
LINK_SETTINGS = "link-settings",
|
||||
PRETTY_URL = "pretty-url",
|
||||
CUSTOM_HTML = "custom-html",
|
||||
}
|
||||
|
||||
export enum LinkTabsType {
|
||||
|
||||
@@ -1609,6 +1609,20 @@ checksums:
|
||||
environments/surveys/share/anonymous_links/source_tracking: dcf85834f1ba490347a301ab55d32402
|
||||
environments/surveys/share/anonymous_links/url_encryption_description: 1509056fdae7b42fc85f1ee3c49de4c3
|
||||
environments/surveys/share/anonymous_links/url_encryption_label: 9c70fd3f64cf8cc5039b198d3af79d14
|
||||
environments/surveys/share/custom_html/add_mode_description: f48dcf53bce27cc40c3546547e8395cb
|
||||
environments/surveys/share/custom_html/add_to_workspace: af9cd24872f25cfc4231b926acc76d7c
|
||||
environments/surveys/share/custom_html/description: 0634048655de8b4b17b41d496e1ea457
|
||||
environments/surveys/share/custom_html/nav_title: 01f993f027ab277058eacb8a48ea7c01
|
||||
environments/surveys/share/custom_html/no_workspace_scripts: 7fc57f576c98e96ee73e7b489345d51a
|
||||
environments/surveys/share/custom_html/placeholder: 229eb1676a69311ff1dcc19c1a52c080
|
||||
environments/surveys/share/custom_html/replace_mode_description: 6eaf17275c02b0d5ac21255747f36271
|
||||
environments/surveys/share/custom_html/replace_workspace: b80e698cc8790246fea42453bfa4b09d
|
||||
environments/surveys/share/custom_html/saved_successfully: 14e7d2d646803ac1dd24cfa45c22606c
|
||||
environments/surveys/share/custom_html/script_mode: 60ed1102dd42ad14e272df5f6921b423
|
||||
environments/surveys/share/custom_html/security_warning: 5faa0f284d48110918a5e8a467e2bcb8
|
||||
environments/surveys/share/custom_html/survey_scripts_description: 948746d51db23b348164105c175391b3
|
||||
environments/surveys/share/custom_html/survey_scripts_label: 095d9fe768abe2bb32428184ee1c9b5a
|
||||
environments/surveys/share/custom_html/workspace_scripts_label: 3d9b6c09eae10a2bacb3ac96b4db4a19
|
||||
environments/surveys/share/dynamic_popup/alert_button: 8932096e3eee837beeb21dd4afd8b662
|
||||
environments/surveys/share/dynamic_popup/alert_description: 53d2ba39984a059a5eca4cb6cf9ba00d
|
||||
environments/surveys/share/dynamic_popup/alert_title: 813a9160940894da26ec2a09bbb1a7bf
|
||||
@@ -1820,6 +1834,13 @@ checksums:
|
||||
environments/workspace/app-connection/setup_alert_title: 9561cca2b391e0df81e8a982921ff2bb
|
||||
environments/workspace/app-connection/webapp_url: d64d8cc3c4c4ecce780d94755f7e4de9
|
||||
environments/workspace/general/cannot_delete_only_workspace: 853f32a75d92b06eaccc0d43d767c183
|
||||
environments/workspace/general/custom_scripts: a6a06a2e20764d76d3e22e5e17d98dbb
|
||||
environments/workspace/general/custom_scripts_card_description: 1585c47126e4b68f9f79f232631c67a1
|
||||
environments/workspace/general/custom_scripts_description: 1c477e711fc08850b2ab70d98ffe18d6
|
||||
environments/workspace/general/custom_scripts_label: 3b189dd62ae0cc35d616e04af90f0b38
|
||||
environments/workspace/general/custom_scripts_placeholder: 229eb1676a69311ff1dcc19c1a52c080
|
||||
environments/workspace/general/custom_scripts_updated_successfully: eabe8e6ededa86342d59093fe308c681
|
||||
environments/workspace/general/custom_scripts_warning: 5faa0f284d48110918a5e8a467e2bcb8
|
||||
environments/workspace/general/delete_workspace: 3badbc0f4b49644986fc19d8b2d8f317
|
||||
environments/workspace/general/delete_workspace_confirmation: 54a4ee78867537e0244c7170453cdb3f
|
||||
environments/workspace/general/delete_workspace_name_includes_surveys_responses_people_and_more: 11e9ac5a799fbec22495f92f42c40d98
|
||||
|
||||
@@ -23,6 +23,7 @@ export const env = createEnv({
|
||||
EMAIL_VERIFICATION_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
ENCRYPTION_KEY: z.string(),
|
||||
ENTERPRISE_LICENSE_KEY: z.string().optional(),
|
||||
ENVIRONMENT: z.enum(["production", "staging"]).default("production"),
|
||||
GITHUB_ID: z.string().optional(),
|
||||
GITHUB_SECRET: z.string().optional(),
|
||||
GOOGLE_CLIENT_ID: z.string().optional(),
|
||||
@@ -151,6 +152,7 @@ export const env = createEnv({
|
||||
EMAIL_VERIFICATION_DISABLED: process.env.EMAIL_VERIFICATION_DISABLED,
|
||||
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY,
|
||||
ENTERPRISE_LICENSE_KEY: process.env.ENTERPRISE_LICENSE_KEY,
|
||||
ENVIRONMENT: process.env.ENVIRONMENT,
|
||||
GITHUB_ID: process.env.GITHUB_ID,
|
||||
GITHUB_SECRET: process.env.GITHUB_SECRET,
|
||||
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
||||
|
||||
@@ -21,7 +21,7 @@ export type TInstanceInfo = {
|
||||
export const getInstanceInfo = reactCache(async (): Promise<TInstanceInfo | null> => {
|
||||
try {
|
||||
const oldestOrg = await prisma.organization.findFirst({
|
||||
orderBy: { createdAt: "asc" },
|
||||
orderBy: [{ createdAt: "asc" }, { id: "asc" }],
|
||||
select: { id: true, createdAt: true },
|
||||
});
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ const selectProject = {
|
||||
environments: true,
|
||||
styling: true,
|
||||
logo: true,
|
||||
customHeadScripts: true,
|
||||
};
|
||||
|
||||
export const getUserProjects = reactCache(
|
||||
|
||||
@@ -268,6 +268,8 @@ export const mockSyncSurveyOutput: SurveyMock = {
|
||||
showLanguageSwitch: null,
|
||||
metadata: {},
|
||||
slug: null,
|
||||
customHeadScripts: null,
|
||||
customHeadScriptsMode: null,
|
||||
};
|
||||
|
||||
export const mockSurveyOutput: SurveyMock = {
|
||||
@@ -292,6 +294,8 @@ export const mockSurveyOutput: SurveyMock = {
|
||||
showLanguageSwitch: null,
|
||||
...baseSurveyProperties,
|
||||
slug: null,
|
||||
customHeadScripts: null,
|
||||
customHeadScriptsMode: null,
|
||||
};
|
||||
|
||||
export const createSurveyInput: TSurveyCreateInput = {
|
||||
@@ -322,6 +326,8 @@ export const updateSurveyInput: TSurvey = {
|
||||
...baseSurveyProperties,
|
||||
...commonMockProperties,
|
||||
slug: null,
|
||||
customHeadScripts: null,
|
||||
customHeadScriptsMode: null,
|
||||
};
|
||||
|
||||
export const mockTransformedSurveyOutput = {
|
||||
@@ -574,4 +580,6 @@ export const mockSurveyWithLogic: TSurvey = {
|
||||
{ id: "siog1dabtpo3l0a3xoxw2922", type: "text", name: "var1", value: "lmao" },
|
||||
{ id: "km1srr55owtn2r7lkoh5ny1u", type: "number", name: "var2", value: 32 },
|
||||
],
|
||||
customHeadScripts: null,
|
||||
customHeadScriptsMode: null,
|
||||
};
|
||||
|
||||
@@ -65,6 +65,8 @@ export const selectSurvey = {
|
||||
showLanguageSwitch: true,
|
||||
recaptcha: true,
|
||||
metadata: true,
|
||||
customHeadScripts: true,
|
||||
customHeadScriptsMode: true,
|
||||
languages: {
|
||||
select: {
|
||||
default: true,
|
||||
@@ -563,6 +565,7 @@ export const updateSurveyInternal = async (
|
||||
...prismaSurvey, // Properties from prismaSurvey
|
||||
displayPercentage: Number(prismaSurvey.displayPercentage) || null,
|
||||
segment: surveySegment,
|
||||
customHeadScriptsMode: prismaSurvey.customHeadScriptsMode,
|
||||
};
|
||||
|
||||
return modifiedSurvey;
|
||||
@@ -783,6 +786,7 @@ export const loadNewSegmentInSurvey = async (surveyId: string, newSegmentId: str
|
||||
const modifiedSurvey: TSurvey = {
|
||||
...prismaSurvey, // Properties from prismaSurvey
|
||||
segment: surveySegment,
|
||||
customHeadScriptsMode: prismaSurvey.customHeadScriptsMode,
|
||||
};
|
||||
|
||||
return modifiedSurvey;
|
||||
|
||||
@@ -29,6 +29,7 @@ export const transformPrismaSurvey = <T extends TSurvey | TJsEnvironmentStateSur
|
||||
...surveyPrisma,
|
||||
displayPercentage: Number(surveyPrisma.displayPercentage) || null,
|
||||
segment,
|
||||
customHeadScriptsMode: surveyPrisma.customHeadScriptsMode,
|
||||
} as T;
|
||||
|
||||
return transformedSurvey;
|
||||
|
||||
@@ -1690,6 +1690,22 @@
|
||||
"url_encryption_description": "Nur deaktivieren, wenn Sie eine benutzerdefinierte Einmal-ID setzen müssen.",
|
||||
"url_encryption_label": "Verschlüsselung der URL für einmalige Nutzung ID"
|
||||
},
|
||||
"custom_html": {
|
||||
"add_mode_description": "Umfrage-Skripte werden zusätzlich zu den Workspace-Skripten ausgeführt.",
|
||||
"add_to_workspace": "Zu Workspace-Skripten hinzufügen",
|
||||
"description": "Tracking-Skripte und Pixel zu dieser Umfrage hinzufügen",
|
||||
"nav_title": "Benutzerdefiniertes HTML",
|
||||
"no_workspace_scripts": "Keine Workspace-Skripte konfiguriert. Sie können diese in Workspace-Einstellungen → Allgemein hinzufügen.",
|
||||
"placeholder": "<!-- Fügen Sie hier Ihre Tracking-Skripte ein -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
|
||||
"replace_mode_description": "Nur Umfrage-Skripte werden ausgeführt. Workspace-Skripte werden ignoriert. Leer lassen, um keine Skripte zu laden.",
|
||||
"replace_workspace": "Workspace-Skripte ersetzen",
|
||||
"saved_successfully": "Benutzerdefinierte Skripte erfolgreich gespeichert",
|
||||
"script_mode": "Skript-Modus",
|
||||
"security_warning": "Skripte werden mit vollem Browser-Zugriff ausgeführt. Fügen Sie nur Skripte aus vertrauenswürdigen Quellen hinzu.",
|
||||
"survey_scripts_description": "Benutzerdefiniertes HTML hinzufügen, das in den <head> dieser Umfrageseite eingefügt wird.",
|
||||
"survey_scripts_label": "Umfragespezifische Skripte",
|
||||
"workspace_scripts_label": "Workspace-Skripte (vererbt)"
|
||||
},
|
||||
"dynamic_popup": {
|
||||
"alert_button": "Umfrage bearbeiten",
|
||||
"alert_description": "Diese Umfrage ist derzeit als Link-Umfrage konfiguriert, die dynamische Pop-ups nicht unterstützt. Sie können dies im Tab ‚Einstellungen‘ im Umfrage-Editor ändern.",
|
||||
@@ -1929,6 +1945,13 @@
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_workspace": "Dies ist Ihr einziges Projekt, es kann nicht gelöscht werden. Erstellen Sie zuerst ein neues Projekt.",
|
||||
"custom_scripts": "Benutzerdefinierte Skripte",
|
||||
"custom_scripts_card_description": "Tracking-Skripte und Pixel zu allen Link-Umfragen in diesem Workspace hinzufügen.",
|
||||
"custom_scripts_description": "Skripte werden in den <head> aller Link-Umfrageseiten eingefügt.",
|
||||
"custom_scripts_label": "HTML-Skripte",
|
||||
"custom_scripts_placeholder": "<!-- Fügen Sie hier Ihre Tracking-Skripte ein -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
|
||||
"custom_scripts_updated_successfully": "Benutzerdefinierte Skripte erfolgreich aktualisiert",
|
||||
"custom_scripts_warning": "Skripte werden mit vollem Browser-Zugriff ausgeführt. Fügen Sie nur Skripte aus vertrauenswürdigen Quellen hinzu.",
|
||||
"delete_workspace": "Projekt löschen",
|
||||
"delete_workspace_confirmation": "Sind Sie sicher, dass Sie {projectName} löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "{projectName} inkl. aller Umfragen, Antworten, Personen, Aktionen und Attribute löschen.",
|
||||
|
||||
@@ -1690,6 +1690,22 @@
|
||||
"url_encryption_description": "Only disable if you need to set a custom single-use ID.",
|
||||
"url_encryption_label": "URL encryption of single-use ID"
|
||||
},
|
||||
"custom_html": {
|
||||
"add_mode_description": "Survey scripts will run in addition to workspace-level scripts.",
|
||||
"add_to_workspace": "Add to Workspace scripts",
|
||||
"description": "Add tracking scripts and pixels to this survey",
|
||||
"nav_title": "Custom HTML",
|
||||
"no_workspace_scripts": "No workspace-level scripts configured. You can add them in Workspace Settings → General.",
|
||||
"placeholder": "<!-- Paste your tracking scripts here -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
|
||||
"replace_mode_description": "Only survey scripts will run. Workspace scripts will be ignored. Keep empty to not load any scripts.",
|
||||
"replace_workspace": "Replace Workspace scripts",
|
||||
"saved_successfully": "Custom scripts saved successfully",
|
||||
"script_mode": "Script Mode",
|
||||
"security_warning": "Scripts execute with full browser access. Only add scripts from trusted sources.",
|
||||
"survey_scripts_description": "Add custom HTML to inject into the <head> of this survey page.",
|
||||
"survey_scripts_label": "Survey-specific scripts",
|
||||
"workspace_scripts_label": "Workspace scripts (inherited)"
|
||||
},
|
||||
"dynamic_popup": {
|
||||
"alert_button": "Edit survey",
|
||||
"alert_description": "This survey is currently configured as a link survey, which does not support dynamic pop-ups. You can change this in the settings tab of the survey editor.",
|
||||
@@ -1929,6 +1945,13 @@
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_workspace": "This is your only workspace, it cannot be deleted. Create a new workspace first.",
|
||||
"custom_scripts": "Custom Scripts",
|
||||
"custom_scripts_card_description": "Add tracking scripts and pixels to all link surveys in this workspace.",
|
||||
"custom_scripts_description": "Scripts will be injected into the <head> of all link survey pages.",
|
||||
"custom_scripts_label": "HTML Scripts",
|
||||
"custom_scripts_placeholder": "<!-- Paste your tracking scripts here -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
|
||||
"custom_scripts_updated_successfully": "Custom scripts updated successfully",
|
||||
"custom_scripts_warning": "Scripts execute with full browser access. Only add scripts from trusted sources.",
|
||||
"delete_workspace": "Delete Workspace",
|
||||
"delete_workspace_confirmation": "Are you sure you want to delete {projectName}? This action cannot be undone.",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "Delete {projectName} including all surveys, responses, people, actions and attributes.",
|
||||
|
||||
@@ -1690,6 +1690,22 @@
|
||||
"url_encryption_description": "Desactiva solo si necesitas establecer un ID de uso único personalizado.",
|
||||
"url_encryption_label": "Cifrado URL del ID de uso único"
|
||||
},
|
||||
"custom_html": {
|
||||
"add_mode_description": "Los scripts de la encuesta se ejecutarán además de los scripts a nivel de espacio de trabajo.",
|
||||
"add_to_workspace": "Añadir a los scripts del espacio de trabajo",
|
||||
"description": "Añade scripts de seguimiento y píxeles a esta encuesta",
|
||||
"nav_title": "HTML personalizado",
|
||||
"no_workspace_scripts": "No hay scripts configurados a nivel de espacio de trabajo. Puedes añadirlos en Configuración del espacio de trabajo → General.",
|
||||
"placeholder": "<!-- Pega tus scripts de seguimiento aquí -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
|
||||
"replace_mode_description": "Solo se ejecutarán los scripts de la encuesta. Los scripts del espacio de trabajo serán ignorados. Déjalo vacío para no cargar ningún script.",
|
||||
"replace_workspace": "Reemplazar scripts del espacio de trabajo",
|
||||
"saved_successfully": "Scripts personalizados guardados correctamente",
|
||||
"script_mode": "Modo de script",
|
||||
"security_warning": "Los scripts se ejecutan con acceso completo al navegador. Solo añade scripts de fuentes confiables.",
|
||||
"survey_scripts_description": "Añade HTML personalizado para inyectar en el <head> de esta página de encuesta.",
|
||||
"survey_scripts_label": "Scripts específicos de la encuesta",
|
||||
"workspace_scripts_label": "Scripts del espacio de trabajo (heredados)"
|
||||
},
|
||||
"dynamic_popup": {
|
||||
"alert_button": "Editar encuesta",
|
||||
"alert_description": "Esta encuesta está actualmente configurada como una encuesta de enlace, que no admite ventanas emergentes dinámicas. Puedes cambiar esto en la pestaña de ajustes del editor de encuestas.",
|
||||
@@ -1929,6 +1945,13 @@
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_workspace": "Este es tu único proyecto, no se puede eliminar. Crea primero un proyecto nuevo.",
|
||||
"custom_scripts": "Scripts personalizados",
|
||||
"custom_scripts_card_description": "Añade scripts de seguimiento y píxeles a todas las encuestas con enlace en este espacio de trabajo.",
|
||||
"custom_scripts_description": "Los scripts se inyectarán en el <head> de todas las páginas de encuestas con enlace.",
|
||||
"custom_scripts_label": "Scripts HTML",
|
||||
"custom_scripts_placeholder": "<!-- Pega tus scripts de seguimiento aquí -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
|
||||
"custom_scripts_updated_successfully": "Scripts personalizados actualizados correctamente",
|
||||
"custom_scripts_warning": "Los scripts se ejecutan con acceso completo al navegador. Solo añade scripts de fuentes confiables.",
|
||||
"delete_workspace": "Eliminar proyecto",
|
||||
"delete_workspace_confirmation": "¿Estás seguro de que quieres eliminar {projectName}? Esta acción no se puede deshacer.",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "Eliminar {projectName} incluyendo todas las encuestas, respuestas, personas, acciones y atributos.",
|
||||
|
||||
@@ -1690,6 +1690,22 @@
|
||||
"url_encryption_description": "Désactiver seulement si vous devez définir un identifiant unique personnalisé",
|
||||
"url_encryption_label": "Cryptage de l'identifiant à usage unique dans l'URL"
|
||||
},
|
||||
"custom_html": {
|
||||
"add_mode_description": "Les scripts de l'enquête s'exécuteront en plus des scripts au niveau de l'espace de travail.",
|
||||
"add_to_workspace": "Ajouter aux scripts de l'espace de travail",
|
||||
"description": "Ajouter des scripts de suivi et des pixels à cette enquête",
|
||||
"nav_title": "HTML personnalisé",
|
||||
"no_workspace_scripts": "Aucun script au niveau de l'espace de travail configuré. Vous pouvez les ajouter dans Paramètres de l'espace de travail → Général.",
|
||||
"placeholder": "<!-- Collez vos scripts de suivi ici -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
|
||||
"replace_mode_description": "Seuls les scripts de l'enquête s'exécuteront. Les scripts de l'espace de travail seront ignorés. Laissez vide pour ne charger aucun script.",
|
||||
"replace_workspace": "Remplacer les scripts de l'espace de travail",
|
||||
"saved_successfully": "Scripts personnalisés enregistrés avec succès",
|
||||
"script_mode": "Mode de script",
|
||||
"security_warning": "Les scripts s'exécutent avec un accès complet au navigateur. Ajoutez uniquement des scripts provenant de sources fiables.",
|
||||
"survey_scripts_description": "Ajouter du HTML personnalisé à injecter dans le <head> de cette page d'enquête.",
|
||||
"survey_scripts_label": "Scripts spécifiques à l'enquête",
|
||||
"workspace_scripts_label": "Scripts de l'espace de travail (hérités)"
|
||||
},
|
||||
"dynamic_popup": {
|
||||
"alert_button": "Modifier enquête",
|
||||
"alert_description": "Ce sondage est actuellement configuré comme un sondage de lien, qui ne prend pas en charge les pop-ups dynamiques. Vous pouvez le modifier dans l'onglet des paramètres de l'éditeur de sondage.",
|
||||
@@ -1929,6 +1945,13 @@
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_workspace": "Il s'agit de votre seul projet, il ne peut pas être supprimé. Créez d'abord un nouveau projet.",
|
||||
"custom_scripts": "Scripts personnalisés",
|
||||
"custom_scripts_card_description": "Ajouter des scripts de suivi et des pixels à toutes les enquêtes par lien dans cet espace de travail.",
|
||||
"custom_scripts_description": "Les scripts seront injectés dans le <head> de toutes les pages d'enquête par lien.",
|
||||
"custom_scripts_label": "Scripts HTML",
|
||||
"custom_scripts_placeholder": "<!-- Collez vos scripts de suivi ici -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
|
||||
"custom_scripts_updated_successfully": "Scripts personnalisés mis à jour avec succès",
|
||||
"custom_scripts_warning": "Les scripts s'exécutent avec un accès complet au navigateur. Ajoutez uniquement des scripts provenant de sources fiables.",
|
||||
"delete_workspace": "Supprimer le projet",
|
||||
"delete_workspace_confirmation": "Êtes-vous sûr de vouloir supprimer {projectName} ? Cette action ne peut pas être annulée.",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "Supprimer {projectName} y compris toutes les enquêtes, réponses, personnes, actions et attributs.",
|
||||
|
||||
@@ -1690,6 +1690,22 @@
|
||||
"url_encryption_description": "カスタムの単一使用IDを設定する必要がある場合にのみ無効にしてください。",
|
||||
"url_encryption_label": "単一使用IDのURL暗号化"
|
||||
},
|
||||
"custom_html": {
|
||||
"add_mode_description": "アンケートスクリプトは、ワークスペースレベルのスクリプトに加えて実行されます。",
|
||||
"add_to_workspace": "ワークスペーススクリプトに追加",
|
||||
"description": "このアンケートにトラッキングスクリプトとピクセルを追加",
|
||||
"nav_title": "カスタムHTML",
|
||||
"no_workspace_scripts": "ワークスペースレベルのスクリプトが設定されていません。ワークスペース設定→一般から追加できます。",
|
||||
"placeholder": "<!-- トラッキングスクリプトをここに貼り付けてください -->\n<script>\n // Google Tag Manager、Analyticsなど\n</script>",
|
||||
"replace_mode_description": "アンケートスクリプトのみが実行されます。ワークスペーススクリプトは無視されます。スクリプトを読み込まない場合は空のままにしてください。",
|
||||
"replace_workspace": "ワークスペーススクリプトを置き換え",
|
||||
"saved_successfully": "カスタムスクリプトを正常に保存しました",
|
||||
"script_mode": "スクリプトモード",
|
||||
"security_warning": "スクリプトはブラウザへの完全なアクセス権で実行されます。信頼できるソースからのスクリプトのみを追加してください。",
|
||||
"survey_scripts_description": "このアンケートページの<head>に挿入するカスタムHTMLを追加します。",
|
||||
"survey_scripts_label": "アンケート固有のスクリプト",
|
||||
"workspace_scripts_label": "ワークスペーススクリプト(継承)"
|
||||
},
|
||||
"dynamic_popup": {
|
||||
"alert_button": "フォームを編集",
|
||||
"alert_description": "このフォームは現在、動的なポップアップをサポートしていないリンクフォームとして設定されています。フォームエディターの設定タブでこれを変更できます。",
|
||||
@@ -1929,6 +1945,13 @@
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_workspace": "これは唯一のワークスペースのため、削除できません。まず新しいワークスペースを作成してください。",
|
||||
"custom_scripts": "カスタムスクリプト",
|
||||
"custom_scripts_card_description": "このワークスペース内のすべてのリンクアンケートにトラッキングスクリプトとピクセルを追加します。",
|
||||
"custom_scripts_description": "すべてのリンクアンケートページの<head>にスクリプトが挿入されます。",
|
||||
"custom_scripts_label": "HTMLスクリプト",
|
||||
"custom_scripts_placeholder": "<!-- トラッキングスクリプトをここに貼り付けてください -->\n<script>\n // Google Tag Manager、Analyticsなど\n</script>",
|
||||
"custom_scripts_updated_successfully": "カスタムスクリプトを正常に更新しました",
|
||||
"custom_scripts_warning": "スクリプトはブラウザへの完全なアクセス権で実行されます。信頼できるソースからのスクリプトのみを追加してください。",
|
||||
"delete_workspace": "ワークスペースを削除",
|
||||
"delete_workspace_confirmation": "{projectName}を削除してもよろしいですか?このアクションは元に戻せません。",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "{projectName}をすべてのフォーム、回答、人物、アクション、属性を含めて削除します。",
|
||||
|
||||
@@ -1690,6 +1690,22 @@
|
||||
"url_encryption_description": "Schakel dit alleen uit als u een aangepaste ID voor eenmalig gebruik moet instellen.",
|
||||
"url_encryption_label": "URL-codering van ID voor eenmalig gebruik"
|
||||
},
|
||||
"custom_html": {
|
||||
"add_mode_description": "Enquêtescripts worden uitgevoerd naast scripts op werkruimteniveau.",
|
||||
"add_to_workspace": "Toevoegen aan werkruimtescripts",
|
||||
"description": "Voeg trackingscripts en pixels toe aan deze enquête",
|
||||
"nav_title": "Aangepaste HTML",
|
||||
"no_workspace_scripts": "Geen scripts op werkruimteniveau geconfigureerd. Je kunt ze toevoegen in Werkruimte-instellingen → Algemeen.",
|
||||
"placeholder": "<!-- Plak hier je trackingscripts -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
|
||||
"replace_mode_description": "Alleen enquêtescripts worden uitgevoerd. Werkruimtescripts worden genegeerd. Laat leeg om geen scripts te laden.",
|
||||
"replace_workspace": "Werkruimtescripts vervangen",
|
||||
"saved_successfully": "Aangepaste scripts succesvol opgeslagen",
|
||||
"script_mode": "Scriptmodus",
|
||||
"security_warning": "Scripts worden uitgevoerd met volledige browsertoegang. Voeg alleen scripts toe van vertrouwde bronnen.",
|
||||
"survey_scripts_description": "Voeg aangepaste HTML toe om te injecteren in de <head> van deze enquêtepagina.",
|
||||
"survey_scripts_label": "Enquêtespecifieke scripts",
|
||||
"workspace_scripts_label": "Werkruimtescripts (overgenomen)"
|
||||
},
|
||||
"dynamic_popup": {
|
||||
"alert_button": "Enquête bewerken",
|
||||
"alert_description": "Deze enquête is momenteel geconfigureerd als een linkenquête, die geen dynamische pop-ups ondersteunt. U kunt dit wijzigen op het tabblad Instellingen van de enquête-editor.",
|
||||
@@ -1929,6 +1945,13 @@
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_workspace": "Dit is uw enige project, het kan niet worden verwijderd. Maak eerst een nieuw project aan.",
|
||||
"custom_scripts": "Aangepaste scripts",
|
||||
"custom_scripts_card_description": "Voeg trackingscripts en pixels toe aan alle linkenquêtes in deze werkruimte.",
|
||||
"custom_scripts_description": "Scripts worden geïnjecteerd in de <head> van alle linkenquêtepagina's.",
|
||||
"custom_scripts_label": "HTML-scripts",
|
||||
"custom_scripts_placeholder": "<!-- Plak hier je trackingscripts -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
|
||||
"custom_scripts_updated_successfully": "Aangepaste scripts succesvol bijgewerkt",
|
||||
"custom_scripts_warning": "Scripts worden uitgevoerd met volledige browsertoegang. Voeg alleen scripts toe van vertrouwde bronnen.",
|
||||
"delete_workspace": "Project verwijderen",
|
||||
"delete_workspace_confirmation": "Weet u zeker dat u {projectName} wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "Verwijder {projectName} incl. alle enquêtes, reacties, mensen, acties en attributen.",
|
||||
|
||||
@@ -1690,6 +1690,22 @@
|
||||
"url_encryption_description": "Desative apenas se precisar definir um ID de uso único personalizado",
|
||||
"url_encryption_label": "Criptografia de URL de ID de uso único"
|
||||
},
|
||||
"custom_html": {
|
||||
"add_mode_description": "Os scripts da pesquisa serão executados além dos scripts do nível do workspace.",
|
||||
"add_to_workspace": "Adicionar aos scripts do workspace",
|
||||
"description": "Adicione scripts de rastreamento e pixels a esta pesquisa",
|
||||
"nav_title": "HTML personalizado",
|
||||
"no_workspace_scripts": "Nenhum script de nível de workspace configurado. Você pode adicioná-los em Configurações do Workspace → Geral.",
|
||||
"placeholder": "<!-- Cole seus scripts de rastreamento aqui -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
|
||||
"replace_mode_description": "Apenas os scripts da pesquisa serão executados. Os scripts do workspace serão ignorados. Deixe vazio para não carregar nenhum script.",
|
||||
"replace_workspace": "Substituir scripts do workspace",
|
||||
"saved_successfully": "Scripts personalizados salvos com sucesso",
|
||||
"script_mode": "Modo de script",
|
||||
"security_warning": "Os scripts são executados com acesso total ao navegador. Adicione apenas scripts de fontes confiáveis.",
|
||||
"survey_scripts_description": "Adicione HTML personalizado para injetar no <head> desta página de pesquisa.",
|
||||
"survey_scripts_label": "Scripts específicos da pesquisa",
|
||||
"workspace_scripts_label": "Scripts do workspace (herdados)"
|
||||
},
|
||||
"dynamic_popup": {
|
||||
"alert_button": "Editar pesquisa",
|
||||
"alert_description": "Esta pesquisa está atualmente configurada como uma pesquisa de link, o que não suporta pop-ups dinâmicos. Você pode alterar isso na aba de configurações do editor de pesquisas.",
|
||||
@@ -1929,6 +1945,13 @@
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_workspace": "Este é seu único projeto, ele não pode ser excluído. Crie um novo projeto primeiro.",
|
||||
"custom_scripts": "Scripts personalizados",
|
||||
"custom_scripts_card_description": "Adicione scripts de rastreamento e pixels a todas as pesquisas de link neste workspace.",
|
||||
"custom_scripts_description": "Os scripts serão injetados no <head> de todas as páginas de pesquisa de link.",
|
||||
"custom_scripts_label": "Scripts HTML",
|
||||
"custom_scripts_placeholder": "<!-- Cole seus scripts de rastreamento aqui -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
|
||||
"custom_scripts_updated_successfully": "Scripts personalizados atualizados com sucesso",
|
||||
"custom_scripts_warning": "Os scripts são executados com acesso total ao navegador. Adicione apenas scripts de fontes confiáveis.",
|
||||
"delete_workspace": "Excluir projeto",
|
||||
"delete_workspace_confirmation": "Tem certeza de que deseja excluir {projectName}? Essa ação não pode ser desfeita.",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "Excluir {projectName} incluindo todas as pesquisas, respostas, pessoas, ações e atributos.",
|
||||
|
||||
@@ -1690,6 +1690,22 @@
|
||||
"url_encryption_description": "Desative apenas se precisar definir um ID de uso único personalizado.",
|
||||
"url_encryption_label": "Encriptação do URL de ID de uso único"
|
||||
},
|
||||
"custom_html": {
|
||||
"add_mode_description": "Os scripts do inquérito serão executados para além dos scripts ao nível da área de trabalho.",
|
||||
"add_to_workspace": "Adicionar aos scripts da área de trabalho",
|
||||
"description": "Adicionar scripts de rastreamento e pixels a este inquérito",
|
||||
"nav_title": "HTML personalizado",
|
||||
"no_workspace_scripts": "Nenhum script ao nível da área de trabalho configurado. Pode adicioná-los em Definições da Área de Trabalho → Geral.",
|
||||
"placeholder": "<!-- Cole os seus scripts de rastreamento aqui -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
|
||||
"replace_mode_description": "Apenas os scripts do inquérito serão executados. Os scripts da área de trabalho serão ignorados. Deixe vazio para não carregar nenhum script.",
|
||||
"replace_workspace": "Substituir scripts da área de trabalho",
|
||||
"saved_successfully": "Scripts personalizados guardados com sucesso",
|
||||
"script_mode": "Modo de script",
|
||||
"security_warning": "Os scripts são executados com acesso total ao navegador. Adicione apenas scripts de fontes fidedignas.",
|
||||
"survey_scripts_description": "Adicionar HTML personalizado para injetar no <head> desta página de inquérito.",
|
||||
"survey_scripts_label": "Scripts específicos do inquérito",
|
||||
"workspace_scripts_label": "Scripts da área de trabalho (herdados)"
|
||||
},
|
||||
"dynamic_popup": {
|
||||
"alert_button": "Editar inquérito",
|
||||
"alert_description": "Este questionário está atualmente configurado como um questionário de link, que não suporta pop-ups dinâmicos. Você pode alterar isso na aba de configurações do editor de questionários.",
|
||||
@@ -1929,6 +1945,13 @@
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_workspace": "Este é o seu único projeto, não pode ser eliminado. Crie primeiro um novo projeto.",
|
||||
"custom_scripts": "Scripts personalizados",
|
||||
"custom_scripts_card_description": "Adicionar scripts de rastreamento e pixels a todos os inquéritos de link nesta área de trabalho.",
|
||||
"custom_scripts_description": "Os scripts serão injetados no <head> de todas as páginas de inquéritos de link.",
|
||||
"custom_scripts_label": "Scripts HTML",
|
||||
"custom_scripts_placeholder": "<!-- Cole os seus scripts de rastreamento aqui -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
|
||||
"custom_scripts_updated_successfully": "Scripts personalizados atualizados com sucesso",
|
||||
"custom_scripts_warning": "Os scripts são executados com acesso total ao navegador. Adicione apenas scripts de fontes fidedignas.",
|
||||
"delete_workspace": "Eliminar projeto",
|
||||
"delete_workspace_confirmation": "Tem a certeza de que pretende eliminar {projectName}? Esta ação não pode ser desfeita.",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "Eliminar {projectName} incluindo todos os inquéritos, respostas, pessoas, ações e atributos.",
|
||||
|
||||
@@ -1690,6 +1690,22 @@
|
||||
"url_encryption_description": "Dezactivați doar dacă trebuie să setați un ID unic personalizat.",
|
||||
"url_encryption_label": "Criptarea URL pentru ID unic de utilizare"
|
||||
},
|
||||
"custom_html": {
|
||||
"add_mode_description": "Scripturile sondajului vor rula în plus față de scripturile la nivel de spațiu de lucru.",
|
||||
"add_to_workspace": "Adaugă la scripturile spațiului de lucru",
|
||||
"description": "Adaugă scripturi de tracking și pixeli acestui sondaj",
|
||||
"nav_title": "HTML personalizat",
|
||||
"no_workspace_scripts": "Nu există scripturi la nivel de spațiu de lucru configurate. Le poți adăuga în Setări spațiu de lucru → General.",
|
||||
"placeholder": "<!-- Lipește aici scripturile tale de tracking -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
|
||||
"replace_mode_description": "Vor rula doar scripturile sondajului. Scripturile spațiului de lucru vor fi ignorate. Lasă gol pentru a nu încărca niciun script.",
|
||||
"replace_workspace": "Înlocuiește scripturile spațiului de lucru",
|
||||
"saved_successfully": "Scripturile personalizate au fost salvate cu succes",
|
||||
"script_mode": "Modul script",
|
||||
"security_warning": "Scripturile se execută cu acces complet la browser. Adaugă doar scripturi din surse de încredere.",
|
||||
"survey_scripts_description": "Adaugă HTML personalizat pentru a fi injectat în <head> pe această pagină de sondaj.",
|
||||
"survey_scripts_label": "Scripturi specifice sondajului",
|
||||
"workspace_scripts_label": "Scripturi spațiu de lucru (moștenite)"
|
||||
},
|
||||
"dynamic_popup": {
|
||||
"alert_button": "Editează chestionar",
|
||||
"alert_description": "Acest sondaj este configurat în prezent ca un sondaj cu link, care nu suportă pop-up-uri dinamice. Puteți schimba acest lucru în fila de setări a editorului de sondaje.",
|
||||
@@ -1929,6 +1945,13 @@
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_workspace": "Acesta este singurul tău proiect, nu poate fi șters. Creează mai întâi un proiect nou.",
|
||||
"custom_scripts": "Scripturi personalizate",
|
||||
"custom_scripts_card_description": "Adaugă scripturi de tracking și pixeli tuturor sondajelor cu link din acest spațiu de lucru.",
|
||||
"custom_scripts_description": "Scripturile vor fi injectate în <head> pe toate paginile sondajelor cu link.",
|
||||
"custom_scripts_label": "Scripturi HTML",
|
||||
"custom_scripts_placeholder": "<!-- Lipește aici scripturile tale de tracking -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
|
||||
"custom_scripts_updated_successfully": "Scripturile personalizate au fost actualizate cu succes",
|
||||
"custom_scripts_warning": "Scripturile se execută cu acces complet la browser. Adaugă doar scripturi din surse de încredere.",
|
||||
"delete_workspace": "Șterge proiectul",
|
||||
"delete_workspace_confirmation": "Sigur vrei să ștergi {projectName}? Această acțiune nu poate fi anulată.",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "Șterge {projectName} incl. toate sondajele, răspunsurile, persoanele, acțiunile și atributele.",
|
||||
|
||||
@@ -1690,6 +1690,22 @@
|
||||
"url_encryption_description": "Отключайте только если нужно задать собственный одноразовый ID.",
|
||||
"url_encryption_label": "Шифрование URL для одноразового ID"
|
||||
},
|
||||
"custom_html": {
|
||||
"add_mode_description": "Скрипты опроса будут выполняться дополнительно к скриптам на уровне рабочего пространства.",
|
||||
"add_to_workspace": "Добавить к скриптам рабочего пространства",
|
||||
"description": "Добавьте трекинговые скрипты и пиксели в этот опрос",
|
||||
"nav_title": "Пользовательский HTML",
|
||||
"no_workspace_scripts": "Скрипты на уровне рабочего пространства не настроены. Вы можете добавить их в настройках рабочего пространства → Общие.",
|
||||
"placeholder": "<!-- Вставьте сюда ваши трекинговые скрипты -->\n<script>\n // Google Tag Manager, Analytics и др.\n</script>",
|
||||
"replace_mode_description": "Будут выполняться только скрипты опроса. Скрипты рабочего пространства будут проигнорированы. Оставьте пустым, чтобы не загружать скрипты.",
|
||||
"replace_workspace": "Заменить скрипты рабочего пространства",
|
||||
"saved_successfully": "Пользовательские скрипты успешно сохранены",
|
||||
"script_mode": "Режим скриптов",
|
||||
"security_warning": "Скрипты выполняются с полным доступом к браузеру. Добавляйте только скрипты из доверенных источников.",
|
||||
"survey_scripts_description": "Добавьте пользовательский HTML для внедрения в <head> этой страницы опроса.",
|
||||
"survey_scripts_label": "Скрипты, специфичные для опроса",
|
||||
"workspace_scripts_label": "Скрипты рабочего пространства (унаследованные)"
|
||||
},
|
||||
"dynamic_popup": {
|
||||
"alert_button": "Редактировать опрос",
|
||||
"alert_description": "Этот опрос сейчас настроен как опрос по ссылке, что не поддерживает динамические pop-up окна. Вы можете изменить это на вкладке настроек редактора опроса.",
|
||||
@@ -1929,6 +1945,13 @@
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_workspace": "Это ваш единственный рабочий проект, его нельзя удалить. Сначала создайте новый проект.",
|
||||
"custom_scripts": "Пользовательские скрипты",
|
||||
"custom_scripts_card_description": "Добавьте трекинговые скрипты и пиксели ко всем опросам по ссылке в этом рабочем пространстве.",
|
||||
"custom_scripts_description": "Скрипты будут внедряться в <head> всех страниц опросов по ссылке.",
|
||||
"custom_scripts_label": "HTML-скрипты",
|
||||
"custom_scripts_placeholder": "<!-- Вставьте сюда ваши трекинговые скрипты -->\n<script>\n // Google Tag Manager, Analytics и др.\n</script>",
|
||||
"custom_scripts_updated_successfully": "Пользовательские скрипты успешно обновлены",
|
||||
"custom_scripts_warning": "Скрипты выполняются с полным доступом к браузеру. Добавляйте только скрипты из доверенных источников.",
|
||||
"delete_workspace": "Удалить рабочий проект",
|
||||
"delete_workspace_confirmation": "Вы уверены, что хотите удалить {projectName}? Это действие необратимо.",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "Удалить {projectName} вместе со всеми опросами, ответами, пользователями, действиями и атрибутами.",
|
||||
|
||||
@@ -1690,6 +1690,22 @@
|
||||
"url_encryption_description": "Inaktivera endast om du behöver ange ett anpassat engångs-ID.",
|
||||
"url_encryption_label": "URL-kryptering av engångs-ID"
|
||||
},
|
||||
"custom_html": {
|
||||
"add_mode_description": "Undersökningsskript kommer att köras utöver arbetsytans skript.",
|
||||
"add_to_workspace": "Lägg till i arbetsytans skript",
|
||||
"description": "Lägg till spårningsskript och pixlar i denna undersökning",
|
||||
"nav_title": "Anpassad HTML",
|
||||
"no_workspace_scripts": "Inga arbetsytans skript har konfigurerats. Du kan lägga till dem i Arbetsytans inställningar → Allmänt.",
|
||||
"placeholder": "<!-- Klistra in dina spårningsskript här -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
|
||||
"replace_mode_description": "Endast undersökningsskript kommer att köras. Arbetsytans skript ignoreras. Lämna tomt för att inte ladda några skript.",
|
||||
"replace_workspace": "Ersätt arbetsytans skript",
|
||||
"saved_successfully": "Anpassade skript har sparats",
|
||||
"script_mode": "Skriptläge",
|
||||
"security_warning": "Skript körs med full åtkomst till webbläsaren. Lägg endast till skript från betrodda källor.",
|
||||
"survey_scripts_description": "Lägg till anpassad HTML för att injicera i <head> på denna undersökningssida.",
|
||||
"survey_scripts_label": "Undersökningsspecifika skript",
|
||||
"workspace_scripts_label": "Arbetsytans skript (ärvda)"
|
||||
},
|
||||
"dynamic_popup": {
|
||||
"alert_button": "Redigera enkät",
|
||||
"alert_description": "Denna enkät är för närvarande konfigurerad som en länkenkät, vilket inte stöder dynamiska popup-fönster. Du kan ändra detta i inställningsfliken i enkätredigeraren.",
|
||||
@@ -1929,6 +1945,13 @@
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_workspace": "Detta är din enda arbetsyta, den kan inte tas bort. Skapa först en ny arbetsyta.",
|
||||
"custom_scripts": "Anpassade skript",
|
||||
"custom_scripts_card_description": "Lägg till spårningsskript och pixlar i alla länkundersökningar i denna arbetsyta.",
|
||||
"custom_scripts_description": "Skript kommer att injiceras i <head> på alla länkundersökningssidor.",
|
||||
"custom_scripts_label": "HTML-skript",
|
||||
"custom_scripts_placeholder": "<!-- Klistra in dina spårningsskript här -->\n<script>\n // Google Tag Manager, Analytics, etc.\n</script>",
|
||||
"custom_scripts_updated_successfully": "Anpassade skript har uppdaterats",
|
||||
"custom_scripts_warning": "Skript körs med full åtkomst till webbläsaren. Lägg endast till skript från betrodda källor.",
|
||||
"delete_workspace": "Ta bort arbetsyta",
|
||||
"delete_workspace_confirmation": "Är du säker på att du vill ta bort {projectName}? Denna åtgärd kan inte ångras.",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "Ta bort {projectName} inkl. alla enkäter, svar, personer, åtgärder och attribut.",
|
||||
|
||||
@@ -1690,6 +1690,22 @@
|
||||
"url_encryption_description": "仅在 需要 设置 自定义 单次使用 ID 时 才 禁用。",
|
||||
"url_encryption_label": "单次 使用 ID 的 URL 加密"
|
||||
},
|
||||
"custom_html": {
|
||||
"add_mode_description": "调查脚本将在工作区级脚本的基础上运行。",
|
||||
"add_to_workspace": "添加到工作区脚本",
|
||||
"description": "为此调查添加跟踪脚本和像素代码",
|
||||
"nav_title": "自定义 HTML",
|
||||
"no_workspace_scripts": "尚未配置工作区级脚本。你可以在工作区设置 → 常规中添加。",
|
||||
"placeholder": "<!-- 在此粘贴你的跟踪脚本 -->\n<script>\n // Google Tag Manager、Analytics 等\n</script>",
|
||||
"replace_mode_description": "仅运行调查脚本,工作区脚本将被忽略。保持为空则不加载任何脚本。",
|
||||
"replace_workspace": "替换工作区脚本",
|
||||
"saved_successfully": "自定义脚本保存成功",
|
||||
"script_mode": "脚本模式",
|
||||
"security_warning": "脚本将以完整浏览器权限执行。请仅添加来自可信来源的脚本。",
|
||||
"survey_scripts_description": "添加自定义 HTML 注入到此调查页面的<head>中。",
|
||||
"survey_scripts_label": "调查专用脚本",
|
||||
"workspace_scripts_label": "工作区脚本(继承)"
|
||||
},
|
||||
"dynamic_popup": {
|
||||
"alert_button": "编辑 survey",
|
||||
"alert_description": "此 问卷 当前 配置 为 链接 问卷, 不 支持 动态 弹出 窗。 您 可以 在 问卷 编辑器 的 设置 选项 中 进行 修改。",
|
||||
@@ -1929,6 +1945,13 @@
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_workspace": "这是您唯一的工作区,无法删除。请先创建一个新工作区。",
|
||||
"custom_scripts": "自定义脚本",
|
||||
"custom_scripts_card_description": "为此工作区内所有链接调查添加跟踪脚本和像素代码。",
|
||||
"custom_scripts_description": "脚本将被注入到所有链接调查页面的<head>中。",
|
||||
"custom_scripts_label": "HTML 脚本",
|
||||
"custom_scripts_placeholder": "<!-- 在此粘贴你的跟踪脚本 -->\n<script>\n // Google Tag Manager、Analytics 等\n</script>",
|
||||
"custom_scripts_updated_successfully": "自定义脚本更新成功",
|
||||
"custom_scripts_warning": "脚本将以完整浏览器权限执行。请仅添加来自可信来源的脚本。",
|
||||
"delete_workspace": "删除工作区",
|
||||
"delete_workspace_confirmation": "您确定要删除 {projectName} 吗?此操作无法撤销。",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "删除 {projectName},包括所有调查、回应、人员、动作和属性。",
|
||||
|
||||
@@ -1690,6 +1690,22 @@
|
||||
"url_encryption_description": "僅在需要設定自訂一次性 ID 時停用",
|
||||
"url_encryption_label": "單次使用 ID 的 URL 加密"
|
||||
},
|
||||
"custom_html": {
|
||||
"add_mode_description": "調查問卷腳本將會與工作區層級的腳本一同執行。",
|
||||
"add_to_workspace": "加入至工作區腳本",
|
||||
"description": "將追蹤腳本與像素碼加入此調查問卷",
|
||||
"nav_title": "自訂 HTML",
|
||||
"no_workspace_scripts": "尚未設定工作區層級腳本。您可以在「工作區設定」→「一般」中新增。",
|
||||
"placeholder": "<!-- 請在此貼上您的追蹤腳本 -->\n<script>\n // Google Tag Manager、Analytics 等\n</script>",
|
||||
"replace_mode_description": "僅執行調查問卷腳本,將忽略工作區腳本。若不需載入任何腳本,請保持空白。",
|
||||
"replace_workspace": "取代工作區腳本",
|
||||
"saved_successfully": "自訂腳本已成功儲存",
|
||||
"script_mode": "腳本模式",
|
||||
"security_warning": "腳本將以完整瀏覽器權限執行。請僅加入來自可信來源的腳本。",
|
||||
"survey_scripts_description": "新增自訂 HTML 以注入至此調查問卷頁面的 <head>。",
|
||||
"survey_scripts_label": "調查問卷專屬腳本",
|
||||
"workspace_scripts_label": "工作區腳本(繼承)"
|
||||
},
|
||||
"dynamic_popup": {
|
||||
"alert_button": "編輯 問卷",
|
||||
"alert_description": "此 問卷 目前 被 設定 為 連結 問卷,不 支援 動態 彈出窗口。您 可 在 問卷 編輯器 的 設定 標籤 中 進行 更改。",
|
||||
@@ -1929,6 +1945,13 @@
|
||||
},
|
||||
"general": {
|
||||
"cannot_delete_only_workspace": "這是您唯一的工作區,無法刪除。請先建立新的工作區。",
|
||||
"custom_scripts": "自訂腳本",
|
||||
"custom_scripts_card_description": "將追蹤腳本與像素碼加入此工作區內所有連結調查問卷。",
|
||||
"custom_scripts_description": "腳本將注入至所有連結調查問卷頁面的 <head>。",
|
||||
"custom_scripts_label": "HTML 腳本",
|
||||
"custom_scripts_placeholder": "<!-- 請在此貼上您的追蹤腳本 -->\n<script>\n // Google Tag Manager、Analytics 等\n</script>",
|
||||
"custom_scripts_updated_successfully": "自訂腳本已成功更新",
|
||||
"custom_scripts_warning": "腳本將以完整瀏覽器權限執行。請僅加入來自可信來源的腳本。",
|
||||
"delete_workspace": "刪除工作區",
|
||||
"delete_workspace_confirmation": "您確定要刪除 {projectName} 嗎?此操作無法復原。",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "刪除 {projectName}(包含所有問卷、回應、人員、操作和屬性)。",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
vi.mock("@/lib/env", () => ({
|
||||
env: {
|
||||
ENTERPRISE_LICENSE_KEY: "test-license-key",
|
||||
ENVIRONMENT: "production",
|
||||
VERCEL_URL: "some.vercel.url",
|
||||
FORMBRICKS_COM_URL: "https://app.formbricks.com",
|
||||
HTTPS_PROXY: undefined,
|
||||
@@ -690,4 +691,61 @@ describe("License Core Logic", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Environment-based endpoint selection", () => {
|
||||
test("should use staging endpoint when ENVIRONMENT is staging", async () => {
|
||||
vi.resetModules();
|
||||
vi.doMock("@/lib/env", () => ({
|
||||
env: {
|
||||
ENTERPRISE_LICENSE_KEY: "test-license-key",
|
||||
ENVIRONMENT: "staging",
|
||||
HTTPS_PROXY: undefined,
|
||||
HTTP_PROXY: undefined,
|
||||
},
|
||||
}));
|
||||
|
||||
const fetch = (await import("node-fetch")).default as Mock;
|
||||
|
||||
// Mock cache.withCache to execute the function (simulating cache miss)
|
||||
mockCache.withCache.mockImplementation(async (fn) => await fn());
|
||||
|
||||
fetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: {
|
||||
status: "active",
|
||||
features: {
|
||||
isMultiOrgEnabled: true,
|
||||
projects: 5,
|
||||
twoFactorAuth: true,
|
||||
sso: true,
|
||||
whitelabel: true,
|
||||
removeBranding: true,
|
||||
contacts: true,
|
||||
ai: true,
|
||||
saml: true,
|
||||
spamProtection: true,
|
||||
auditLogs: true,
|
||||
multiLanguageSurveys: true,
|
||||
accessControl: true,
|
||||
quotas: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
} as any);
|
||||
|
||||
// Re-import the module to apply the new mock
|
||||
const { fetchLicense } = await import("./license");
|
||||
await fetchLicense();
|
||||
|
||||
// Verify the staging endpoint was called
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"https://staging.ee.formbricks.com/api/licenses/check",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,10 @@ const CONFIG = {
|
||||
RETRY_DELAY_MS: 1000,
|
||||
},
|
||||
API: {
|
||||
ENDPOINT: "https://ee.formbricks.com/api/licenses/check",
|
||||
ENDPOINT:
|
||||
env.ENVIRONMENT === "staging"
|
||||
? "https://staging.ee.formbricks.com/api/licenses/check"
|
||||
: "https://ee.formbricks.com/api/licenses/check",
|
||||
TIMEOUT_MS: 5000,
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -153,6 +153,7 @@ export const getEnvironmentWithRelations = reactCache(async (environmentId: stri
|
||||
darkOverlay: true,
|
||||
styling: true,
|
||||
logo: true,
|
||||
customHeadScripts: true,
|
||||
// All project environments
|
||||
environments: {
|
||||
select: {
|
||||
@@ -222,6 +223,7 @@ export const getEnvironmentWithRelations = reactCache(async (environmentId: stri
|
||||
darkOverlay: data.project.darkOverlay,
|
||||
styling: data.project.styling,
|
||||
logo: data.project.logo,
|
||||
customHeadScripts: data.project.customHeadScripts,
|
||||
environments: data.project.environments,
|
||||
},
|
||||
organization: {
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangleIcon } from "lucide-react";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { z } from "zod";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormProvider,
|
||||
} from "@/modules/ui/components/form";
|
||||
import { updateProjectAction } from "../../actions";
|
||||
|
||||
interface CustomScriptsFormProps {
|
||||
project: TProject;
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
const ZCustomScriptsInput = z.object({
|
||||
customHeadScripts: z.string().nullish(),
|
||||
});
|
||||
|
||||
type TCustomScriptsFormValues = z.infer<typeof ZCustomScriptsInput>;
|
||||
|
||||
export const CustomScriptsForm: React.FC<CustomScriptsFormProps> = ({ project, isReadOnly }) => {
|
||||
const { t } = useTranslation();
|
||||
const form = useForm<TCustomScriptsFormValues>({
|
||||
defaultValues: {
|
||||
customHeadScripts: project.customHeadScripts ?? "",
|
||||
},
|
||||
resolver: zodResolver(ZCustomScriptsInput),
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
const { isDirty, isSubmitting } = form.formState;
|
||||
|
||||
const updateCustomScripts: SubmitHandler<TCustomScriptsFormValues> = async (data) => {
|
||||
try {
|
||||
const updatedProjectResponse = await updateProjectAction({
|
||||
projectId: project.id,
|
||||
data: {
|
||||
customHeadScripts: data.customHeadScripts || null,
|
||||
},
|
||||
});
|
||||
if (updatedProjectResponse?.data) {
|
||||
toast.success(t("environments.workspace.general.custom_scripts_updated_successfully"));
|
||||
form.reset({ customHeadScripts: updatedProjectResponse.data.customHeadScripts ?? "" });
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(updatedProjectResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
} catch {
|
||||
toast.error(t("common.something_went_wrong_please_try_again"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormProvider {...form}>
|
||||
<form className="flex w-full flex-col space-y-4" onSubmit={form.handleSubmit(updateCustomScripts)}>
|
||||
<Alert variant="warning" className="flex items-start gap-2">
|
||||
<AlertTriangleIcon className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<AlertDescription>{t("environments.workspace.general.custom_scripts_warning")}</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="customHeadScripts"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel htmlFor="customHeadScripts">
|
||||
{t("environments.workspace.general.custom_scripts_label")}
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
{t("environments.workspace.general.custom_scripts_description")}
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<textarea
|
||||
id="customHeadScripts"
|
||||
rows={8}
|
||||
placeholder={t("environments.workspace.general.custom_scripts_placeholder")}
|
||||
className={cn(
|
||||
"focus:border-brand-dark flex w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 font-mono text-xs text-slate-800 placeholder:text-slate-400 focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
isReadOnly && "bg-slate-50"
|
||||
)}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
className="w-fit"
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting || !isDirty || isReadOnly}>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</form>
|
||||
</FormProvider>
|
||||
{isReadOnly && (
|
||||
<Alert variant="warning" className="mt-4">
|
||||
<AlertDescription>
|
||||
{t("common.only_owners_managers_and_manage_access_members_can_perform_this_action")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import packageJson from "@/package.json";
|
||||
import { CustomScriptsForm } from "./components/custom-scripts-form";
|
||||
import { DeleteProject } from "./components/delete-project";
|
||||
import { EditProjectNameForm } from "./components/edit-project-name-form";
|
||||
import { EditWaitingTimeForm } from "./components/edit-waiting-time-form";
|
||||
@@ -39,6 +40,13 @@ export const GeneralSettingsPage = async (props: { params: Promise<{ environment
|
||||
description={t("environments.workspace.general.recontact_waiting_time_settings_description")}>
|
||||
<EditWaitingTimeForm project={project} isReadOnly={isReadOnly} />
|
||||
</SettingsCard>
|
||||
{!IS_FORMBRICKS_CLOUD && (
|
||||
<SettingsCard
|
||||
title={t("environments.workspace.general.custom_scripts")}
|
||||
description={t("environments.workspace.general.custom_scripts_card_description")}>
|
||||
<CustomScriptsForm project={project} isReadOnly={!isOwnerOrManager} />
|
||||
</SettingsCard>
|
||||
)}
|
||||
<SettingsCard
|
||||
title={t("environments.workspace.general.delete_workspace")}
|
||||
description={t("environments.workspace.general.delete_workspace_settings_description")}>
|
||||
|
||||
@@ -28,6 +28,7 @@ const selectProject = {
|
||||
environments: true,
|
||||
styling: true,
|
||||
logo: true,
|
||||
customHeadScripts: true,
|
||||
};
|
||||
|
||||
export const updateProject = async (
|
||||
|
||||
@@ -112,7 +112,7 @@ export const InviteMembers = ({ IS_SMTP_CONFIGURED, organizationId }: InviteMemb
|
||||
<div className="relative">
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={`Full Name (optional)`}
|
||||
placeholder={t("common.full_name")}
|
||||
className="w-80"
|
||||
isInvalid={Boolean(error?.message)}
|
||||
/>
|
||||
|
||||
@@ -111,7 +111,7 @@ export const CTAElementForm = ({
|
||||
description={t("environments.surveys.edit.button_external_description")}
|
||||
childBorder
|
||||
customContainerClass="p-0 mt-4">
|
||||
<div className="flex flex-1 flex-col gap-2 px-4 pb-4 pt-1">
|
||||
<div className="flex flex-1 flex-col gap-2 px-4 pt-1 pb-4">
|
||||
<ElementFormInput
|
||||
id="ctaButtonLabel"
|
||||
value={element.ctaButtonLabel}
|
||||
@@ -133,6 +133,7 @@ export const CTAElementForm = ({
|
||||
<Input
|
||||
id="buttonUrl"
|
||||
name="buttonUrl"
|
||||
className="mt-1 bg-white"
|
||||
value={element.buttonUrl}
|
||||
placeholder="https://website.com"
|
||||
onChange={(e) => updateElement(elementIdx, { buttonUrl: e.target.value })}
|
||||
|
||||
@@ -271,6 +271,7 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> =>
|
||||
...prismaSurvey, // Properties from prismaSurvey
|
||||
displayPercentage: Number(prismaSurvey.displayPercentage) || null,
|
||||
segment: surveySegment,
|
||||
customHeadScriptsMode: prismaSurvey.customHeadScriptsMode,
|
||||
};
|
||||
|
||||
return modifiedSurvey;
|
||||
|
||||
@@ -40,6 +40,8 @@ export const selectSurvey = {
|
||||
isBackButtonHidden: true,
|
||||
metadata: true,
|
||||
slug: true,
|
||||
customHeadScripts: true,
|
||||
customHeadScriptsMode: true,
|
||||
languages: {
|
||||
select: {
|
||||
default: true,
|
||||
|
||||
@@ -20,6 +20,7 @@ export const transformPrismaSurvey = <T extends TSurvey | TJsEnvironmentStateSur
|
||||
...surveyPrisma,
|
||||
displayPercentage: Number(surveyPrisma.displayPercentage) || null,
|
||||
segment,
|
||||
customHeadScriptsMode: surveyPrisma.customHeadScriptsMode,
|
||||
} as T;
|
||||
|
||||
return transformedSurvey;
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface CustomScriptsInjectorProps {
|
||||
projectScripts?: string | null;
|
||||
surveyScripts?: string | null;
|
||||
scriptsMode?: TSurvey["customHeadScriptsMode"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects custom HTML scripts into the document head for link surveys.
|
||||
* Supports merging project and survey scripts or replacing project scripts with survey scripts.
|
||||
*
|
||||
* @param projectScripts - Scripts configured at the workspace/project level
|
||||
* @param surveyScripts - Scripts configured at the survey level
|
||||
* @param scriptsMode - "add" merges both, "replace" uses only survey scripts
|
||||
*/
|
||||
export const CustomScriptsInjector = ({
|
||||
projectScripts,
|
||||
surveyScripts,
|
||||
scriptsMode,
|
||||
}: CustomScriptsInjectorProps) => {
|
||||
const injectedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Prevent double injection in React strict mode
|
||||
if (injectedRef.current) return;
|
||||
|
||||
// Determine which scripts to inject based on mode
|
||||
let scriptsToInject: string;
|
||||
|
||||
if (scriptsMode === "replace" && surveyScripts) {
|
||||
// Replace mode: only use survey scripts
|
||||
scriptsToInject = surveyScripts;
|
||||
} else {
|
||||
// Add mode (default): merge project and survey scripts
|
||||
scriptsToInject = [projectScripts, surveyScripts].filter(Boolean).join("\n");
|
||||
}
|
||||
|
||||
if (!scriptsToInject.trim()) return;
|
||||
|
||||
try {
|
||||
// Create a temporary container to parse the HTML
|
||||
const container = document.createElement("div");
|
||||
container.innerHTML = scriptsToInject;
|
||||
|
||||
// Process and inject script elements
|
||||
const scripts = container.querySelectorAll("script");
|
||||
scripts.forEach((script) => {
|
||||
const newScript = document.createElement("script");
|
||||
|
||||
// Copy all attributes (src, async, defer, type, etc.)
|
||||
Array.from(script.attributes).forEach((attr) => {
|
||||
newScript.setAttribute(attr.name, attr.value);
|
||||
});
|
||||
|
||||
// Copy inline script content
|
||||
if (script.textContent) {
|
||||
newScript.textContent = script.textContent;
|
||||
}
|
||||
|
||||
document.head.appendChild(newScript);
|
||||
});
|
||||
|
||||
// Process and inject non-script elements (noscript, meta, link, style, etc.)
|
||||
const nonScripts = container.querySelectorAll(":not(script)");
|
||||
nonScripts.forEach((el) => {
|
||||
const clonedEl = el.cloneNode(true) as Element;
|
||||
document.head.appendChild(clonedEl);
|
||||
});
|
||||
|
||||
injectedRef.current = true;
|
||||
} catch (error) {
|
||||
// Log error but don't break the survey - self-hosted admins can check console
|
||||
console.warn("[Formbricks] Error injecting custom scripts:", error);
|
||||
}
|
||||
}, [projectScripts, surveyScripts, scriptsMode]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -13,7 +13,7 @@ import { OTPInput } from "@/modules/ui/components/otp-input";
|
||||
|
||||
interface PinScreenProps {
|
||||
surveyId: string;
|
||||
project: Pick<Project, "styling" | "logo" | "linkSurveyBranding">;
|
||||
project: Pick<Project, "styling" | "logo" | "linkSurveyBranding" | "customHeadScripts">;
|
||||
singleUseId?: string;
|
||||
singleUseResponse?: Pick<Response, "id" | "finished">;
|
||||
publicDomain: string;
|
||||
|
||||
@@ -7,13 +7,14 @@ import { TProjectStyling } from "@formbricks/types/project";
|
||||
import { TResponseData } from "@formbricks/types/responses";
|
||||
import { TSurvey, TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { CustomScriptsInjector } from "@/modules/survey/link/components/custom-scripts-injector";
|
||||
import { LinkSurveyWrapper } from "@/modules/survey/link/components/link-survey-wrapper";
|
||||
import { getPrefillValue } from "@/modules/survey/link/lib/prefill";
|
||||
import { SurveyInline } from "@/modules/ui/components/survey";
|
||||
|
||||
interface SurveyClientWrapperProps {
|
||||
survey: TSurvey;
|
||||
project: Pick<Project, "styling" | "logo" | "linkSurveyBranding">;
|
||||
project: Pick<Project, "styling" | "logo" | "linkSurveyBranding" | "customHeadScripts">;
|
||||
styling: TProjectStyling | TSurveyStyling;
|
||||
publicDomain: string;
|
||||
responseCount?: number;
|
||||
@@ -117,52 +118,62 @@ export const SurveyClientWrapper = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<LinkSurveyWrapper
|
||||
project={project}
|
||||
surveyId={survey.id}
|
||||
isWelcomeCardEnabled={survey.welcomeCard.enabled}
|
||||
isPreview={isPreview}
|
||||
surveyType={survey.type}
|
||||
determineStyling={() => styling}
|
||||
handleResetSurvey={handleResetSurvey}
|
||||
isEmbed={isEmbed}
|
||||
publicDomain={publicDomain}
|
||||
IS_FORMBRICKS_CLOUD={IS_FORMBRICKS_CLOUD}
|
||||
IMPRINT_URL={IMPRINT_URL}
|
||||
PRIVACY_URL={PRIVACY_URL}
|
||||
isBrandingEnabled={project.linkSurveyBranding}>
|
||||
<SurveyInline
|
||||
appUrl={publicDomain}
|
||||
environmentId={survey.environmentId}
|
||||
isPreviewMode={isPreview}
|
||||
survey={survey}
|
||||
styling={styling}
|
||||
languageCode={languageCode}
|
||||
isBrandingEnabled={project.linkSurveyBranding}
|
||||
shouldResetQuestionId={false}
|
||||
autoFocus={autoFocus}
|
||||
prefillResponseData={prefillValue}
|
||||
skipPrefilled={skipPrefilled}
|
||||
responseCount={responseCount}
|
||||
getSetBlockId={(f: (value: string) => void) => {
|
||||
setBlockId = f;
|
||||
}}
|
||||
getSetResponseData={(f: (value: TResponseData) => void) => {
|
||||
setResponseData = f;
|
||||
}}
|
||||
startAtQuestionId={startAt && isStartAtValid ? startAt : undefined}
|
||||
fullSizeCards={isEmbed}
|
||||
hiddenFieldsRecord={{
|
||||
...hiddenFieldsRecord,
|
||||
...getVerifiedEmail,
|
||||
}}
|
||||
singleUseId={singleUseId}
|
||||
singleUseResponseId={singleUseResponseId}
|
||||
getSetIsResponseSendingFinished={(_f: (value: boolean) => void) => {}}
|
||||
contactId={contactId}
|
||||
recaptchaSiteKey={recaptchaSiteKey}
|
||||
isSpamProtectionEnabled={isSpamProtectionEnabled}
|
||||
/>
|
||||
</LinkSurveyWrapper>
|
||||
<>
|
||||
{/* Inject custom scripts for tracking/analytics (self-hosted only) */}
|
||||
{!IS_FORMBRICKS_CLOUD && !isPreview && (
|
||||
<CustomScriptsInjector
|
||||
projectScripts={project.customHeadScripts}
|
||||
surveyScripts={survey.customHeadScripts}
|
||||
scriptsMode={survey.customHeadScriptsMode}
|
||||
/>
|
||||
)}
|
||||
<LinkSurveyWrapper
|
||||
project={project}
|
||||
surveyId={survey.id}
|
||||
isWelcomeCardEnabled={survey.welcomeCard.enabled}
|
||||
isPreview={isPreview}
|
||||
surveyType={survey.type}
|
||||
determineStyling={() => styling}
|
||||
handleResetSurvey={handleResetSurvey}
|
||||
isEmbed={isEmbed}
|
||||
publicDomain={publicDomain}
|
||||
IS_FORMBRICKS_CLOUD={IS_FORMBRICKS_CLOUD}
|
||||
IMPRINT_URL={IMPRINT_URL}
|
||||
PRIVACY_URL={PRIVACY_URL}
|
||||
isBrandingEnabled={project.linkSurveyBranding}>
|
||||
<SurveyInline
|
||||
appUrl={publicDomain}
|
||||
environmentId={survey.environmentId}
|
||||
isPreviewMode={isPreview}
|
||||
survey={survey}
|
||||
styling={styling}
|
||||
languageCode={languageCode}
|
||||
isBrandingEnabled={project.linkSurveyBranding}
|
||||
shouldResetQuestionId={false}
|
||||
autoFocus={autoFocus}
|
||||
prefillResponseData={prefillValue}
|
||||
skipPrefilled={skipPrefilled}
|
||||
responseCount={responseCount}
|
||||
getSetBlockId={(f: (value: string) => void) => {
|
||||
setBlockId = f;
|
||||
}}
|
||||
getSetResponseData={(f: (value: TResponseData) => void) => {
|
||||
setResponseData = f;
|
||||
}}
|
||||
startAtQuestionId={startAt && isStartAtValid ? startAt : undefined}
|
||||
fullSizeCards={isEmbed}
|
||||
hiddenFieldsRecord={{
|
||||
...hiddenFieldsRecord,
|
||||
...getVerifiedEmail,
|
||||
}}
|
||||
singleUseId={singleUseId}
|
||||
singleUseResponseId={singleUseResponseId}
|
||||
getSetIsResponseSendingFinished={(_f: (value: boolean) => void) => {}}
|
||||
contactId={contactId}
|
||||
recaptchaSiteKey={recaptchaSiteKey}
|
||||
isSpamProtectionEnabled={isSpamProtectionEnabled}
|
||||
/>
|
||||
</LinkSurveyWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -60,6 +60,10 @@ export const getSurveyWithMetadata = reactCache(async (surveyId: string) => {
|
||||
recaptcha: true,
|
||||
metadata: true,
|
||||
|
||||
// Custom scripts (self-hosted only)
|
||||
customHeadScripts: true,
|
||||
customHeadScriptsMode: true,
|
||||
|
||||
// Related data
|
||||
languages: {
|
||||
select: {
|
||||
|
||||
@@ -85,6 +85,7 @@ describe("getEnvironmentContextForLinkSurvey", () => {
|
||||
styling: true,
|
||||
logo: true,
|
||||
linkSurveyBranding: true,
|
||||
customHeadScripts: true,
|
||||
organizationId: true,
|
||||
organization: {
|
||||
select: {
|
||||
|
||||
@@ -16,7 +16,10 @@ import { validateInputs } from "@/lib/utils/validate";
|
||||
* deduplication within the same render cycle.
|
||||
*/
|
||||
|
||||
type TProjectForLinkSurvey = Pick<Project, "id" | "name" | "styling" | "logo" | "linkSurveyBranding">;
|
||||
type TProjectForLinkSurvey = Pick<
|
||||
Project,
|
||||
"id" | "name" | "styling" | "logo" | "linkSurveyBranding" | "customHeadScripts"
|
||||
>;
|
||||
|
||||
export interface TEnvironmentContextForLinkSurvey {
|
||||
project: TProjectForLinkSurvey;
|
||||
@@ -61,6 +64,7 @@ export const getEnvironmentContextForLinkSurvey = reactCache(
|
||||
styling: true,
|
||||
logo: true,
|
||||
linkSurveyBranding: true,
|
||||
customHeadScripts: true,
|
||||
organizationId: true,
|
||||
organization: {
|
||||
select: {
|
||||
@@ -91,6 +95,7 @@ export const getEnvironmentContextForLinkSurvey = reactCache(
|
||||
styling: environment.project.styling,
|
||||
logo: environment.project.logo,
|
||||
linkSurveyBranding: environment.project.linkSurveyBranding,
|
||||
customHeadScripts: environment.project.customHeadScripts,
|
||||
},
|
||||
organizationId: environment.project.organizationId,
|
||||
organizationBilling: environment.project.organization.billing,
|
||||
|
||||
@@ -61,6 +61,7 @@ describe("getProjectByEnvironmentId", () => {
|
||||
},
|
||||
},
|
||||
select: {
|
||||
customHeadScripts: true,
|
||||
linkSurveyBranding: true,
|
||||
logo: true,
|
||||
styling: true,
|
||||
|
||||
@@ -10,7 +10,10 @@ import { validateInputs } from "@/lib/utils/validate";
|
||||
export const getProjectByEnvironmentId = reactCache(
|
||||
async (
|
||||
environmentId: string
|
||||
): Promise<Pick<Project, "styling" | "logo" | "linkSurveyBranding" | "name"> | null> => {
|
||||
): Promise<Pick<
|
||||
Project,
|
||||
"styling" | "logo" | "linkSurveyBranding" | "name" | "customHeadScripts"
|
||||
> | null> => {
|
||||
validateInputs([environmentId, ZId]);
|
||||
|
||||
let projectPrisma;
|
||||
@@ -29,6 +32,7 @@ export const getProjectByEnvironmentId = reactCache(
|
||||
logo: true,
|
||||
linkSurveyBranding: true,
|
||||
name: true,
|
||||
customHeadScripts: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"generate-api-specs": "./scripts/openapi/generate.sh",
|
||||
"merge-client-endpoints": "tsx ./scripts/openapi/merge-client-endpoints.ts",
|
||||
"generate-and-merge-api-specs": "pnpm run generate-api-specs && pnpm run merge-client-endpoints",
|
||||
"i18n:generate": "npx lingo.dev@latest i18n"
|
||||
"i18n:generate": "npx lingo.dev@latest run && npx lingo.dev@latest lockfile --force"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.879.0",
|
||||
|
||||
@@ -101,7 +101,8 @@
|
||||
"xm-and-surveys/surveys/link-surveys/start-at-question",
|
||||
"xm-and-surveys/surveys/link-surveys/verify-email-before-survey",
|
||||
"xm-and-surveys/surveys/link-surveys/market-research-panel",
|
||||
"xm-and-surveys/surveys/link-surveys/pin-protected-surveys"
|
||||
"xm-and-surveys/surveys/link-surveys/pin-protected-surveys",
|
||||
"xm-and-surveys/surveys/link-surveys/custom-head-scripts"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
193
docs/xm-and-surveys/surveys/link-surveys/custom-head-scripts.mdx
Normal file
193
docs/xm-and-surveys/surveys/link-surveys/custom-head-scripts.mdx
Normal file
@@ -0,0 +1,193 @@
|
||||
---
|
||||
title: "Custom Head Scripts"
|
||||
description: "Add tracking pixels, analytics, or custom code to your link surveys for self-hosted instances."
|
||||
icon: "code"
|
||||
---
|
||||
|
||||
Custom Head Scripts allow you to inject custom HTML code into the `<head>` section of your **Link Surveys**. This is useful for adding tracking pixels, analytics scripts, chatbots, or any other third-party code.
|
||||
|
||||
<Note>
|
||||
Custom Head Scripts is only available for **Link Surveys on self-hosted instances**. This feature is not available for Website & App Surveys or on Formbricks Cloud.
|
||||
</Note>
|
||||
|
||||
## When to Use Custom Head Scripts
|
||||
|
||||
Use Custom Head Scripts when you need to:
|
||||
|
||||
- Add analytics tools (Google Analytics, Plausible, Mixpanel, etc.)
|
||||
- Integrate tracking pixels (Facebook Pixel, LinkedIn Insight Tag, etc.)
|
||||
- Include custom JavaScript for advanced survey behavior
|
||||
- Add third-party widgets or chatbots
|
||||
- Inject custom meta tags or stylesheets
|
||||
|
||||
## Configuration Guide
|
||||
|
||||
### Workspace-Level Scripts
|
||||
|
||||
These scripts apply to **all link surveys** in your workspace.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/xm-and-surveys/surveys/link-surveys/custom-head-scripts/workspace-setting.webp" alt="Custom Scripts in Workspace Settings" />
|
||||
</Frame>
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to Workspace Settings">
|
||||
Go to your Workspace Settings from the main navigation menu.
|
||||
</Step>
|
||||
|
||||
<Step title="Locate Custom Scripts Section">
|
||||
Scroll down to the **Custom Scripts** card in the General settings.
|
||||
</Step>
|
||||
|
||||
<Step title="Add Your Scripts">
|
||||
Paste your HTML code into the text area. You can include:
|
||||
|
||||
- `<script>` tags (inline or with `src` attribute)
|
||||
- `<meta>` tags
|
||||
- `<link>` tags for stylesheets
|
||||
- `<style>` tags
|
||||
- `<noscript>` tags
|
||||
|
||||
**Example:**
|
||||
|
||||
```html
|
||||
<!-- Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'GA_MEASUREMENT_ID');
|
||||
</script>
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Save Your Changes">
|
||||
Click the **Save** button to apply your custom scripts to all link surveys.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Survey-Level Scripts
|
||||
|
||||
Override or extend workspace scripts for **specific surveys**.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/xm-and-surveys/surveys/link-surveys/custom-head-scripts/share-survey-modal-setting.webp" alt="Custom HTML tab in Share Survey Modal" />
|
||||
</Frame>
|
||||
|
||||
<Steps>
|
||||
<Step title="Open Share Survey Modal">
|
||||
Navigate to your survey and click the **Share** button to open the share modal.
|
||||
</Step>
|
||||
|
||||
<Step title="Go to Custom HTML Tab">
|
||||
In the share modal, click on the **Custom HTML** tab under the "Share Settings" section.
|
||||
</Step>
|
||||
|
||||
<Step title="Choose Script Mode">
|
||||
Select how you want survey scripts to interact with workspace scripts:
|
||||
|
||||
- **Add to Workspace Scripts** - Survey scripts will be added **after** workspace scripts (both run)
|
||||
- **Replace Workspace Scripts** - Survey scripts will **replace** workspace scripts entirely (only survey scripts run)
|
||||
</Step>
|
||||
|
||||
<Step title="Add Survey-Specific Scripts">
|
||||
Paste your HTML code into the "Survey Scripts" text area.
|
||||
|
||||
**Example** (Facebook Pixel for a specific campaign):
|
||||
|
||||
```html
|
||||
<!-- Facebook Pixel for Campaign X -->
|
||||
<script>
|
||||
fbq('track', 'ViewContent', {
|
||||
content_name: 'Customer Satisfaction Survey',
|
||||
campaign: 'Q1-2024'
|
||||
});
|
||||
</script>
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Save Changes">
|
||||
Click **Save** to apply the survey-specific scripts.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
|
||||
## To keep in mind
|
||||
|
||||
<Warning>
|
||||
Custom scripts execute in the context of your survey pages. Only add scripts from trusted sources. Malicious scripts could compromise your survey data or user privacy.
|
||||
</Warning>
|
||||
|
||||
- **Scripts Don't Load in Preview Mode** — Custom Head Scripts are not loaded in preview mode (editor preview or `?preview=true`). This prevents analytics tracking and pixel triggers during testing. To test your scripts, publish your survey and view it through the actual link survey URL without the preview parameter.
|
||||
|
||||
- **Link Surveys Only** — Custom Head Scripts only work with link surveys. They are not available for app/website surveys, as those are embedded in your application and should use your application's existing script management.
|
||||
|
||||
- **Self-Hosted Only** — This feature is only available on self-hosted instances. Formbricks Cloud does not support Custom Head Scripts for security and performance reasons.
|
||||
|
||||
- **Permissions** — Only Owners, Managers, and members with Manage access can configure Custom Head Scripts.
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### Global Analytics (Workspace Level)
|
||||
|
||||
Set up Google Analytics for all surveys in your workspace:
|
||||
|
||||
```html
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'GA_MEASUREMENT_ID');
|
||||
</script>
|
||||
```
|
||||
|
||||
### Campaign-Specific Tracking (Survey Level with Add Mode)
|
||||
|
||||
Add Facebook Pixel tracking for a specific marketing campaign:
|
||||
|
||||
**Workspace Scripts:** Google Analytics (as above)
|
||||
|
||||
**Survey Scripts:**
|
||||
```html
|
||||
<script>
|
||||
!function(f,b,e,v,n,t,s)
|
||||
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
||||
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
|
||||
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
|
||||
n.queue=[];t=b.createElement(e);t.async=!0;
|
||||
t.src=v;s=b.getElementsByTagName(e)[0];
|
||||
s.parentNode.insertBefore(t,s)}(window, document,'script',
|
||||
'https://connect.facebook.net/en_US/fbevents.js');
|
||||
fbq('init', 'YOUR_PIXEL_ID');
|
||||
fbq('track', 'PageView');
|
||||
</script>
|
||||
```
|
||||
|
||||
**Result:** Both Google Analytics and Facebook Pixel run on this survey.
|
||||
|
||||
|
||||
### Custom Fonts (Workspace Level)
|
||||
|
||||
Load custom fonts for all your surveys:
|
||||
|
||||
```html
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If your scripts aren't loading, check:
|
||||
|
||||
1. **Is it a self-hosted instance?** Custom scripts only work on self-hosted Formbricks.
|
||||
2. **Are you in preview mode?** Scripts don't load in preview—test with the actual survey link.
|
||||
3. **Check the browser console** for JavaScript errors that might prevent script execution.
|
||||
4. **Verify your HTML syntax** is correct (properly closed tags, valid attributes).
|
||||
@@ -0,0 +1,9 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "public"."SurveyScriptMode" AS ENUM ('add', 'replace');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Project" ADD COLUMN "customHeadScripts" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Survey" ADD COLUMN "customHeadScripts" TEXT,
|
||||
ADD COLUMN "customHeadScriptsMode" "public"."SurveyScriptMode" DEFAULT 'add';
|
||||
@@ -312,6 +312,11 @@ enum displayOptions {
|
||||
respondMultiple
|
||||
}
|
||||
|
||||
enum SurveyScriptMode {
|
||||
add
|
||||
replace
|
||||
}
|
||||
|
||||
/// Represents a complete survey configuration including questions, styling, and display rules.
|
||||
/// Core model for the survey functionality in Formbricks.
|
||||
///
|
||||
@@ -324,6 +329,8 @@ enum displayOptions {
|
||||
/// @property displayOption - Rules for how often the survey can be shown
|
||||
/// @property triggers - Actions that can trigger this survey
|
||||
/// @property attributeFilters - Rules for targeting specific contacts
|
||||
/// @property customHeadScripts - Survey-specific custom HTML scripts (self-hosted only)
|
||||
/// @property customHeadScriptsMode - "add" (merge with project) or "replace" (override project)
|
||||
model Survey {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
@@ -390,6 +397,9 @@ model Survey {
|
||||
|
||||
slug String? @unique
|
||||
|
||||
customHeadScripts String?
|
||||
customHeadScriptsMode SurveyScriptMode? @default(add)
|
||||
|
||||
@@index([environmentId, updatedAt])
|
||||
@@index([segmentId])
|
||||
}
|
||||
@@ -620,6 +630,7 @@ model Project {
|
||||
/// [Logo]
|
||||
logo Json?
|
||||
projectTeams ProjectTeam[]
|
||||
customHeadScripts String? // Custom HTML scripts for link surveys (self-hosted only)
|
||||
|
||||
@@unique([organizationId, name])
|
||||
@@index([organizationId])
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"clean": "rimraf .turbo node_modules dist",
|
||||
"test": "vitest run",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"i18n:generate": "npx lingo.dev@latest i18n"
|
||||
"i18n:generate": "npx lingo.dev@latest run && npx lingo.dev@latest lockfile --force"
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/embed-snippet": "1.3.3",
|
||||
|
||||
@@ -69,6 +69,7 @@ export const ZProject = z.object({
|
||||
environments: z.array(ZEnvironment),
|
||||
languages: z.array(ZLanguage),
|
||||
logo: ZLogo.nullish(),
|
||||
customHeadScripts: z.string().nullish(),
|
||||
});
|
||||
|
||||
export type TProject = z.infer<typeof ZProject>;
|
||||
@@ -88,6 +89,7 @@ export const ZProjectUpdateInput = z.object({
|
||||
styling: ZProjectStyling.optional(),
|
||||
logo: ZLogo.optional(),
|
||||
teamIds: z.array(z.string()).optional(),
|
||||
customHeadScripts: z.string().nullish(),
|
||||
});
|
||||
|
||||
export type TProjectUpdateInput = z.infer<typeof ZProjectUpdateInput>;
|
||||
|
||||
@@ -900,6 +900,8 @@ export const ZSurvey = z
|
||||
languages: z.array(ZSurveyLanguage),
|
||||
metadata: ZSurveyMetadata,
|
||||
slug: ZSurveySlug.nullable(),
|
||||
customHeadScripts: z.string().nullish(),
|
||||
customHeadScriptsMode: z.enum(["add", "replace"]).nullish(),
|
||||
})
|
||||
.superRefine((survey, ctx) => {
|
||||
const { questions, blocks, languages, welcomeCard, endings, isBackButtonHidden } = survey;
|
||||
|
||||
@@ -163,6 +163,7 @@
|
||||
"EMAIL_VERIFICATION_DISABLED",
|
||||
"ENCRYPTION_KEY",
|
||||
"ENTERPRISE_LICENSE_KEY",
|
||||
"ENVIRONMENT",
|
||||
"GITHUB_ID",
|
||||
"GITHUB_SECRET",
|
||||
"GOOGLE_CLIENT_ID",
|
||||
|
||||
Reference in New Issue
Block a user